Files
ObsidianHome/小鱼/.cdp/cdp-probe.mjs
T
2026-11-05 15:16:24 +08:00

187 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 只读 CDP 采集:连接已登录 Chrome(chrome://inspect 调试模式)
// 该模式只暴露浏览器级 WebSocket,不提供 /json HTTP 接口,
// 因此按 chrome-devtools-mcp autoConnect 的做法读取 DevToolsActivePort。
// 用法: node cdp-probe.mjs <输出目录>
import fs from 'node:fs';
import path from 'node:path';
const OUT = path.resolve(process.argv[2] || '.cdp');
fs.mkdirSync(OUT, { recursive: true });
const USER_DATA_DIR =
process.env.CHROME_USER_DATA_DIR ||
'C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\User Data';
const say = (s) => console.log(String(s));
const writeJson = (n, o) =>
fs.writeFileSync(path.join(OUT, n), JSON.stringify(o, null, 2), 'utf8');
const writeText = (n, s) => fs.writeFileSync(path.join(OUT, n), String(s ?? ''), 'utf8');
class Cdp {
constructor(ws) { this.ws = ws; this.seq = 0; this.pending = new Map(); }
static async attach(wsUrl) {
const ws = new WebSocket(wsUrl);
await new Promise((res, rej) => {
const t = setTimeout(() => rej(new Error('ws open timeout')), 15000);
ws.addEventListener('open', () => { clearTimeout(t); res(); }, { once: true });
ws.addEventListener('error', (e) => { clearTimeout(t); rej(new Error('ws error')); }, { once: true });
});
const c = new Cdp(ws);
ws.addEventListener('message', (ev) => {
let m; try { m = JSON.parse(ev.data); } catch { return; }
if (m.id && c.pending.has(m.id)) {
const p = c.pending.get(m.id); c.pending.delete(m.id);
clearTimeout(p.timer);
if (m.error) p.reject(new Error(m.error.message)); else p.resolve(m.result);
}
});
return c;
}
send(method, params = {}, sessionId, timeoutMs = 60000) {
const id = ++this.seq;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id); reject(new Error('timeout: ' + method));
}, timeoutMs);
this.pending.set(id, { resolve, reject, timer });
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
this.ws.send(JSON.stringify(msg));
});
}
async evalExpr(expression, sessionId) {
const r = await this.send('Runtime.evaluate', {
expression, returnByValue: true, awaitPromise: true, userGesture: false,
}, sessionId);
if (r.exceptionDetails) {
const d = r.exceptionDetails;
return { __error: d.text + ' :: ' + (d.exception?.description || '') };
}
return r.result?.value;
}
close() { try { this.ws.close(); } catch {} }
}
// ---------------- 页面侧探针(纯只读,不点击任何按钮) ----------------
const PROBE = String.raw`(() => {
const out = {};
try {
const q = (s) => Array.from(document.querySelectorAll(s));
out.meta = { href: location.href, title: document.title, readyState: document.readyState };
out.iframeCount = q('iframe').length;
out.antTableCount = q('.ant-table').length;
out.pagination = q('.ant-pagination-item').map(e => e.innerText.trim());
out.activePage = (document.querySelector('.ant-pagination-item-active') || {}).innerText || null;
out.paginationTotal = (document.querySelector('.ant-pagination-total-text') || {}).innerText || null;
out.tabs = q('.ant-tabs-tab').map(e => e.innerText.trim());
out.radioGroups = q('.ant-radio-group').map(g => g.innerText.replace(/\s+/g, ' ').trim());
out.selects = q('.ant-select').map(s => s.innerText.replace(/\s+/g, ' ').trim());
out.dateInputs = q('.ant-picker input').map(i => i.value);
out.buttons = q('button').map(b => b.innerText.trim()).filter(Boolean).slice(0, 50);
out.headers = q('.ant-table-thead th').map(th => th.innerText.replace(/\s+/g, ' ').trim()).filter(Boolean);
const rows = q('.ant-table-tbody tr');
out.renderedRowCount = rows.length;
out.renderedRows = rows.map(tr =>
Array.from(tr.querySelectorAll('td')).map(td => (td.innerText || '').replace(/\s+/g, ' ').trim()));
// 从 React fiber 取完整 dataSource(绕开虚拟滚动只渲染部分行的问题)
let ds = null, dsPath = null;
const anchor = rows[0] || document.querySelector('.ant-table');
const fk = anchor && Object.keys(anchor).find(k =>
k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));
out.reactFiberFound = !!fk;
if (fk) {
let f = anchor[fk], d = 0;
while (f && d < 80) {
const p = f.memoizedProps;
if (p && Array.isArray(p.dataSource) && p.dataSource.length) {
ds = p.dataSource; dsPath = 'fiber depth=' + d; break;
}
f = f.return; d++;
}
}
const plain = (o, depth) => {
if (o === null || o === undefined) return null;
const t = typeof o;
if (t === 'string' || t === 'number' || t === 'boolean') return o;
if (t !== 'object') return String(o);
if (Array.isArray(o)) return depth > 5 ? '[array]' : o.slice(0, 300).map(x => plain(x, depth + 1));
if (o.$$typeof) return '[react-element]';
if (depth > 5) return '[object]';
const r = {};
for (const k of Object.keys(o)) { try { r[k] = plain(o[k], depth + 1); } catch { r[k] = '[err]'; } }
return r;
};
out.dataSource = { path: dsPath, length: ds ? ds.length : 0, keys: ds ? Object.keys(ds[0] || {}) : [] };
if (ds) out._dataSourceFull = plain(ds, 0);
const main = document.querySelector('.ant-layout-content') || document.body;
out._mainText = (main.innerText || '').slice(0, 30000);
return out;
} catch (e) { return { __error: String((e && e.stack) || e) }; }
})()`;
// ---------------- 主流程 ----------------
let port, wsPath;
try {
const raw = fs.readFileSync(path.join(USER_DATA_DIR, 'DevToolsActivePort'), 'utf8');
const lines = raw.split('\n').map(s => s.trim()).filter(Boolean);
port = parseInt(lines[0], 10);
wsPath = lines[1];
if (!port || !wsPath) throw new Error('bad DevToolsActivePort: ' + JSON.stringify(lines));
} catch (e) {
say('[cdp] FATAL cannot read DevToolsActivePort: ' + e.message);
say('[cdp] -> 请在 Chrome 打开 chrome://inspect/#remote-debugging 并启用远程调试');
process.exit(3);
}
const wsUrl = 'ws://127.0.0.1:' + port + wsPath;
say('[cdp] endpoint = ' + wsUrl);
const cdp = await Cdp.attach(wsUrl);
say('[cdp] browser ws connected');
const { targetInfos } = await cdp.send('Target.getTargets');
writeJson('targets.json', targetInfos);
say('[cdp] targets = ' + targetInfos.length);
for (const t of targetInfos) say(' - ' + t.type + ' | ' + t.title + ' | ' + t.url);
let target = targetInfos.find(t => t.type === 'page' && /seller\.goofish\.com/.test(t.url));
if (!target) target = targetInfos.find(t => t.type === 'page' && /goofish\.com/.test(t.url));
if (!target) { say('[cdp] NO goofish page target found (请确认该标签页已打开)'); cdp.close(); process.exit(2); }
say('[cdp] selected = ' + target.title + ' | ' + target.url);
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
say('[cdp] sessionId = ' + sessionId);
const res = await cdp.evalExpr(PROBE, sessionId);
if (!res || res.__error) {
say('[cdp] PROBE ERROR: ' + (res && res.__error));
writeJson('probe-error.json', res || null);
cdp.close();
process.exit(4);
}
const { _dataSourceFull, _mainText, ...rest } = res;
writeJson('probe.json', rest);
if (_dataSourceFull) writeJson('datasource.json', _dataSourceFull);
if (_mainText) writeText('maintext.txt', _mainText);
say('[cdp] ---------- probe summary ----------');
say(' href = ' + (rest.meta && rest.meta.href));
say(' tabs = ' + JSON.stringify(rest.tabs));
say(' dateInputs = ' + JSON.stringify(rest.dateInputs));
say(' radioGroups = ' + JSON.stringify(rest.radioGroups));
say(' selects = ' + JSON.stringify(rest.selects));
say(' pagination = ' + JSON.stringify(rest.pagination) + ' active=' + rest.activePage + ' total=' + rest.paginationTotal);
say(' headers = ' + JSON.stringify(rest.headers));
say(' rows = rendered ' + rest.renderedRowCount + ' / dataSource ' + JSON.stringify(rest.dataSource));
say(' buttons = ' + JSON.stringify(rest.buttons));
say(' files = probe.json' + (_dataSourceFull ? ' + datasource.json' : '') + (_mainText ? ' + maintext.txt' : ''));
const head = (rest.renderedRows || []).slice(0, 3);
say(' sampleRows = ' + JSON.stringify(head));
cdp.close();
process.exit(0);