120 lines
5.3 KiB
JavaScript
120 lines
5.3 KiB
JavaScript
// 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); });
|