// 常驻 CDP 桥:只与 Chrome 建立一条长连接,对外提供本地 HTTP 接口 import http from 'node:http'; import { connect, evaluate, saveJson } from './lib.mjs'; const BRIDGE_PORT = 65177; let cdp = null; let sessionId = null; let attachedTargetId = null; let reconnects = 0; async function ensureConnected() { if (cdp && cdp.ws && cdp.ws.readyState === 1) return cdp; console.log('[bridge] connecting to Chrome...'); cdp = await connect({ timeoutMs: 45000 }); sessionId = null; attachedTargetId = null; reconnects++; console.log(`[bridge] connected port=${cdp.port} reconnects=${reconnects}`); cdp.ws.addEventListener('close', () => { console.log('[bridge] ws closed'); sessionId = null; attachedTargetId = null; }); return cdp; } async function ensureAttached(urlMatch) { const c = await ensureConnected(); if (sessionId && attachedTargetId) { // 验证 session 仍有效 try { await c.send('Runtime.evaluate', { expression: '1', returnByValue: true }, sessionId); return sessionId; } catch { sessionId = null; attachedTargetId = null; } } const { targetInfos } = await c.send('Target.getTargets'); let page = targetInfos.find(t => t.type === 'page' && t.url.includes(urlMatch)); if (!page) { const created = await c.send('Target.createTarget', { url: 'https://' + urlMatch + '/', background: false }); await new Promise(r => setTimeout(r, 3000)); const t2 = await c.send('Target.getTargets'); page = t2.targetInfos.find(t => t.targetId === created.targetId); } if (!page) throw new Error('no page matching ' + urlMatch); const res = await c.send('Target.attachToTarget', { targetId: page.targetId, flatten: true }); sessionId = res.sessionId; attachedTargetId = page.targetId; await c.send('Runtime.enable', {}, sessionId).catch(() => {}); return sessionId; } const server = http.createServer(async (req, res) => { const reply = (code, obj) => { res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)); }; if (req.url === '/health') { try { const c = await ensureConnected(); return reply(200, { ok: true, port: c.port, attached: !!sessionId, reconnects }); } catch (e) { return reply(500, { ok: false, error: e.message }); } } if (req.method === 'POST' && req.url === '/eval') { let body = ''; for await (const chunk of req) body += chunk; try { const { expr, urlMatch = 'baijiahao.baidu.com', save } = JSON.parse(body); const sid = await ensureAttached(urlMatch); const value = await evaluate(cdp, sid, expr); if (save) saveJson(save, value); return reply(200, { ok: true, value }); } catch (e) { return reply(500, { ok: false, error: e.message }); } } reply(404, { ok: false, error: 'unknown endpoint' }); }); server.listen(BRIDGE_PORT, '127.0.0.1', () => { console.log(`[bridge] listening on http://127.0.0.1:${BRIDGE_PORT}`); }); // 保持连接活跃 setInterval(async () => { try { if (cdp && cdp.ws && cdp.ws.readyState === 1) { await cdp.send('Target.getTargets').catch(() => {}); } else { await ensureConnected(); } } catch (e) { console.log('[bridge] keepalive error: ' + e.message); } }, 25000);