cdp.mjs (4576B)
1 // Pilotage d'un Chromium headless par le protocole DevTools, sans dépendance (pas de Playwright sur hotrats). 2 import { spawn } from 'node:child_process'; 3 import { writeFileSync } from 'node:fs'; 4 5 export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 6 7 export async function open(url, { port = Number(process.env.CDP_PORT ?? 9334), width = 1000, height = 1400 } = {}) { 8 const chrome = spawn(process.env.CHROME ?? 'chromium', [ 9 '--headless=new', '--no-sandbox', '--hide-scrollbars', 10 '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', 11 `--remote-debugging-port=${port}`, `--window-size=${width},${height}`, 'about:blank', 12 ], { stdio: 'ignore' }); 13 14 let target; 15 for (let i = 0; i < 50 && !target; i++) { 16 await sleep(200); 17 try { 18 const r = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' }); 19 target = await r.json(); 20 } catch { /* pas encore prêt */ } 21 } 22 if (!target) { chrome.kill(); throw new Error('Chromium ne répond pas'); } 23 24 const ws = new WebSocket(target.webSocketDebuggerUrl); 25 await new Promise((r) => ws.addEventListener('open', r)); 26 let id = 0; 27 const pending = new Map(); 28 const logs = []; 29 ws.addEventListener('message', (ev) => { 30 const m = JSON.parse(ev.data); 31 if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? m.error); pending.delete(m.id); } 32 else if (m.method === 'Runtime.consoleAPICalled') logs.push(`[${m.params.type}] ${m.params.args.map((a) => a.value ?? a.description).join(' ')}`); 33 else if (m.method === 'Runtime.exceptionThrown') logs.push(`[exception] ${m.params.exceptionDetails.exception?.description ?? m.params.exceptionDetails.text}`); 34 }); 35 const send = (method, params = {}) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method, params })); }); 36 const evaluate = async (expression) => (await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })).result?.value; 37 const screenshot = async (path) => { 38 const png = (await send('Page.captureScreenshot', { format: 'png' })).data; 39 writeFileSync(path, Buffer.from(png, 'base64')); 40 return path; 41 }; 42 // Coordonnées de la page pour un point de l'écran Smalltalk (640×480 mis à l'échelle CSS) 43 const canvasPoint = async (x, y) => { 44 const r = await evaluate("(() => { const c = document.getElementById('canvas').getBoundingClientRect(); return [c.left, c.top, c.width, c.height]; })()"); 45 const [left, top, w, h] = r; 46 return { x: left + (x * w) / 640, y: top + (y * h) / 480 }; 47 }; 48 const mouse = async (type, x, y, button = 'none', clickCount = 1) => 49 send('Input.dispatchMouseEvent', { type, x, y, button, clickCount, buttons: button === 'left' ? 1 : button === 'right' ? 2 : button === 'middle' ? 4 : 0 }); 50 const click = async (sx, sy, button = 'left') => { 51 const p = await canvasPoint(sx, sy); 52 await mouse('mouseMoved', p.x, p.y); 53 await sleep(100); 54 await mouse('mousePressed', p.x, p.y, button); 55 await sleep(120); 56 await mouse('mouseReleased', p.x, p.y, button); 57 }; 58 // Frappe : chaque touche porte un `code` physique et un keyCode, sinon SDL ne la reconnaît pas 59 // (symbole 0) ; les caractères passent par le keypress (texte), Entrée par le keydown seul. 60 const keyOf = (ch) => { 61 if (ch === '\r') return { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, text: '\r' }; 62 if (/[a-z]/i.test(ch)) return { key: ch, code: `Key${ch.toUpperCase()}`, windowsVirtualKeyCode: ch.toUpperCase().charCodeAt(0), text: ch }; 63 if (/[0-9]/.test(ch)) return { key: ch, code: `Digit${ch}`, windowsVirtualKeyCode: ch.charCodeAt(0), text: ch }; 64 if (ch === ' ') return { key: ' ', code: 'Space', windowsVirtualKeyCode: 32, text: ' ' }; 65 return { key: ch, code: 'Unidentified', windowsVirtualKeyCode: 0, text: ch }; 66 }; 67 const type = async (text) => { 68 for (const ch of text) { 69 const k = keyOf(ch); 70 await send('Input.dispatchKeyEvent', { type: 'keyDown', key: k.key, code: k.code, windowsVirtualKeyCode: k.windowsVirtualKeyCode, text: k.text, unmodifiedText: k.text }); 71 await send('Input.dispatchKeyEvent', { type: 'keyUp', key: k.key, code: k.code, windowsVirtualKeyCode: k.windowsVirtualKeyCode }); 72 await sleep(40); 73 } 74 }; 75 const close = () => { ws.close(); chrome.kill(); }; 76 77 await send('Runtime.enable'); 78 await send('Page.enable'); 79 await send('Page.navigate', { url }); 80 return { send, evaluate, logs, screenshot, canvasPoint, mouse, click, type, close }; 81 }