157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
// @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 */ }
|
||
}
|