136 lines
4.0 KiB
JavaScript
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 };
|