// CDP 客户端:读 DevToolsActivePort,建立 WebSocket 连接,提供 evaluate 能力 import fs from 'node:fs'; import path from 'node:path'; const CHROME_USER_DATA = 'C:/Users/Administrator/AppData/Local/Google/Chrome/User Data'; export function readDevToolsPort(userDataDir = CHROME_USER_DATA) { const p = path.join(userDataDir, 'DevToolsActivePort'); const raw = fs.readFileSync(p, 'utf8').trim().split(/\r?\n/); const port = Number(raw[0]); const browserPath = raw[1]; if (!port || !browserPath) throw new Error('DevToolsActivePort invalid: ' + raw.join('|')); return { port, browserPath, wsUrl: `ws://127.0.0.1:${port}${browserPath}` }; } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); this.events = []; ws.addEventListener('message', (ev) => { const msg = JSON.parse(ev.data); if (msg.id && this.pending.has(msg.id)) { const { resolve, reject } = this.pending.get(msg.id); this.pending.delete(msg.id); msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result); } else if (msg.method) { this.events.push(msg); } }); } send(method, params = {}, sessionId) { const id = ++this.id; const payload = { id, method, params }; if (sessionId) payload.sessionId = sessionId; this.ws.send(JSON.stringify(payload)); return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`timeout: ${method}`)); } }, 90000); }); } } export async function connect({ timeoutMs = 40000 } = {}) { const { wsUrl, port } = readDevToolsPort(); const ws = new WebSocket(wsUrl); const opened = new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error('ws open timeout')), timeoutMs); ws.addEventListener('open', () => { clearTimeout(t); resolve(); }); ws.addEventListener('error', (e) => { clearTimeout(t); reject(new Error('ws error')); }); }); await opened; const cdp = new Cdp(ws); cdp.port = port; return cdp; } export async function listPages(cdp) { const { targetInfos } = await cdp.send('Target.getTargets'); return targetInfos.filter(t => t.type === 'page'); } export async function attachPage(cdp, urlMatch, { create = null } = {}) { let pages = await listPages(cdp); let target = pages.find(t => t.url.includes(urlMatch)); if (!target && create) { const { targetId } = await cdp.send('Target.createTarget', { url: create, background: false }); await new Promise(r => setTimeout(r, 3500)); pages = await listPages(cdp); target = pages.find(t => t.targetId === targetId) || pages.find(t => t.url.includes(urlMatch)); } if (!target) return null; const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }); return { target, sessionId }; } export async function evaluate(cdp, sessionId, expression, { awaitPromise = true } = {}) { const res = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise, userGesture: true, }, sessionId); if (res.exceptionDetails) { throw new Error('EVAL: ' + (res.exceptionDetails.exception?.description || res.exceptionDetails.text)); } return res.result?.value; } export function saveJson(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); }