#!/usr/bin/env node /** * cdp_module_probe.js —— 在真 Chrome 里**抓到每个采集器的真实取值**(不需要手工配平)。 * * 背景(2026-10-12): * 腾讯验证码 iframe (captcha.gtimg.com) 与页面 (1ms.run) **不同站**,Chrome 站点隔离下 * 它是独立 target —— 直接在页面里包 Function.prototype.apply **进不去**(实测: * 钩子装上时模块系统已经初始化完,__mods 恒为空)。 * * 解法(CDP 标准做法): * Target.setAutoAttach(autoAttach, waitForDebuggerOnStart, flatten) * -> 新的 iframe target 一创建就**暂停在任何脚本执行之前**; * -> 此时 Page.addScriptToEvaluateOnNewDocument 注册钩子,再 runIfWaitingForDebugger 放行; * -> tdc.js 执行时钩子已就位,模块工厂的 require/get 全被记账。 * * 产出: {url, mods: [[module_id, get()返回值], ...], reqs: [module_id,...]} * 配合同一会话的 tdc.js 静态分析(tdc_modules.analyze)即可得到「语义名 -> 真实取值」, * 无需任何人工对齐。 * * 用法: * node tools/cdp_module_probe.js --aid 195672344 --out /tmp/1ms_bridge/modvals.json */ const fs = require('fs'); 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 = {}, sessionId) { const id = ++this.id; const msg = { id, method, params }; if (sessionId) msg.sessionId = sessionId; this.ws.send(JSON.stringify(msg)); return new Promise((res, rej) => this.waits.set(id, { res, rej })); } on(fn) { this.handlers.push(fn); } close() { try { this.ws.close(); } catch (e) {} } } // 在任何页面脚本之前注入的钩子:记账「模块工厂 require 了谁」+「每个模块 .get() 返回什么」 const HOOK = `(() => { if (window.__mp) return; window.__mp = true; window.__mods = []; window.__reqs = []; const oa = Function.prototype.apply; Function.prototype.apply = function (t, a) { try { if (a && a.length >= 3 && typeof a[a.length - 1] === 'function') { const req = a[a.length - 1]; if (typeof req === 'function' && !req.__w) { const w = function (id) { try { window.__reqs.push(String(id)); } catch (e) {} const m = req.apply(this, arguments); try { if (m && typeof m.get === 'function' && !m.__g) { m.__g = true; const og = m.get; m.get = function () { let v; try { v = og.apply(this, arguments); } catch (e) { v = '__ERR__:' + e; } try { window.__mods.push([String(id), v]); } catch (e) {} return v; }; } } catch (e) {} return m; }; w.__w = true; const na = Array.prototype.slice.call(a); na[na.length - 1] = w; return oa.call(this, t, na); } } } catch (e) {} return oa.call(this, t, a); }; })();`; const SHOW = aid => `(async () => { const load = (src) => new Promise((res, rej) => { const s = document.createElement('script'); s.src = src; s.onload = () => res(true); s.onerror = () => rej(new Error('fail')); document.head.appendChild(s); }); if (typeof window.TencentCaptcha !== 'function') await load('https://ssl.captcha.qq.com/TCaptcha.js'); window.__capres = null; window.__cap = new window.TencentCaptcha('${aid}', function (r) { window.__capres = r; }, {}); window.__cap.show(); return {ok: true}; })()`; (async () => { const port = parseInt(arg('port', '9222'), 10); const aid = arg('aid', '195672344'); const out = arg('out', '/tmp/1ms_bridge/modvals.json'); const waitMs = parseInt(arg('wait', '9000'), 10); const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); const page = list.find(t => t.type === 'page' && !(t.url || '').startsWith('chrome')); if (!page) throw new Error('没有可用 page target'); const pageCdp = await Cdp.connect(page.webSocketDebuggerUrl); await pageCdp.send('Page.enable'); await pageCdp.send('Runtime.enable'); const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json(); const brCdp = await Cdp.connect(ver.webSocketDebuggerUrl); const sessions = []; brCdp.on(async m => { if (m.method === 'Target.attachedToTarget') { const { sessionId, targetInfo } = m.params; sessions.push({ sessionId, targetInfo }); console.error(`[attach] ${targetInfo.type} ${(targetInfo.url || '').slice(0, 80)}`); try { await brCdp.send('Page.enable', {}, sessionId); await brCdp.send('Page.addScriptToEvaluateOnNewDocument', { source: HOOK }, sessionId); console.error(' -> hook registered'); } catch (e) { console.error(' -> hook fail', e.message); } try { await brCdp.send('Runtime.runIfWaitingForDebugger', {}, sessionId); } catch (e) {} } }); await brCdp.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true, }); console.error('[1] autoAttach 已开(新 target 会先暂停,等我们注册钩子)'); console.error('[2] 导航到 1ms.run 并拉起验证码'); await pageCdp.send('Page.navigate', { url: 'https://1ms.run/' }); await sleep(4000); await pageCdp.send('Runtime.evaluate', { expression: SHOW(aid), awaitPromise: true, returnByValue: true, }); await sleep(waitMs); console.error('[3] 在各 session 里读取记账结果'); const results = []; for (const s of sessions) { try { const r = await brCdp.send('Runtime.evaluate', { expression: `(() => { try { return {u: location.href, n: (window.__mods||[]).length, mods: window.__mods||[], reqs: window.__reqs||[]}; } catch(e) { return {err:String(e)}; } })()`, returnByValue: true, }, s.sessionId); const v = r.result && r.result.value; if (v && v.mods && v.mods.length) { results.push(v); } else if (v) console.error(` ${(v.u || '?').slice(0, 60)}: mods=${v.n || 0}`); } catch (e) { console.error(' read fail', e.message); } } fs.writeFileSync(out, JSON.stringify(results, null, 1)); console.error(`[4] -> ${out} (命中 ${results.length} 个 session 有记账)`); for (const r of results) { console.error(` ${(r.u || '').slice(0, 70)} mods=${r.mods.length} reqs=${r.reqs.length}`); } brCdp.close(); pageCdp.close(); })().catch(e => { console.error('ERR', e.message); process.exit(1); });