#!/usr/bin/env node /** * cdp_click.js —— 用 CDP 在页面里派发**真实(trusted)鼠标事件** (点击 / 拖动)。 * 用于驱动真实浏览器里的验证码 widget (合成 MouseEvent 会被 isTrusted 检测)。 * * 用法: * # 依次点击若干点 (页面视口坐标) * node tools/cdp_click.js --match 1ms.run --clicks "520,700;300,600" * # 拖动: 从 (x,y) 水平/任意拖到 (x2,y2), 分 steps 步 (带中间移动) * node tools/cdp_click.js --match 1ms.run --drag "200,700,600,700,30" * * 注意: 坐标是**顶层页面视口**坐标 (iframe 内的元素要先换算成页面坐标)。 */ function arg(name, def) { const i = process.argv.indexOf('--' + name); if (i < 0) return def; const v = process.argv[i + 1]; return (v === undefined || v.startsWith('--')) ? true : v; } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new Cdp(ws); ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && c.waits.has(m.id)) { const { res, rej } = c.waits.get(m.id); c.waits.delete(m.id); m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); } }; return c; } send(method, params = {}) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((res, rej) => this.waits.set(id, { res, rej })); } close() { try { this.ws.close(); } catch (e) {} } } const sleep = ms => new Promise(r => setTimeout(r, ms)); (async () => { const port = parseInt(arg('port', '9222'), 10); const match = arg('match', null); const targets = (await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()) .filter(t => t.type === 'page'); const target = match ? targets.find(t => (t.url || '').includes(match)) : targets[0]; if (!target) throw new Error('没有可用标签页'); const cdp = await Cdp.connect(target.webSocketDebuggerUrl); const click = async (x, y) => { const base = { x: Math.round(x), y: Math.round(y), button: 'left', clickCount: 1, buttons: 1, pointerType: 'mouse' }; await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...base, buttons: 0 }); await sleep(40); await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...base }); await sleep(60); await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...base }); await sleep(260); }; const clicks = arg('clicks', null); if (clicks && clicks !== true) { for (const p of String(clicks).split(';')) { const [x, y] = p.split(',').map(Number); if (Number.isFinite(x) && Number.isFinite(y)) await click(x, y); } console.log(JSON.stringify({ clicked: String(clicks).split(';').length, target: target.url })); } const drag = arg('drag', null); if (drag && drag !== true) { const [x1, y1, x2, y2, steps] = String(drag).split(',').map(Number); const n = steps || 30; const base = { button: 'left', buttons: 1, pointerType: 'mouse', clickCount: 1 }; await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: x1, y: y1, buttons: 0 }); await sleep(80); await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: x1, y: y1, ...base }); for (let i = 1; i <= n; i++) { const t = i / n; const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; const x = x1 + (x2 - x1) * ease + (Math.random() - 0.5) * 1.5; const y = y1 + (y2 - y1) * ease + (Math.random() - 0.5) * 1.5; await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: Math.round(x), y: Math.round(y), ...base }); await sleep(12 + Math.random() * 10); } await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: x2, y: y2, ...base }); console.log(JSON.stringify({ dragged: [x1, y1, x2, y2], steps: n, target: target.url })); } cdp.close(); })().catch(e => { console.error('CDP_ERROR: ' + e.message); process.exit(1); });