#!/usr/bin/env node /** * cdp_script_source.js —— 用 CDP Debugger 取某个标签页里**已解析脚本**的原文。 * * 为什么需要它(而不是再 fetch 一次): * `tdc.js?app_data=..&t=..` **每次请求返回不同构建**, 所以"再抓一次"拿不到当前会话 * 正在跑的那一份。只有 Debugger.scriptParsed + Debugger.getScriptSource 能拿到**运行时 * 真正执行的那份源码**(缓存命中也不影响)。 * * 零依赖 (Node >= 22 内置 WebSocket/fetch)。 * * 用法: * node tools/cdp_script_source.js --match drag_ele --url-substr tdc.js --out /tmp/build.js * node tools/cdp_script_source.js --match drag_ele --list */ 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; } 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 sleep = ms => new Promise(r => setTimeout(r, ms)); (async () => { const port = parseInt(arg('port', '9222'), 10); const match = arg('match', null); const substr = arg('url-substr', null); const wait = parseInt(arg('wait', '1500'), 10); const targets = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) .filter(t => t.type === 'page' || t.type === 'iframe'); let target = match ? targets.find(t => (t.url || '').includes(match)) : targets[0]; if (!target) throw new Error('没有可用标签页'); 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' }); // 触发一次上下文 if (arg('list', false)) { for (const s of scripts) console.log(`${String(s.url || '').slice(0, 120)}\t${s.length}B`); cdp.close(); return; } const hits = scripts.filter(s => substr ? String(s.url || '').includes(substr) : true); if (!hits.length) throw new Error(`没找到匹配 ${substr} 的已解析脚本 (共 ${scripts.length} 个)`); const pick = hits[hits.length - 1]; const r = await cdp.send('Debugger.getScriptSource', { scriptId: pick.scriptId }); const out = arg('out', '/tmp/cdp_script.js'); fs.writeFileSync(out, r.scriptSource, 'utf8'); console.log(JSON.stringify({ ok: true, url: pick.url, length: r.scriptSource.length, out, candidates: hits.map(h => String(h.url).slice(0, 90)) }, null, 1)); cdp.close(); })().catch(e => { console.error('CDP_ERROR: ' + e.message); process.exit(1); });