Skill: huangm199/zhipu-image (version 2.0.0)

--- name: zhipu-image description: 智谱 GLM-Image 网页端图片生成与下载。用于：检查 image.z.ai 登录态、必要时自动打开浏览器登录、抓取浏览器 Cookie、通过网页接口生成图片并下载到本地。适用于“用智谱生图”“生成一张图并保存/发送”“检查智谱登录状态”“自动打开智谱登录页”等场景。 --- # zhipu-image 这个 skill 走的是**网页登录态**路线，不是独立 API Key 路线。 ## 主入口 - `scripts/zhipu_api.js`：检查 session、必要时自动打开登录页、生成图片并下载 - `scripts/network_monitor.js`：历史抓包辅助脚本，仅在逆向接口变化时参考 ## 一条命令先准备好 ```bash npm run ready ``` 或： ```bash node zhipu_api.js ready ``` 它会自动完成： 1. 检查本地 `~/.zhipu_image_session.json` 2. 必要时尝试抓当前浏览器登录态 3. 如果还没登录，则自动打开 `https://image.z.ai/` 4. 等待登录完成 5. 最后输出 `available` / `unavailable` ## 生成图片 ```bash node zhipu_api.js generate "一只可爱老虎吉祥物，圆润，萌系插画风" ``` 也可以省略 `generate`，直接把提示词传进去： ```bash node zhipu_api.js "一只可爱老虎吉祥物，圆润，萌系插画风" ``` 默认会： - 自动检查/修复登录态 - 调用 `image.z.ai` 网页接口生成图片 - 下载到当前目录下的 `captures/` ## 常用命令 ```bash npm run ready npm run check-session npm run login-if-needed npm run generate ``` ## 注意 - 依赖浏览器远程调试端口 `18800` - 需要浏览器里登录 `https://image.z.ai/` - 这是网页接口方案；如果网页字段更新，优先用 `network_monitor.js` 重新观察请求

path: scripts/network_monitor.js
content: #!/usr/bin/env node
/**
 * 网络请求抓包工具
 * 用于分析智谱AI的API请求
 */

const CDP = require('chrome-remote-interface');

async function monitorNetwork() {
    console.log('Connecting to browser via CDP...');
    
    const client = await CDP({ port: 18800 });
    const { Network, Page } = client;
    
    await Network.enable();
    await Page.enable();
    
    console.log('Monitoring network requests...');
    console.log('Press Ctrl+C to stop');
    console.log();
    
    let requestCount = 0;
    
    // 监听请求
    Network.requestWillBeSent(params => {
        const url = params.request.url;
        
        // 只显示API相关请求
        if (url.includes('api') || url.includes('z.ai') || url.includes('generation')) {
            requestCount++;
            console.log(`\n[${requestCount}] ${params.request.method} ${url}`);
            
            // 显示请求体
            if (params.request.postData) {
                try {
                    const postData = JSON.parse(params.request.postData);
                    console.log('  POST data:', JSON.stringify(postData).substring(0, 200));
                } catch (e) {
                    console.log('  POST data:', params.request.postData.substring(0, 200));
                }
            }
        }
    });
    
    // 监听响应
    Network.responseReceived(params => {
        const url = params.response.url;
        
        if (url.includes('api') || url.includes('generation')) {
            console.log(`  <- Status: ${params.response.status} ${params.response.statusText}`);
        }
    });
    
    // 保持运行
    process.on('SIGINT', async () => {
        console.log('\n\nStopping...');
        await client.close();
        process.exit(0);
    });
}

monitorNetwork().catch(err => {
    console.error('Error:', err.message);
    process.exit(1);
});

