#!/usr/bin/env node /** * cdp_shot.js —— 用 CDP 对**已经在跑的** Chrome 某个标签页截图存成 PNG。 * 零依赖 (Node >= 22 内置 WebSocket/fetch)。 * * 用法: * node tools/cdp_shot.js --match drag_ele --out /tmp/a.png # 整页 * node tools/cdp_shot.js --match drag_ele --out /tmp/a.png --clip 10,68,340,243 * node tools/cdp_shot.js --list * * 选项: * --port N 调试端口 (默认 9222) * --match SUBSTR 选 url 含该子串的 page/iframe 标签页 (默认第一个) * --out PATH 输出 PNG 路径 (默认 /tmp/cdp_shot.png) * --clip x,y,w,h 只截该区域 (CSS px, 相对该标签页视口) * --scale N 设备像素比 (默认 1) */ const fs = require('fs'); function arg(name, def) { const i = process.argv.indexOf('--' + name); if (i < 0) return def; const v = process.argv[i + 1]; return (v === undefined || v.startsWith('--')) ? true : v; } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new Cdp(ws); ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && c.waits.has(m.id)) { const { res, rej } = c.waits.get(m.id); c.waits.delete(m.id); m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); } }; return c; } send(method, params = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((res, rej) => this.waits.set(id, { res, rej })); } close() { try { this.ws.close(); } catch (e) {} } } (async () => { const port = parseInt(arg('port', '9222'), 10); if (arg('list', false)) { const t = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); for (const x of t) console.log(`${x.type}\t${(x.url || '').slice(0, 120)}\t${x.id}`); return; } const match = arg('match', null); const targets = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) .filter(t => t.type === 'page' || t.type === 'iframe'); let target = match ? targets.find(t => (t.url || '').includes(match)) : null; if (!target) target = targets[0]; if (!target) throw new Error('没有可用标签页'); const cdp = await Cdp.connect(target.webSocketDebuggerUrl); const params = { format: 'png', captureBeyondViewport: !!arg('clip', false) }; const clipRaw = arg('clip', null); if (clipRaw) { const [x, y, w, h] = String(clipRaw).split(',').map(Number); params.clip = { x, y, width: w, height: h, scale: parseFloat(arg('scale', '1')) }; } const r = await cdp.send('Page.captureScreenshot', params); const out = arg('out', '/tmp/cdp_shot.png'); fs.writeFileSync(out, Buffer.from(r.data, 'base64')); console.log(JSON.stringify({ ok: true, out, target: { url: target.url, id: target.id } })); cdp.close(); })().catch(e => { console.error(String(e)); process.exit(1); });