Files
ObsidianHome/百家/.cdp/lib.mjs
T

101 lines
3.5 KiB
JavaScript

// CDP 客户端:读 DevToolsActivePort,建立 WebSocket 连接,提供 evaluate 能力
import fs from 'node:fs';
import path from 'node:path';
const CHROME_USER_DATA = 'C:/Users/Administrator/AppData/Local/Google/Chrome/User Data';
export function readDevToolsPort(userDataDir = CHROME_USER_DATA) {
const p = path.join(userDataDir, 'DevToolsActivePort');
const raw = fs.readFileSync(p, 'utf8').trim().split(/\r?\n/);
const port = Number(raw[0]);
const browserPath = raw[1];
if (!port || !browserPath) throw new Error('DevToolsActivePort invalid: ' + raw.join('|'));
return { port, browserPath, wsUrl: `ws://127.0.0.1:${port}${browserPath}` };
}
class Cdp {
constructor(ws) {
this.ws = ws;
this.id = 0;
this.pending = new Map();
this.events = [];
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data);
if (msg.id && this.pending.has(msg.id)) {
const { resolve, reject } = this.pending.get(msg.id);
this.pending.delete(msg.id);
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result);
} else if (msg.method) {
this.events.push(msg);
}
});
}
send(method, params = {}, sessionId) {
const id = ++this.id;
const payload = { id, method, params };
if (sessionId) payload.sessionId = sessionId;
this.ws.send(JSON.stringify(payload));
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id);
reject(new Error(`timeout: ${method}`));
}
}, 90000);
});
}
}
export async function connect({ timeoutMs = 40000 } = {}) {
const { wsUrl, port } = readDevToolsPort();
const ws = new WebSocket(wsUrl);
const opened = new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('ws open timeout')), timeoutMs);
ws.addEventListener('open', () => { clearTimeout(t); resolve(); });
ws.addEventListener('error', (e) => { clearTimeout(t); reject(new Error('ws error')); });
});
await opened;
const cdp = new Cdp(ws);
cdp.port = port;
return cdp;
}
export async function listPages(cdp) {
const { targetInfos } = await cdp.send('Target.getTargets');
return targetInfos.filter(t => t.type === 'page');
}
export async function attachPage(cdp, urlMatch, { create = null } = {}) {
let pages = await listPages(cdp);
let target = pages.find(t => t.url.includes(urlMatch));
if (!target && create) {
const { targetId } = await cdp.send('Target.createTarget', { url: create, background: false });
await new Promise(r => setTimeout(r, 3500));
pages = await listPages(cdp);
target = pages.find(t => t.targetId === targetId) || pages.find(t => t.url.includes(urlMatch));
}
if (!target) return null;
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true });
return { target, sessionId };
}
export async function evaluate(cdp, sessionId, expression, { awaitPromise = true } = {}) {
const res = await cdp.send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise,
userGesture: true,
}, sessionId);
if (res.exceptionDetails) {
throw new Error('EVAL: ' + (res.exceptionDetails.exception?.description || res.exceptionDetails.text));
}
return res.result?.value;
}
export function saveJson(file, data) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
}