Files
cz-web/cdp.js
2026-10-01 14:02:50 +08:00

157 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-nocheck
/**
* 无依赖的 headless Chrome 截图 + DOM 测量工具(CDP over WebSocket)
*
* 用法:
* node cdp.js <url> [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 */ }
}