// 在「商品数据」页(隐藏标签页,按需创建)上下文执行 JS // 用法: node cdp-commodity.mjs <表达式文件> <输出文件> [--close] import fs from 'node:fs'; import path from 'node:path'; const exprFile = process.argv[2]; const outFile = path.resolve(process.argv[3] || '.cdp/out/commodity.json'); const doClose = process.argv.includes('--close'); const expr = fs.readFileSync(exprFile, 'utf8'); const URL_ = 'https://seller.goofish.com/?site=COMMONPRO#/seller-data/commodity'; const UDD = 'C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\User Data'; const lines = fs.readFileSync(path.join(UDD, 'DevToolsActivePort'), 'utf8') .split('\n').map(s => s.trim()).filter(Boolean); 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 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); } }); return c; } send(method, params = {}, sessionId, t = 300000) { const id = ++this.seq; return new Promise((res, rej) => { const timer = setTimeout(() => { this.pending.delete(id); rej(new Error('timeout ' + method)); }, t); this.pending.set(id, { resolve: res, reject: rej, timer }); const msg = { id, method, params }; if (sessionId) msg.sessionId = sessionId; this.ws.send(JSON.stringify(msg)); }); } close() { try { this.ws.close(); } catch {} } } const cdp = await Cdp.attach(`ws://127.0.0.1:${lines[0]}${lines[1]}`); let { targetInfos } = await cdp.send('Target.getTargets'); let page = targetInfos.find(t => t.type === 'page' && /seller\.goofish\.com/.test(t.url) && /seller-data\/commodity/.test(t.url)); let created = false; if (!page) { const r = await cdp.send('Target.createTarget', { url: URL_, background: true }); created = true; await new Promise(r2 => setTimeout(r2, 5000)); ({ targetInfos } = await cdp.send('Target.getTargets')); page = targetInfos.find(t => t.targetId === r.targetId); if (!page) { console.log('failed to create tab'); process.exit(2); } } console.log('tab: ' + page.url + (created ? ' (created)' : ' (existing)')); const { sessionId: S } = await cdp.send('Target.attachToTarget', { targetId: page.targetId, flatten: true }); const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true, userGesture: false }, S, 300000); if (r.exceptionDetails) { const d = { __error: r.exceptionDetails.text, desc: r.exceptionDetails.exception?.description }; fs.writeFileSync(outFile, JSON.stringify(d, null, 2), 'utf8'); console.log('EXCEPTION: ' + JSON.stringify(d).slice(0, 800)); cdp.close(); process.exit(1); } const v = r.result?.value; fs.mkdirSync(path.dirname(outFile), { recursive: true }); fs.writeFileSync(outFile, JSON.stringify(v, null, 2), 'utf8'); console.log('written ' + outFile); console.log(JSON.stringify(v).slice(0, 2500)); if (doClose) { await cdp.send('Target.closeTarget', { targetId: page.targetId }); console.log('tab closed'); } cdp.close(); process.exit(0);