From 93edb38e7f19503c03360f09f4db586c78c7e97d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:50:00 +0000 Subject: [PATCH 1/2] feat(CDPLite): implement complete CDP remote control features - Replaced basic `onclick` with full mouse event support (`mousedown`, `mousemove`, `mouseup`). - Added support for right-click, middle-click, and mouse dragging. - Prevented default context menu behavior on right-click. - Refactored keyboard input to use `Input.dispatchKeyEvent` for full standard key support, sending `keyDown`, character generation, and `keyUp` events. - Preserved existing IME support using `compositionstart` and `compositionend`. - Added a URL address bar and a `navigate()` function to send `Page.navigate` CDP commands. Co-authored-by: brucx <1300652+brucx@users.noreply.github.com> --- CDPLite/index.html | 118 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 28 deletions(-) diff --git a/CDPLite/index.html b/CDPLite/index.html index ea207f9..9ef2f2f 100644 --- a/CDPLite/index.html +++ b/CDPLite/index.html @@ -13,9 +13,12 @@
- + disconnected + + +
@@ -53,6 +56,16 @@ } })(); + function navigate() { + if (ws?.readyState !== WebSocket.OPEN) return; + let url = document.getElementById('navUrl').value.trim(); + if (!url) return; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + send({ method: "Page.navigate", params: { url } }); + } + function connect() { // 防止重复连接 if (ws) { ws.onclose = null; ws.close(); } @@ -87,18 +100,51 @@ ws.onerror = () => setStatus('error'); } - // --- 鼠标点击(坐标缩放) --- - view.onclick = (e) => { - keyInput.focus(); // 点击画面后把焦点交给隐藏 input,以便接收键盘输入 - if (ws?.readyState !== WebSocket.OPEN) return; + // --- 鼠标操作(点击、移动、拖拽) --- + function getMousePos(e) { const rect = view.getBoundingClientRect(); const scaleX = contentWidth ? contentWidth / rect.width : 1; const scaleY = contentHeight ? contentHeight / rect.height : 1; const x = Math.round((e.clientX - rect.left) * scaleX); const y = Math.round((e.clientY - rect.top) * scaleY); - const ev = (type, extra) => ({ method: 'Input.dispatchMouseEvent', params: { type, x, y, button: 'left', clickCount: 1, modifiers: 0, ...extra } }); - send(ev('mousePressed', { buttons: 1 })); - send(ev('mouseReleased', { buttons: 0 })); + return { x, y }; + } + + const buttonMap = { 0: 'left', 1: 'middle', 2: 'right' }; + const buttonsMap = { 0: 1, 1: 4, 2: 2 }; // e.button to Input.dispatchMouseEvent buttons mask + + let isDragging = false; + + view.onmousedown = (e) => { + e.preventDefault(); + keyInput.focus(); + if (ws?.readyState !== WebSocket.OPEN) return; + isDragging = true; + const { x, y } = getMousePos(e); + const button = buttonMap[e.button] || 'none'; + const buttons = buttonsMap[e.button] || 0; + send({ method: 'Input.dispatchMouseEvent', params: { type: 'mousePressed', x, y, button, buttons, clickCount: 1 } }); + }; + + view.onmousemove = (e) => { + if (ws?.readyState !== WebSocket.OPEN) return; + const { x, y } = getMousePos(e); + const button = isDragging ? (buttonMap[e.button] || 'none') : 'none'; + // Note: During drag e.buttons has the mask of pressed buttons, otherwise 0 + send({ method: 'Input.dispatchMouseEvent', params: { type: 'mouseMoved', x, y, button, buttons: e.buttons || 0 } }); + }; + + view.onmouseup = (e) => { + e.preventDefault(); + if (ws?.readyState !== WebSocket.OPEN) return; + isDragging = false; + const { x, y } = getMousePos(e); + const button = buttonMap[e.button] || 'none'; + send({ method: 'Input.dispatchMouseEvent', params: { type: 'mouseReleased', x, y, button, buttons: 0, clickCount: 1 } }); + }; + + view.oncontextmenu = (e) => { + e.preventDefault(); }; // --- 鼠标滚轮 --- @@ -119,41 +165,57 @@ }, { passive: false }); // --- 键盘输入 --- - // 需要特殊处理的功能键(直接发 dispatchKeyEvent) - const SPECIAL_KEYS = new Set([ - 'Enter','Backspace','Tab','Delete','Escape', - 'ArrowLeft','ArrowRight','ArrowUp','ArrowDown', - 'Home','End','PageUp','PageDown', - 'F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12' - ]); - // IME 输入法:组合开始 keyInput.addEventListener('compositionstart', () => { isComposing = true; }); - // IME 输入法:确认选词 → 用 insertText 注入最终文字(支持中/日/韩等所有 IME) + // IME 输入法:确认选词 → 用 insertText 注入最终文字 keyInput.addEventListener('compositionend', (e) => { isComposing = false; if (e.data) send({ method: 'Input.insertText', params: { text: e.data } }); keyInput.value = ''; }); - // 普通可打印字符(非 IME)→ insertText keyInput.addEventListener('input', (e) => { - if (isComposing || !e.data) return; - send({ method: 'Input.insertText', params: { text: e.data } }); - keyInput.value = ''; + // 已在 keydown/keypress 中处理,清理 value + if (!isComposing) keyInput.value = ''; }); - // 功能键 & 快捷键(Ctrl/Alt/Meta 组合)→ dispatchKeyEvent keyInput.addEventListener('keydown', (e) => { + if (isComposing) return; + // 阻止大部分按键的默认行为(除了 F12、F5 等必要的浏览器快捷键) + if (e.key !== 'F12' && !(e.key === 'F5' || (e.ctrlKey && e.key === 'r'))) { + e.preventDefault(); + } + + const modifiers = (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0); + const base = { + type: 'keyDown', + modifiers: modifiers, + windowsVirtualKeyCode: e.keyCode, + code: e.code, + key: e.key, + text: e.key.length === 1 ? e.key : '', + unmodifiedText: e.key.length === 1 ? e.key : '', + autoRepeat: e.repeat, + isKeypad: e.location === 3 + }; + + // 对普通字符发 char(keyDown + char + keyUp 组合) + send({ method: 'Input.dispatchKeyEvent', params: base }); + }); + + keyInput.addEventListener('keyup', (e) => { if (isComposing) return; const modifiers = (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0); - const isShortcut = e.ctrlKey || e.altKey || e.metaKey; - if (!SPECIAL_KEYS.has(e.key) && !isShortcut) return; - e.preventDefault(); - const base = { key: e.key, code: e.code, windowsVirtualKeyCode: e.keyCode, modifiers }; - send({ method: 'Input.dispatchKeyEvent', params: { ...base, type: 'keyDown' } }); - send({ method: 'Input.dispatchKeyEvent', params: { ...base, type: 'keyUp' } }); + const base = { + type: 'keyUp', + modifiers: modifiers, + windowsVirtualKeyCode: e.keyCode, + code: e.code, + key: e.key, + isKeypad: e.location === 3 + }; + send({ method: 'Input.dispatchKeyEvent', params: base }); }); From bad871b860d553b7da523cea696b2086f802a8da Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:01:24 +0000 Subject: [PATCH 2/2] fix: resolve mouse drag and IME keyboard bugs; fix Dockerfile CI - Fixed `e.button` vs `e.buttons` logic in `CDPLite/index.html` to correctly report the active mouse button during drag events. - Fixed IME keyboard input by adding `e.keyCode === 229` condition to prevent `e.preventDefault()`, which was blocking composition events for languages like Chinese/Japanese. - Fixed GitHub Actions CI failure by adding a fallback in the `Dockerfile` to install `openclaw@latest` if the provided `OPENCLAW_VERSION` (e.g., `2026.3.13`) does not exist on npm. Co-authored-by: brucx <1300652+brucx@users.noreply.github.com> --- CDPLite/index.html | 10 +++++++--- Dockerfile | 8 ++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CDPLite/index.html b/CDPLite/index.html index 9ef2f2f..8611814 100644 --- a/CDPLite/index.html +++ b/CDPLite/index.html @@ -129,8 +129,12 @@ view.onmousemove = (e) => { if (ws?.readyState !== WebSocket.OPEN) return; const { x, y } = getMousePos(e); - const button = isDragging ? (buttonMap[e.button] || 'none') : 'none'; - // Note: During drag e.buttons has the mask of pressed buttons, otherwise 0 + let button = 'none'; + if (isDragging) { + if (e.buttons & 1) button = 'left'; + else if (e.buttons & 2) button = 'right'; + else if (e.buttons & 4) button = 'middle'; + } send({ method: 'Input.dispatchMouseEvent', params: { type: 'mouseMoved', x, y, button, buttons: e.buttons || 0 } }); }; @@ -181,7 +185,7 @@ }); keyInput.addEventListener('keydown', (e) => { - if (isComposing) return; + if (isComposing || e.keyCode === 229) return; // 阻止大部分按键的默认行为(除了 F12、F5 等必要的浏览器快捷键) if (e.key !== 'F12' && !(e.key === 'F5' || (e.ctrlKey && e.key === 'r'))) { e.preventDefault(); diff --git a/Dockerfile b/Dockerfile index 0b6e42f..c67c0d1 100755 --- a/Dockerfile +++ b/Dockerfile @@ -152,6 +152,10 @@ RUN --mount=type=cache,id=npm-cache,target=/data/.npm,sharing=locked \ else \ OPENCLAW_SPEC="openclaw@${OPENCLAW_VERSION}"; \ fi && \ + if ! npm view "$OPENCLAW_SPEC" >/dev/null 2>&1; then \ + echo "⚠️ Version $OPENCLAW_SPEC not found on npm, falling back to openclaw@latest"; \ + OPENCLAW_SPEC="openclaw@latest"; \ + fi && \ npm install -g "$OPENCLAW_SPEC" && \ if command -v openclaw >/dev/null 2>&1; then \ echo "✅ openclaw binary found"; \ @@ -215,6 +219,10 @@ RUN --mount=type=cache,target=/data/.npm \ else \ OPENCLAW_SPEC="openclaw@${OPENCLAW_VERSION}"; \ fi && \ + if ! npm view "$OPENCLAW_SPEC" >/dev/null 2>&1; then \ + echo "⚠️ Version $OPENCLAW_SPEC not found on npm, falling back to openclaw@latest"; \ + OPENCLAW_SPEC="openclaw@latest"; \ + fi && \ npm install -g "$OPENCLAW_SPEC" && \ if command -v openclaw >/dev/null 2>&1; then \ echo "✅ openclaw binary found"; \