136 lines
4.0 KiB
JavaScript

import { spawn } from 'node:child_process';
import { setTimeout as sleep } from 'node:timers/promises';
import fs from 'node:fs';
const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
export async function launchEdge({ port = 9333, profileDir, headless = true, downloadDir } = {}) {
const args = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-features=Translate,OptimizationHints',
'--window-size=1600,1000',
];
if (headless) args.push('--headless=new');
if (downloadDir) args.push('--disable-popup-blocking');
const child = spawn(EDGE, args, { detached: false, stdio: 'ignore' });
let version = null;
for (let i = 0; i < 60; i++) {
try {
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
version = await res.json();
break;
} catch {
await sleep(500);
}
}
if (!version) throw new Error('Edge 调试端口未就绪');
return { child, version, port };
}
export class CDP {
constructor(ws) {
this.ws = ws;
this.seq = 0;
this.pending = new Map();
this.listeners = [];
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);
return;
}
for (const l of this.listeners) l(msg);
});
}
static async connect(wsUrl) {
const ws = new WebSocket(wsUrl);
await new Promise((resolve, reject) => {
ws.addEventListener('open', resolve);
ws.addEventListener('error', reject);
});
return new CDP(ws);
}
send(method, params = {}, sessionId) {
const id = ++this.seq;
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 }));
}
on(fn) {
this.listeners.push(fn);
}
waitForEvent(predicate, timeoutMs = 60000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('等待事件超时')), timeoutMs);
const fn = (msg) => {
if (predicate(msg)) {
clearTimeout(timer);
this.listeners = this.listeners.filter((l) => l !== fn);
resolve(msg);
}
};
this.on(fn);
});
}
close() {
try { this.ws.close(); } catch {}
}
}
export async function openPage(cdp, url = 'about:blank') {
const { targetId } = await cdp.send('Target.createTarget', { url });
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true });
await cdp.send('Page.enable', {}, sessionId);
await cdp.send('Runtime.enable', {}, sessionId);
return { targetId, sessionId };
}
export async function goto(cdp, sessionId, url, { timeoutMs = 60000 } = {}) {
const done = cdp.waitForEvent(
(m) => m.sessionId === sessionId && m.method === 'Page.loadEventFired',
timeoutMs,
).catch(() => null);
await cdp.send('Page.navigate', { url }, sessionId);
await done;
await sleep(800);
}
export async function evaluate(cdp, sessionId, expression, awaitPromise = true) {
const res = await cdp.send('Runtime.evaluate', {
expression,
awaitPromise,
returnByValue: true,
}, sessionId);
if (res.exceptionDetails) throw new Error('JS 异常: ' + JSON.stringify(res.exceptionDetails).slice(0, 500));
return res.result?.value;
}
export async function shot(cdp, sessionId, file) {
const { data } = await cdp.send('Page.captureScreenshot', { format: 'png' }, sessionId);
fs.writeFileSync(file, Buffer.from(data, 'base64'));
return file;
}
export async function setDownloadDir(cdp, dir) {
fs.mkdirSync(dir, { recursive: true });
await cdp.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: dir,
eventsEnabled: true,
});
return dir;
}
export { sleep };