From d1c58d8ff0d526300bdfc633da819809fe81abbd Mon Sep 17 00:00:00 2001 From: shenk-b Date: Wed, 24 Jun 2026 10:56:01 +0800 Subject: [PATCH] fix(proxy): detect stale proxy after browser restart + surface /navigate errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A long-running cdp-proxy left over from before Chrome/Edge restarted passes /health (connected:true) but holds dead sessions: every Page.navigate silently fails, tabs stay on about:blank, and the /navigate handler returns an EMPTY response body — so the caller has no signal to act on. This makes the skill look totally broken ("CDP can do nothing") for as long as the stale proxy survives. check-deps reuses any proxy that answers /health, never verifying that sessions actually work, so the dead proxy is reused indefinitely. Two fixes: 1. check-deps.mjs — add a functional probe (new about:blank tab -> eval -> close) in the reuse branch. If it fails, kill the stale proxy and restart, so a browser restart no longer silently breaks all CDP ops. 2. cdp-proxy.mjs — /navigate now returns HTTP 502 + the CDP error JSON when Page.navigate yields a {error} (or no result), instead of writing an empty body that hides the failure. Repro of the original bug (any machine): - start Chrome with --remote-debugging-port, run check-deps (proxy up) - quit & relaunch Chrome (new process, same port) - run check-deps again -> "proxy: ready" (health still ok) - /new -> tab stuck on about:blank; /navigate -> empty response After this patch, step 3 detects the stale proxy and restarts it. Co-Authored-By: Claude Fable 5 --- scripts/cdp-proxy.mjs | 9 ++++++++- scripts/check-deps.mjs | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/scripts/cdp-proxy.mjs b/scripts/cdp-proxy.mjs index 9457405..82162b1 100755 --- a/scripts/cdp-proxy.mjs +++ b/scripts/cdp-proxy.mjs @@ -399,7 +399,14 @@ const server = http.createServer(async (req, res) => { // 等待页面加载完成 await waitForLoad(sid); - res.end(JSON.stringify(resp.result)); + // 显式透出导航错误:浏览器重启导致僵尸会话时 Page.navigate 会返回 {error}(无 result), + // 直接 JSON.stringify(resp.result) 会写出空 body,调用方无从判断。这里把错误透出来。 + if (resp.error) { + res.statusCode = 502; + res.end(JSON.stringify({ error: 'Page.navigate failed', cdp: resp.error })); + return; + } + res.end(JSON.stringify(resp.result ?? { error: 'empty navigation result' })); } // GET /back?target=xxx - 后退 diff --git a/scripts/check-deps.mjs b/scripts/check-deps.mjs index 87c49ba..1978edd 100644 --- a/scripts/check-deps.mjs +++ b/scripts/check-deps.mjs @@ -8,7 +8,7 @@ // 持久偏好 → config.env (skill 根目录, gitignored) // 单次覆盖 → --browser 命令行参数(全链路 argv,不碰 process.env) -import { spawn } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -75,9 +75,27 @@ function startProxyDetached(browserOverride) { fs.closeSync(logFd); } +// 功能性探针:health/targets 正常不等于会话可用。浏览器若在 proxy 运行期间重启过, +// 旧 proxy 的会话会失效——所有 Page.navigate 静默失败、tab 卡在 about:blank,但 /health 仍报 ok。 +// 建一个 about:blank 测试 tab → eval → close,全过才算真健康。 +async function functionalProbe(baseUrl) { + try { + const newRes = await fetch(`${baseUrl}/new`, { method: 'POST', body: 'about:blank', signal: AbortSignal.timeout(5000) }); + const { targetId } = await newRes.json(); + if (!targetId) return false; + const evalRes = await fetch(`${baseUrl}/eval?target=${targetId}`, { method: 'POST', body: '1+1', signal: AbortSignal.timeout(5000) }); + const evalJson = await evalRes.json(); + await fetch(`${baseUrl}/close?target=${targetId}`, { signal: AbortSignal.timeout(5000) }).catch(() => {}); + return evalJson?.value === 2; + } catch { + return false; + } +} + async function ensureProxy(expectedBrowserId, browserOverride) { - const healthUrl = `http://127.0.0.1:${PROXY_PORT}/health`; - const targetsUrl = `http://127.0.0.1:${PROXY_PORT}/targets`; + const baseUrl = `http://127.0.0.1:${PROXY_PORT}`; + const healthUrl = `${baseUrl}/health`; + const targetsUrl = `${baseUrl}/targets`; // 复用:proxy 已运行 + 已连接浏览器 → 校验 expected vs actual const health = await httpGetJson(healthUrl); @@ -89,8 +107,17 @@ async function ensureProxy(expectedBrowserId, browserOverride) { console.log(' 请在终端运行 pkill -f cdp-proxy.mjs 重置后再试'); return false; } - console.log(`proxy: ready (${runningLabel})`); - return true; + // 健康但未必能用:实测一遍 new/eval/close 是否真的工作;失败则判定为僵尸 proxy + // (浏览器重启后遗留的旧进程,会话失效),杀掉后走下方重启分支。 + const probeOk = await functionalProbe(baseUrl); + if (probeOk) { + console.log(`proxy: ready (${runningLabel})`); + return true; + } + console.log(`proxy: stale — 健康检查通过但功能性探针失败(浏览器可能已重启),杀掉旧 proxy 重启...`); + try { spawnSync('pkill', ['-f', 'cdp-proxy.mjs']); } catch {} + await new Promise((r) => setTimeout(r, 1500)); + // 落入下方 startProxyDetached 重启分支 } console.log('proxy: connecting...');