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

190 lines
8.0 KiB
JavaScript

// 只读采集:数据总览 近1天/近7天/近30天 + 捕获 mtop 接口响应
// 仅切换时间范围单选按钮(看板筛选),不点击任何操作/下载/导出按钮。
import fs from 'node:fs';
import path from 'node:path';
const OUT = path.resolve(process.argv[2] || '.cdp/out');
fs.mkdirSync(path.join(OUT, 'ranges'), { recursive: true });
fs.mkdirSync(path.join(OUT, 'api'), { 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 wj = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
const wt = (p, s) => fs.writeFileSync(p, String(s ?? ''), 'utf8');
class Cdp {
constructor(ws) { this.ws = ws; this.seq = 0; this.pending = new Map(); }
static async attach(url) {
const ws = new WebSocket(url);
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', () => { 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);
m.error ? p.reject(new Error(m.error.message)) : p.resolve(m.result);
} else if (m.method) {
c.emit(m.method, m.params, m.sessionId);
}
});
return c;
}
emit(method, params, sessionId) { if (this.onEvent) this.onEvent(method, params, sessionId); }
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 ev(expression, sessionId) {
const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, sessionId);
if (r.exceptionDetails) return { __error: r.exceptionDetails.text + ' :: ' + (r.exceptionDetails.exception?.description || '') };
return r.result?.value;
}
close() { try { this.ws.close(); } catch {} }
}
// ---------- endpoint ----------
const lines = fs.readFileSync(path.join(USER_DATA_DIR, 'DevToolsActivePort'), 'utf8')
.split('\n').map(s => s.trim()).filter(Boolean);
const wsUrl = `ws://127.0.0.1:${lines[0]}${lines[1]}`;
const cdp = await Cdp.attach(wsUrl);
const { targetInfos } = await cdp.send('Target.getTargets');
const target = targetInfos.find(t => t.type === 'page' && /seller\.goofish\.com/.test(t.url));
if (!target) { say('no goofish tab'); process.exit(2); }
const { sessionId: S } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
say('attached: ' + target.title);
// ---------- network capture ----------
const reqs = new Map();
const apiHits = [];
let lastEvent = Date.now();
cdp.onEvent = (method, p, sid) => {
if (sid !== S) return;
lastEvent = Date.now();
if (method === 'Network.requestWillBeSent') {
reqs.set(p.requestId, { url: p.request.url, method: p.request.method, postData: p.request.postData || null });
} else if (method === 'Network.responseReceived') {
const r = reqs.get(p.requestId);
if (r) { r.status = p.response.status; r.mime = p.response.mimeType; }
if (/mtop|api|\.json/i.test(p.response.url)) apiHits.push({ requestId: p.requestId, url: p.response.url, ts: Date.now() });
}
};
await cdp.send('Network.enable', {}, S);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function waitQuiet(quietMs = 2500, maxMs = 25000) {
const t0 = Date.now();
while (Date.now() - t0 < maxMs) {
await sleep(250);
if (Date.now() - lastEvent > quietMs) return true;
}
return false;
}
const SNAP = String.raw`(() => {
const q = (s) => Array.from(document.querySelectorAll(s));
const main = document.querySelector('.ant-layout-content') || document.body;
const groups = q('.ant-radio-group').map(g => {
const opts = Array.from(g.querySelectorAll('.ant-radio-wrapper, .ant-radio-button-wrapper'));
const checked = opts.find(o => /checked/.test(o.className) || o.querySelector('.ant-radio-checked'));
return { opts: opts.map(o => (o.innerText || '').trim()), checked: checked ? (checked.innerText || '').trim() : null };
});
return {
href: location.href,
mainText: (main.innerText || '').slice(0, 30000),
groups,
tables: q('.ant-table').length,
tableHeaders: q('.ant-table-thead th').map(th => th.innerText.replace(/\s+/g, ' ').trim()),
renderedRows: q('.ant-table-tbody tr').length,
navLinks: q('a[href]').map(a => ({ t: (a.innerText || '').trim(), h: a.getAttribute('href') })).filter(x => x.t).slice(0, 60),
menuText: q('[class*="menu"] li, [class*="Menu"] li').map(li => (li.innerText || '').replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, 60),
};
})()`;
async function snapshot(tag) {
const s = await cdp.ev(SNAP, S);
if (!s || s.__error) { say('snapshot error ' + tag + ': ' + JSON.stringify(s)); return null; }
wt(path.join(OUT, 'ranges', tag + '-maintext.txt'), s.mainText);
wj(path.join(OUT, 'ranges', tag + '-state.json'), { href: s.href, groups: s.groups, tables: s.tables, tableHeaders: s.tableHeaders, renderedRows: s.renderedRows, navLinks: s.navLinks, menuText: s.menuText });
say('[' + tag + '] groups=' + JSON.stringify(s.groups.map(g => g.checked)) +
' tables=' + s.tables + ' rows=' + s.renderedRows +
' headers=' + JSON.stringify(s.tableHeaders));
return s;
}
async function clickRange(label) {
const expr = `(() => {
const label = ${JSON.stringify(label)};
const out = { clicked: 0, skipped: 0 };
for (const g of document.querySelectorAll('.ant-radio-group')) {
const opts = Array.from(g.querySelectorAll('.ant-radio-wrapper, .ant-radio-button-wrapper'));
const t = opts.find(o => (o.innerText || '').trim() === label);
if (!t) continue;
if (/checked/.test(t.className) || t.querySelector('.ant-radio-checked')) { out.skipped++; continue; }
t.click(); out.clicked++;
}
return out;
})()`;
return await cdp.ev(expr, S);
}
async function dumpApi(tag) {
const seen = new Set();
let n = 0;
for (const hit of apiHits) {
if (seen.has(hit.requestId)) continue;
seen.add(hit.requestId);
try {
const b = await cdp.send('Network.getResponseBody', { requestId: hit.requestId }, S, 20000);
let body = b.base64Encoded ? Buffer.from(b.body, 'base64').toString('utf8') : b.body;
if (body.length > 400000) body = body.slice(0, 400000) + '\n...[truncated]';
const name = `${tag}-${String(n).padStart(2, '0')}.txt`;
wt(path.join(OUT, 'api', name), 'URL: ' + hit.url + '\n\n' + body);
n++;
} catch (e) { /* body unavailable */ }
}
say('[' + tag + '] api bodies saved = ' + n + ' / hits=' + apiHits.length);
return n;
}
// ---------- run ----------
say('--- initial state ---');
const s0 = await snapshot('1d-before');
await dumpApi('1d-before');
say('--- switch to 近7天 ---');
say('click result: ' + JSON.stringify(await clickRange('近7天')));
await waitQuiet();
await snapshot('7d');
await dumpApi('7d');
say('--- switch to 近30天 ---');
say('click result: ' + JSON.stringify(await clickRange('近30天')));
await waitQuiet();
await snapshot('30d');
await dumpApi('30d');
say('--- restore 近1天 ---');
say('click result: ' + JSON.stringify(await clickRange('近1天')));
await waitQuiet();
await snapshot('1d-after');
// 请求清单
wj(path.join(OUT, 'requests.json'), Array.from(reqs.entries()).map(([id, r]) => ({ id, ...r }))
.filter(r => /mtop|api|summary|item/i.test(r.url)));
say('done.');
cdp.close();
process.exit(0);