/** * 通过 CDP 截图,用于无人值守环境下的界面功能验证。 * * 用法: * node scripts/shot.mjs */ import { writeFile } from 'node:fs/promises' const port = Number(process.argv[2] ?? 9335) const outFile = process.argv[3] ?? 'shot.png' async function main() { const listRes = await fetch(`http://127.0.0.1:${port}/json/list`) const targets = await listRes.json() const page = targets.find((t) => t.type === 'page') if (!page) { console.error('未找到 page 目标。当前目标:', targets.map((t) => `${t.type}:${t.url}`).join(', ')) process.exit(1) } console.log('目标页面:', page.url) const ws = new WebSocket(page.webSocketDebuggerUrl) let id = 0 const pending = new Map() const send = (method, params = {}) => new Promise((resolve, reject) => { const msgId = ++id const timer = setTimeout(() => { pending.delete(msgId) reject(new Error(`CDP 调用超时: ${method}`)) }, 8000) pending.set(msgId, (value) => { clearTimeout(timer) resolve(value) }) ws.send(JSON.stringify({ id: msgId, method, params })) }) await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('WebSocket 连接超时(8s)')), 8000) ws.addEventListener('open', () => { clearTimeout(timer) resolve() }) ws.addEventListener('error', (e) => { clearTimeout(timer) reject(new Error(`WebSocket 错误: ${e?.message ?? 'unknown'}`)) }) }) console.log('CDP 已连接') ws.addEventListener('message', (ev) => { const msg = JSON.parse(ev.data) if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg.result) pending.delete(msg.id) } }) await send('Page.enable') // 等首屏与字体稳定 await new Promise((r) => setTimeout(r, 2500)) const metrics = await send('Runtime.evaluate', { expression: `(() => { const c = document.createElement('canvas') const gl = c.getContext('webgl2') || c.getContext('webgl') const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info') return JSON.stringify({ title: document.title, theme: document.documentElement.dataset.theme, hasApi: typeof window.ycode === 'object', gpuRenderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : (gl ? 'webgl(no-debug-ext)' : 'NO WEBGL'), bodyText: (document.body.innerText || '').slice(0, 400), shell: !!document.querySelector('.app-shell'), columns: document.querySelector('.app-shell') ? getComputedStyle(document.querySelector('.app-shell')).gridTemplateColumns : null, composer: !!document.querySelector('.composer__input'), sessionItems: document.querySelectorAll('.sidebar__item-title').length, panels: document.querySelectorAll('.panel').length }) })()`, returnByValue: true }) console.log('DOM 状态:', metrics?.result?.value) const shot = await send('Page.captureScreenshot', { format: 'png' }) if (!shot?.data) { console.error('截图失败') process.exit(1) } await writeFile(outFile, Buffer.from(shot.data, 'base64')) console.log('已保存:', outFile) ws.close() } main().catch((err) => { console.error('异常:', err.message) process.exit(1) })