path: scripts/package-lock.json
content: {
  "name": "zhipu-image",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "zhipu-image",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "chrome-remote-interface": "^0.33.3"
      },
      "bin": {
        "zhipu-image": "zhipu_api.js"
      },
      "engines": {
        "node": ">=14.0.0"
      }
    },
    "node_modules/chrome-remote-interface": {
      "version": "0.33.3",
      "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "commander": "2.11.x",
        "ws": "^7.2.0"
      },
      "bin": {
        "chrome-remote-interface": "bin/client.js"
      }
    },
    "node_modules/commander": {
      "version": "2.11.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ws": {
      "version": "7.5.10",
      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8.3.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": "^5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: scripts/package.json
content: {
  "name": "zhipu-image",
  "version": "2.0.0",
  "description": "智谱 GLM-Image 网页登录态图片生成与下载工具",
  "main": "./zhipu_api.js",
  "bin": {
    "zhipu-image": "./zhipu_api.js"
  },
  "scripts": {
    "ready": "node zhipu_api.js ready",
    "check-session": "node zhipu_api.js check-session",
    "login": "node zhipu_api.js login",
    "login-if-needed": "node zhipu_api.js login-if-needed",
    "generate": "node zhipu_api.js generate \"a cute tiger mascot\""
  },
  "keywords": [
    "image",
    "generation",
    "ai",
    "zhipu",
    "glm",
    "openclaw",
    "skill",
    "glm-image",
    "image.z.ai"
  ],
  "author": "小虎",
  "license": "MIT",
  "dependencies": {
    "chrome-remote-interface": "^0.33.3"
  },
  "engines": {
    "node": ">=14.0.0"
  }
}

path: scripts/zhipu_api.js
content: #!/usr/bin/env node
/**
 * Zhipu GLM-Image helper
 * - check saved session
 * - auto-open browser login when needed
 * - generate images via image.z.ai
 */

let CDP;
try {
  CDP = require('chrome-remote-interface');
} catch {
  CDP = require('C:\\Users\\huang\\.openclaw\\workspace\\skills\\doubao-media\\scripts\\node_modules\\chrome-remote-interface');
}
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');
const { exec } = require('child_process');

const API_BASE = 'https://image.z.ai/api/proxy';
const LOGIN_URL = 'https://image.z.ai/';
const SESSION_FILE = path.join(process.env.USERPROFILE, '.zhipu_image_session.json');
const DEFAULT_OUTPUT = path.join(process.cwd(), 'captures');

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function parseArgs(argv) {
  const args = { cmd: argv[0], prompt: '', output: DEFAULT_OUTPUT, timeoutMs: 120000, autoOpen: true, waitLogin: true };
  const rest = [];
  for (let i = 1; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === '--output') args.output = argv[++i] || DEFAULT_OUTPUT;
    else if (arg === '--timeout-ms') args.timeoutMs = Number(argv[++i]) || 120000;
    else if (arg === '--no-open-browser') args.autoOpen = false;
    else if (arg === '--no-wait-login') args.waitLogin = false;
    else rest.push(arg);
  }
  args.prompt = rest.join(' ').trim();
  return args;
}

function ensureDir(dir) {
  fs.mkdirSync(dir, { recursive: true });
}

function loadSession() {
  try {
    if (fs.existsSync(SESSION_FILE)) return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
  } catch {}
  return null;
}

function saveSession(session) {
  fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
}

function buildCookieString(cookies) {
  return (cookies || []).map(c => `${c.name}=${c.value}`).join('; ');
}

function buildHeaders(cookies) {
  return {
    'Content-Type': 'application/json',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'application/json',
    'Cookie': buildCookieString(cookies),
    'Origin': 'https://image.z.ai',
    'Referer': 'https://image.z.ai/',
  };
}

function request(method, url, cookies, body = null) {
  return new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const client = parsed.protocol === 'https:' ? https : http;
    const options = {
      hostname: parsed.hostname,
      port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
      path: parsed.pathname + parsed.search,
      method,
      headers: buildHeaders(cookies),
    };
    const req = client.request(options, res => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve({ status: res.statusCode, data, headers: res.headers }));
    });
    req.on('error', reject);
    req.setTimeout(30000, () => req.destroy(new Error('Request timeout')));
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

async function captureCookies() {
  const client = await CDP({ port: 18800 });
  const { Network } = client;
  await Network.enable();
  const cookies = await Network.getCookies();
  await client.close();

  const picked = cookies.cookies.filter(c =>
    c.domain.includes('z.ai') || c.domain.includes('bigmodel.cn') || c.domain.includes('chatglm')
  );
  if (!picked.length) throw new Error('No Zhipu cookies found. Please log in at image.z.ai first.');

  const session = { cookies: picked.map(c => ({ name: c.name, value: c.value })), savedAt: new Date().toISOString() };
  saveSession(session);
  return session;
}

function openLoginPage() {
  return new Promise((resolve, reject) => {
    exec(`start "" "${LOGIN_URL}"`, { shell: 'cmd.exe' }, error => {
      if (error) return reject(error);
      resolve({ ok: true, url: LOGIN_URL });
    });
  });
}

async function checkSession() {
  const session = loadSession();
  if (!session?.cookies?.length) return { ok: false, reason: 'missing-session-file', sessionFile: SESSION_FILE };
  try {
    const resp = await request('POST', `${API_BASE}/images/generate`, session.cookies, {
      prompt: 'test', ratio: '1:1', resolution: '512x512', rm_label_watermark: true,
    });
    if (resp.status === 200) {
      return { ok: true, reason: 'session-valid', sessionFile: SESSION_FILE, cookieCount: session.cookies.length, savedAt: session.savedAt || null };
    }
    return { ok: false, reason: 'probe-failed', sessionFile: SESSION_FILE, status: resp.status, bodyPreview: String(resp.data).slice(0, 300) };
  } catch (error) {
    return { ok: false, reason: 'check-error', sessionFile: SESSION_FILE, error: error.message };
  }
}

