#!/usr/bin/env node /** * cdp_collect.js —— 在**正在运行**的真 Chrome 里,对验证码 iframe 求 TDC.getData(true), * 并把结果**直接落盘**(不经 stdout),避免终端输出的脱敏 / 截断。 * * 为什么要落盘: cdp_eval.js 把结果打到 stdout,链路上有脱敏风险; * 而「同会话 build + 同会话 collect」必须逐字节保真才可用于对齐,故这里由 Node 直接写文件。 * * 用法: * node tools/cdp_collect.js --match drag_ele --out /tmp/1ms_bridge/session_collect.json * node tools/cdp_collect.js --match drag_ele --expr /tmp/my_expr.js --out /tmp/x.json */ const fs = require('fs'); const crypto = require('crypto'); function arg(name, def) { const i = process.argv.indexOf('--' + name); if (i < 0) return def; const v = process.argv[i + 1]; return (v === undefined || v.startsWith('--')) ? true : v; } const sleep = ms => new Promise(r => setTimeout(r, ms)); class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); this.handlers = []; } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new Cdp(ws); ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && c.waits.has(m.id)) { const { res, rej } = c.waits.get(m.id); c.waits.delete(m.id); m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); } else if (m.method) { for (const h of c.handlers) h(m); } }; return c; } send(method, params = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((res, rej) => this.waits.set(id, { res, rej })); } on(fn) { this.handlers.push(fn); } close() { try { this.ws.close(); } catch (e) {} } } const DEFAULT_EXPR = `(async () => { const o = { href: location.href, tdcType: typeof window.TDC }; try { o.tdcKeys = Object.keys(window.TDC || {}); } catch (e) { o.tdcKeysErr = String(e); } try { o.collect = window.TDC.getData(true); } catch (e) { o.collectErr = String(e); } try { o.info = window.TDC.getInfo(); } catch (e) { o.infoErr = String(e); } return o; })()`; (async () => { const port = parseInt(arg('port', '9222'), 10); const match = arg('match', 'drag_ele'); const wait = parseInt(arg('wait', '800'), 10); const exprFile = arg('expr', null); const out = arg('out', '/tmp/1ms_bridge/session_collect.json'); const expr = (exprFile && exprFile !== true) ? fs.readFileSync(exprFile, 'utf8') : DEFAULT_EXPR; const targets = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) .filter(t => t.type === 'page' || t.type === 'iframe'); const target = targets.find(t => (t.url || '').includes(match)); if (!target) throw new Error('没有匹配的 target: ' + match); console.error(`[target] ${target.type} ${target.url.slice(0, 100)}`); const cdp = await Cdp.connect(target.webSocketDebuggerUrl); await cdp.send('Runtime.enable'); await sleep(wait); const res = await cdp.send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true, timeout: 30000, }); const val = res.result ? res.result.value : null; const exc = res.exceptionDetails ? ((res.exceptionDetails.exception || {}).description || JSON.stringify(res.exceptionDetails)) : null; const payload = { fetchedAt: new Date().toISOString(), target: { type: target.type, url: target.url, title: target.title }, ok: !res.exceptionDetails, exception: exc, data: val, }; fs.mkdirSync(require('path').dirname(out), { recursive: true }); fs.writeFileSync(out, JSON.stringify(payload, null, 1), 'utf8'); const d = val || {}; const col = typeof d.collect === 'string' ? d.collect : null; console.error(`[result] ok=${payload.ok} tdcType=${d.tdcType} tdcKeys=${JSON.stringify(d.tdcKeys || null)}`); if (col) { const sha = crypto.createHash('sha256').update(col).digest('hex').slice(0, 16); console.error(`[collect] ${col.length} 字符 sha256=${sha} 头=${col.slice(0, 24)}... 尾=${col.slice(-24)}`); } else { console.error(`[collect] 无 (collectErr=${d.collectErr || '-'})`); } console.error(`-> ${out}`); cdp.close(); })().catch(e => { console.error('ERR', e.message); process.exit(1); });