feat 登录页面

This commit is contained in:
Ironsp
2026-10-01 14:02:50 +08:00
parent eda0bc2200
commit b99b2a0a88
13 changed files with 481 additions and 289 deletions

156
cdp.js Normal file
View 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 */ }
}