async function waitForLogin(timeoutMs = 120000) {
  const started = Date.now();
  let lastError = null;
  while (Date.now() - started < timeoutMs) {
    try {
      const session = await captureCookies();
      return { ok: true, waitedMs: Date.now() - started, cookieCount: session.cookies.length };
    } catch (error) {
      lastError = error;
      await sleep(5000);
    }
  }
  return { ok: false, reason: 'login-timeout', waitedMs: Date.now() - started, error: lastError?.message || 'Timed out waiting for login' };
}

async function ensureSession(opts = {}) {
  const checked = await checkSession();
  if (checked.ok) return { ...checked, refreshed: false, openedBrowser: false };
  try {
    const session = await captureCookies();
    return { ok: true, reason: 'session-refreshed', refreshed: true, openedBrowser: false, cookieCount: session.cookies.length, sessionFile: SESSION_FILE };
  } catch (error) {
    if (opts.autoOpen === false) throw error;
    await openLoginPage();
    if (opts.waitLogin === false) {
      return { ok: false, reason: 'login-required-browser-opened', openedBrowser: true, loginUrl: LOGIN_URL, sessionFile: SESSION_FILE };
    }
    const waited = await waitForLogin(opts.timeoutMs || 120000);
    if (!waited.ok) return { ok: false, ...waited, openedBrowser: true, loginUrl: LOGIN_URL, sessionFile: SESSION_FILE };
    const rechecked = await checkSession();
    return { ...rechecked, refreshed: true, openedBrowser: true, loginUrl: LOGIN_URL, waitedMs: waited.waitedMs };
  }
}

async function downloadImage(url, outputDir, nameBase) {
  ensureDir(outputDir);
  const filePath = path.join(outputDir, `${nameBase}.png`);
  return new Promise((resolve, reject) => {
    const client = url.startsWith('https:') ? https : http;
    client.get(url, res => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        res.resume();
        return resolve(downloadImage(res.headers.location, outputDir, nameBase));
      }
      if (res.statusCode !== 200) {
        res.resume();
        return reject(new Error(`HTTP ${res.statusCode}`));
      }
      const file = fs.createWriteStream(filePath);
      res.pipe(file);
      file.on('finish', () => file.close(() => resolve(filePath)));
      file.on('error', reject);
    }).on('error', reject);
  });
}

async function generate(prompt, outputDir) {
  const ensured = await ensureSession({ autoOpen: true, waitLogin: true, timeoutMs: 120000 });
  if (!ensured.ok) throw new Error(`Session unavailable: ${ensured.reason}`);
  const session = loadSession();
  const resp = await request('POST', `${API_BASE}/images/generate`, session.cookies, {
    prompt,
    ratio: '1:1',
    resolution: '1K',
    rm_label_watermark: true,
  });
  const result = JSON.parse(resp.data || '{}');
  const imageUrl = result?.data?.image?.image_url || result?.data?.[0]?.url || null;
  if (!imageUrl) throw new Error(`Generate failed: ${String(resp.data).slice(0, 500)}`);
  const saved = await downloadImage(imageUrl, outputDir, `zhipu-${Date.now()}`);
  return { ok: true, prompt, imageUrl, saved };
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const cmd = args.cmd;

  if (cmd === 'check-session' || cmd === 'check') {
    const result = await checkSession();
    console.log(JSON.stringify(result, null, 2));
    process.exit(result.ok ? 0 : 1);
  }
  if (cmd === 'login') {
    const result = await captureCookies();
    console.log(JSON.stringify({ ok: true, cookieCount: result.cookies.length, sessionFile: SESSION_FILE }, null, 2));
    return;
  }
  if (cmd === 'login-if-needed') {
    const result = await ensureSession(args);
    console.log(JSON.stringify(result, null, 2));
    process.exit(result.ok ? 0 : 1);
  }
  if (cmd === 'ready') {
    const result = await ensureSession(args);
    console.log(JSON.stringify({
      ok: !!result.ok,
      status: result.ok ? 'available' : 'unavailable',
      sessionFile: SESSION_FILE,
      refreshed: !!result.refreshed,
      openedBrowser: !!result.openedBrowser,
      cookieCount: result.cookieCount || 0,
      reason: result.reason || null,
      loginUrl: result.loginUrl || null,
      waitedMs: result.waitedMs || 0,
    }, null, 2));
    process.exit(result.ok ? 0 : 1);
  }

  const prompt = cmd === 'generate' ? (args.prompt || 'a cute tiger mascot') : ([cmd, args.prompt].filter(Boolean).join(' ') || 'a cute tiger mascot');
  const result = await generate(prompt, args.output);
  console.log(JSON.stringify(result, null, 2));
}

main().catch(error => {
  console.error(error.message);
  process.exit(1);
});


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)