Files
code-utils/tools/cdp.js
2026-08-14 07:52:01 +08:00

120 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// cdp.js - drive the Code Count WebView2 via Chrome DevTools Protocol.
// Usage:
// node cdp.js eval "<expression>" evaluate JS (await-ed, JSON result)
// node cdp.js click <x> <y> trusted click at CSS viewport coords
// node cdp.js rclick <x> <y> trusted right-click
// node cdp.js key <Key> [text] dispatch a key press (e.g. Enter, Escape)
// node cdp.js type "<text>" insert text into the focused element
// node cdp.js shot <file.png> capture in-page screenshot
// node cdp.js dialog <accept|dismiss> set auto dialog policy for this run (default: report only)
// Multiple commands can be chained with ";;" between argument groups.
const PORT = process.env.CC_DEVTOOLS_PORT || '9222';
let seq = 0;
const pending = new Map();
let ws;
let dialogPolicy = null;
const dialogEvents = [];
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
}
async function connect() {
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
const targets = await res.json();
const page = targets.find(t => t.type === 'page');
if (!page) throw new Error('no page target');
ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((ok, bad) => { ws.onopen = ok; ws.onerror = bad; });
ws.onmessage = ev => {
const msg = JSON.parse(ev.data);
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
} else if (msg.method === 'Page.javascriptDialogOpening') {
dialogEvents.push(msg.params);
console.log('DIALOG-OPEN', JSON.stringify({ type: msg.params.type, message: msg.params.message }));
if (dialogPolicy) {
send('Page.handleJavaScriptDialog', { accept: dialogPolicy === 'accept' })
.then(() => console.log('DIALOG-HANDLED', dialogPolicy))
.catch(e => console.log('DIALOG-ERR', e.message));
}
}
};
await send('Page.enable');
await send('Runtime.enable');
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function mouse(type, x, y, button, clickCount = 1) {
await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none' });
await sleep(40);
await send('Input.dispatchMouseEvent', { type, x, y, button, clickCount });
}
async function click(x, y, button = 'left') {
await mouse('mousePressed', x, y, button);
await sleep(50);
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button, clickCount: 1 });
}
async function main() {
const groups = process.argv.slice(2).join('\u0000').split('\u0000;;\u0000').map(g => g.split('\u0000'));
await connect();
for (const g of groups) {
const [cmd, ...args] = g;
if (cmd === 'dialog') {
dialogPolicy = args[0];
console.log('dialog policy:', dialogPolicy);
} else if (cmd === 'eval') {
const r = await send('Runtime.evaluate', { expression: args.join(' '), awaitPromise: true, returnByValue: true });
console.log('EVAL', JSON.stringify(r.result && 'value' in r.result ? r.result.value : r.result));
if (r.exceptionDetails) console.log('EXC', JSON.stringify(r.exceptionDetails.exception?.description || r.exceptionDetails.text));
} else if (cmd === 'click') {
await click(Number(args[0]), Number(args[1]));
console.log('clicked', args[0], args[1]);
} else if (cmd === 'rclick') {
await click(Number(args[0]), Number(args[1]), 'right');
console.log('right-clicked', args[0], args[1]);
} else if (cmd === 'key') {
const key = args[0];
const defs = { Enter: { code: 'Enter', keyCode: 13, text: '\r' }, Escape: { code: 'Escape', keyCode: 27 }, Tab: { code: 'Tab', keyCode: 9 } };
const d = defs[key] || { code: key, keyCode: key.charCodeAt(0) };
await send('Input.dispatchKeyEvent', { type: 'keyDown', key, code: d.code, windowsVirtualKeyCode: d.keyCode, text: d.text });
await sleep(30);
await send('Input.dispatchKeyEvent', { type: 'keyUp', key, code: d.code, windowsVirtualKeyCode: d.keyCode });
console.log('key', key);
} else if (cmd === 'type') {
await send('Input.insertText', { text: args.join(' ') });
console.log('typed', args.join(' '));
} else if (cmd === 'shot') {
const shot = await send('Page.captureScreenshot', { format: 'png' });
require('fs').writeFileSync(args[0], Buffer.from(shot.data, 'base64'));
console.log('saved', args[0]);
} else if (cmd === 'viewport') {
// viewport <w> <h> 模拟视口尺寸viewport 0 恢复真实尺寸
const w = Number(args[0]);
if (w) await send('Emulation.setDeviceMetricsOverride', { width: w, height: Number(args[1] || 700), deviceScaleFactor: 0, mobile: false });
else await send('Emulation.clearDeviceMetricsOverride');
console.log('viewport', args.join('x') || 'cleared');
} else if (cmd === 'wait') {
await sleep(Number(args[0] || 500));
} else {
throw new Error('unknown cmd: ' + cmd);
}
await sleep(120);
}
// give async dialog handlers a moment before exit
await sleep(300);
ws.close();
}
main().catch(e => { console.error('FATAL', e.message); process.exit(1); });