feat 登录页面
This commit is contained in:
156
cdp.js
Normal file
156
cdp.js
Normal file
@@ -0,0 +1,156 @@
|
||||
// @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 */ }
|
||||
}
|
||||
Reference in New Issue
Block a user