// 调试:验证 free_show_local_agent v2 的 CDP + 手写 WebSocket 管道 // 仅用 about:blank 等无害 URL,不访问大麦(保护服务器 IP) // 用法:node dev_test_scripts/debug/debug_agent_cdp.js 'use strict' const http = require('http') const net = require('net') const fs = require('fs') const path = require('path') const crypto = require('crypto') const { spawn } = require('child_process') const CDP_PORT = 9322 // 避开代理默认 9222 const PROFILE = path.join(__dirname, '.edge_debug_profile') const sleep = ms => new Promise(r => setTimeout(r, ms)) function findBrowser() { const candidates = [ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' ] for (const p of candidates) if (fs.existsSync(p)) return p return null } function cdpHttp(method, p) { return new Promise((resolve, reject) => { const req = http.request({ host: '127.0.0.1', port: CDP_PORT, path: p, method, timeout: 5000 }, res => { let body = '' res.on('data', c => { body += c }) res.on('end', () => { try { resolve(JSON.parse(body)) } catch (e) { reject(new Error('parse fail')) } }) }) req.on('timeout', () => req.destroy(new Error('timeout'))) req.on('error', reject) req.end() }) } // ---- 与代理同款最小 WS 客户端(复制自 free_show_local_agent.js) ---- function parseWsFrame(buf) { if (buf.length < 2) return null const fin = (buf[0] & 0x80) !== 0 const opcode = buf[0] & 0x0f const masked = (buf[1] & 0x80) !== 0 let len = buf[1] & 0x7f let off = 2 if (len === 126) { if (buf.length < 4) return null; len = buf.readUInt16BE(2); off = 4 } else if (len === 127) { if (buf.length < 10) return null; len = Number(buf.readBigUInt64BE(2)); off = 10 } let mask = null if (masked) { if (buf.length < off + 4) return null; mask = buf.slice(off, off + 4); off += 4 } if (buf.length < off + len) return null let payload = buf.slice(off, off + len) if (mask) { payload = Buffer.from(payload); for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4] } return { fin, opcode, payload, total: off + len } } function encodeWsFrame(text) { const payload = Buffer.from(text, 'utf8') const len = payload.length let header if (len < 126) header = Buffer.from([0x81, 0x80 | len]) else if (len < 65536) { header = Buffer.from([0x81, 0x80 | 126, 0, 0]); header.writeUInt16BE(len, 2) } else { header = Buffer.from([0x81, 0x80 | 127, 0, 0, 0, 0, 0, 0, 0, 0]); header.writeBigUInt64BE(BigInt(len), 2) } const mask = crypto.randomBytes(4) const masked = Buffer.from(payload) for (let i = 0; i < masked.length; i++) masked[i] ^= mask[i % 4] return Buffer.concat([header, mask, masked]) } function wsConnect(wsUrl) { return new Promise((resolve, reject) => { const u = new URL(wsUrl) const sock = net.connect(Number(u.port) || 80, u.hostname || '127.0.0.1') const key = crypto.randomBytes(16).toString('base64') const handshake = ['GET ' + (u.pathname || '/') + (u.search || '') + ' HTTP/1.1', 'Host: ' + u.host, 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Key: ' + key, 'Sec-WebSocket-Version: 13', '', ''].join('\r\n') let upgraded = false let buf = Buffer.alloc(0) let fragBuf = null const handlers = { message: null, close: null } const client = { send(t) { try { sock.write(encodeWsFrame(t)) } catch (e) {} }, close() { try { sock.end() } catch (e) {} }, onMessage(fn) { handlers.message = fn }, onClose(fn) { handlers.close = fn } } sock.on('connect', () => sock.write(handshake)) sock.on('data', chunk => { buf = Buffer.concat([buf, chunk]) if (!upgraded) { const idx = buf.indexOf('\r\n\r\n') if (idx < 0) return if (!/^HTTP\/1\.1\s+101/.test(buf.slice(0, idx).toString())) { reject(new Error('握手失败')); sock.end(); return } upgraded = true buf = buf.slice(idx + 4) resolve(client) } while (true) { const frame = parseWsFrame(buf) if (!frame) break buf = buf.slice(frame.total) if (frame.opcode === 0x8) { if (handlers.close) handlers.close(); return } if (frame.opcode === 0x9) { sock.write(Buffer.from([0x8a, 0x80, 0, 0, 0, 0])); continue } if (frame.opcode === 0xA) continue if (frame.opcode === 0x1 || frame.opcode === 0x2) { if (frame.fin) { const full = fragBuf ? Buffer.concat([fragBuf, frame.payload]) : frame.payload; fragBuf = null; if (handlers.message) handlers.message(full.toString('utf8')) } else fragBuf = frame.payload } else if (frame.opcode === 0x0) { fragBuf = fragBuf ? Buffer.concat([fragBuf, frame.payload]) : frame.payload if (frame.fin) { const full = fragBuf; fragBuf = null; if (handlers.message) handlers.message(full.toString('utf8')) } } } }) sock.on('error', e => { if (!upgraded) reject(e); else if (handlers.close) handlers.close() }) sock.on('close', () => { if (handlers.close) handlers.close() }) }) } function makeCdp(ws) { let nextId = 1 const pending = new Map() ws.onMessage(text => { let msg = null try { msg = JSON.parse(text) } catch (e) { return } if (msg && msg.id && pending.has(msg.id)) { const cb = pending.get(msg.id); pending.delete(msg.id); cb(msg) } }) return { send(method, params) { return new Promise((resolve, reject) => { const id = nextId++ const timer = setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error('CDP 超时: ' + method)) } }, 15000) pending.set(id, msg => { clearTimeout(timer); resolve(msg) }) ws.send(JSON.stringify({ id, method, params: params || {} })) }) } } } async function main() { const browser = findBrowser() if (!browser) { console.log('FAIL: 未找到浏览器'); return } console.log('浏览器:', browser) const child = spawn(browser, [ '--headless=new', '--disable-gpu', '--remote-debugging-port=' + CDP_PORT, '--user-data-dir=' + PROFILE, '--disable-blink-features=AutomationControlled', '--exclude-switches=enable-automation', '--no-first-run', '--no-default-browser-check', 'about:blank' ], { detached: true, stdio: 'ignore' }) child.unref() let ver = null for (let i = 0; i < 30; i++) { await sleep(500) ver = await cdpHttp('GET', '/json/version').catch(() => null) if (ver) break } if (!ver) { console.log('FAIL: CDP 未就绪'); try { child.kill() } catch (e) {}; return } console.log('PASS: /json/version =>', ver.Browser || ver['User-Agent'] || 'ok') let tab = null try { tab = await cdpHttp('PUT', '/json/new?' + encodeURIComponent('about:blank')) } catch (e) { tab = await cdpHttp('GET', '/json/new?' + encodeURIComponent('about:blank')).catch(() => null) } if (!tab || !tab.webSocketDebuggerUrl) { console.log('FAIL: 开标签失败'); try { child.kill() } catch (e) {}; return } console.log('PASS: /json/new =>', tab.id) const ws = await wsConnect(tab.webSocketDebuggerUrl) const cdp = makeCdp(ws) const r1 = await cdp.send('Runtime.evaluate', { expression: '1+1', returnByValue: true }) console.log('PASS: evaluate 1+1 =>', r1 && r1.result && r1.result.result ? r1.result.result.value : 'null') const r2 = await cdp.send('Runtime.evaluate', { expression: 'navigator.webdriver', returnByValue: true }) console.log('INFO: navigator.webdriver =>', r2 && r2.result && r2.result.result ? r2.result.result.value : 'null') ws.close() await cdpHttp('GET', '/json/close/' + tab.id).catch(() => null) console.log('PASS: 标签已关闭') try { child.kill() } catch (e) {} console.log('DONE') process.exit(0) } main().catch(e => { console.log('FAIL:', e.message || e); process.exit(1) })