// @ts-nocheck /** * 无依赖的 headless Chrome 截图 + DOM 测量工具(CDP over WebSocket) * * 用法: * node cdp.js [width] [height] [outPng] [evalExpr] * * 环境变量: * CDP_FULL=1 截整页(captureBeyondViewport) * CDP_WAIT=2500 导航后等待毫秒 * CDP_HASH= 路由 hash/附加查询串,直接拼在 url 后面 * * 说明:本机 agent-browser 会反复 SIGTERM,故改为此脚本;仅用 Node 内置能力。 */ import { spawn } from 'node:child_process' import { writeFileSync, mkdtempSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' const CANDIDATES = [ 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe', ] const CHROME = CANDIDATES.find((p) => existsSync(p)) if (!CHROME) { console.error('找不到 Chrome/Edge') process.exit(1) } const [, , urlArg, wArg, hArg, outArg, exprArg] = process.argv const url = (urlArg || 'http://localhost:5173/') + (process.env.CDP_HASH || '') const width = Number(wArg || 1440) const height = Number(hArg || 900) const out = outArg || 'shot.png' const expr = exprArg || '' const full = process.env.CDP_FULL === '1' const waitMs = Number(process.env.CDP_WAIT || 2800) const PORT = 9300 + Math.floor(Math.random() * 400) const userDataDir = mkdtempSync(join(tmpdir(), 'cdp-')) const child = spawn( CHROME, [ '--headless=new', `--remote-debugging-port=${PORT}`, `--user-data-dir=${userDataDir}`, `--window-size=${width},${height}`, '--no-first-run', '--no-default-browser-check', '--disable-gpu', '--force-device-scale-factor=1', 'about:blank', ], { stdio: 'ignore' }, ) async function findTarget() { for (let i = 0; i < 100; i += 1) { try { const res = await fetch(`http://127.0.0.1:${PORT}/json/list`) const list = await res.json() const page = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl) if (page) return page.webSocketDebuggerUrl } catch { /* 还没起来 */ } await new Promise((r) => setTimeout(r, 150)) } throw new Error('Chrome 调试端口未就绪') } function makeClient(wsUrl) { const ws = new WebSocket(wsUrl) let seq = 0 const pending = new Map() const events = [] const ready = new Promise((resolve, reject) => { ws.onopen = () => resolve() ws.onerror = (e) => reject(new Error(`ws error ${e.message || ''}`)) }) ws.onmessage = (ev) => { const msg = JSON.parse(ev.data) if (msg.id && pending.has(msg.id)) { const { resolve, reject } = pending.get(msg.id) pending.delete(msg.id) if (msg.error) reject(new Error(JSON.stringify(msg.error))) else resolve(msg.result) } else if (msg.method) events.push(msg.method) } const send = (method, params = {}) => new Promise((resolve, reject) => { seq += 1 pending.set(seq, { resolve, reject }) ws.send(JSON.stringify({ id: seq, method, params })) }) return { ready, send, events, close: () => ws.close() } } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) try { const wsUrl = await findTarget() const cdp = makeClient(wsUrl) await cdp.ready await cdp.send('Page.enable') await cdp.send('Runtime.enable') await cdp.send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false, }) await cdp.send('Page.navigate', { url }) await sleep(waitMs) if (expr) { const res = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true, }) if (res.exceptionDetails) { console.error('EVAL-ERROR:', JSON.stringify(res.exceptionDetails.exception?.description || res.exceptionDetails)) } else { console.log(typeof res.result.value === 'string' ? res.result.value : JSON.stringify(res.result.value)) } } let clip if (full) { const metrics = await cdp.send('Page.getLayoutMetrics') const size = metrics.cssContentSize || metrics.contentSize clip = { x: 0, y: 0, width: size.width, height: size.height, scale: 1 } } const shot = await cdp.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: full, ...(clip ? { clip } : {}), }) writeFileSync(out, Buffer.from(shot.data, 'base64')) console.error(`saved ${out} (${width}x${height}${full ? ' fullpage' : ''})`) cdp.close() } catch (err) { console.error('FAIL:', err.message) process.exitCode = 1 } finally { try { child.kill() } catch { /* ignore */ } }