#!/usr/bin/env node /** * cdp_dump_scripts.js —— 把某个 target 里**所有已解析脚本**逐个导出并标注特征。 * * 为什么需要(2026-10-12 踩坑): * cdp_script_source.js 取的是 `hits[hits.length-1]`(最后一个匹配), * 在验证码 iframe(同 target 里有 dy-jy.js / dy-ele.js / tdc.js 多个脚本)里 * **可能取错**。有一次取到的 205KB 文件既没有 __TENCENT_CHAOS_VM 也没有 delta, * 根本不是 tdc.js。 * * 本工具: 一律全量导出 + 对每个脚本标注是否含 VM 锚点 / delta / 长度,绝不猜。 * * 用法: * node tools/cdp_dump_scripts.js --match drag_ele --outdir /tmp/1ms_bridge/scripts */ const fs = require('fs'); const path = require('path'); 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) {} } } (async () => { const port = parseInt(arg('port', '9222'), 10); const match = arg('match', null); const outdir = arg('outdir', '/tmp/cdp_scripts'); const wait = parseInt(arg('wait', '2000'), 10); const targets = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) .filter(t => t.type === 'page' || t.type === 'iframe'); const target = match ? targets.find(t => (t.url || '').includes(match)) : targets[0]; if (!target) throw new Error('没有匹配的 target: ' + match); console.error(`[target] ${target.type} ${target.url.slice(0, 90)}`); const cdp = await Cdp.connect(target.webSocketDebuggerUrl); const scripts = []; cdp.on(m => { if (m.method === 'Debugger.scriptParsed') scripts.push(m.params); }); await cdp.send('Debugger.enable'); await sleep(wait); await cdp.send('Runtime.enable'); await cdp.send('Runtime.evaluate', { expression: '1' }); await sleep(500); fs.mkdirSync(outdir, { recursive: true }); const index = []; const seen = new Set(); for (const s of scripts) { if (seen.has(s.scriptId)) continue; seen.add(s.scriptId); let src = ''; try { const r = await cdp.send('Debugger.getScriptSource', { scriptId: s.scriptId }); src = r.scriptSource || ''; } catch (e) { continue; } if (!src) continue; const hasVM = src.includes('__TENCENT_CHAOS_VM'); const hasDelta = src.includes('2654435769'); const hasTDCNAME = src.includes('TDC_NAME'); const base = (s.url || 'inline').split('?')[0].split('/').pop() || 'inline'; const fn = `${String(s.scriptId).replace(/[^\w.-]/g, '_')}_${base}`.slice(0, 80); fs.writeFileSync(path.join(outdir, fn), src, 'utf8'); index.push({ scriptId: s.scriptId, url: String(s.url || '(inline)').slice(0, 130), bytes: src.length, hasVM, hasDelta, hasTDCNAME, file: fn, }); } fs.writeFileSync(path.join(outdir, 'index.json'), JSON.stringify(index, null, 1), 'utf8'); for (const r of index) { console.log(`${r.hasVM ? 'VM ' : ' '}${r.hasDelta ? 'DELTA' : ' '}` + ` ${String(r.bytes).padStart(7)}B ${r.url}`); } console.error(`\n-> ${outdir} (共 ${index.length} 个脚本, 含 VM 的 ${index.filter(x => x.hasVM).length} 个)`); cdp.close(); })().catch(e => { console.error('ERR', e.message); process.exit(1); });