Back to Qwen Code

Plan C — CDP 隧道:复用 DevTools MCP 工具驱动真实浏览器

packages/chrome-extension/docs/06-plan-c-cdp-tunnel.md

0.19.913.7 KB
Original Source

Plan C — CDP 隧道:复用 DevTools MCP 工具驱动真实浏览器

设计文档(可行性 + 实施方案)。配套:05-daemon-direct-architecture.md(Phase 1/2 已落地:side panel + 逆向工具通道 chrome-tools)。 关联 issue #5626 / PR #5777。

0. TL;DR

  • 问题:当前 chrome-tools 逆向通道(Plan A)在扩展端用 chrome.* 重新实现上游 DevTools MCP server 已有的能力(console / network / screenshot…),每加一个新能力都要手写 executor。
  • Plan C:扩展只用 chrome.debugger 把真实标签页的 CDP 协议透传给 daemon,daemon 暴露一个 CDP endpoint 让 外部 DevTools MCP adapter 连进来 —— 复用它现成的全套工具,操作用户真实浏览器,不再逐个手写。
  • 结论:有条件可行。能让浏览器自动化运行时连上单 tab(命题 A,可行);但「把未改的上游 adapter 直接接上」(命题 B)不可行 —— 必须给它打一个小补丁(patch-package,~2 处 / 十几行 diff)。
  • 形态(历史方案,已不作为主包方案):早期评估过 pin 上游 adapter + patch-package。0.19.4 的包扫描问题证明这类依赖、patch 和安装钩子不应进入主 CLI 包;后续应走显式外部 adapter 命令。
  • 建议:先做 Phase 0 半天 spike 钉死那堵墙,再决定是否全量上。

1. 决定性发现:为什么「零改造复用」不行(已逐行核实 1.4.0 源码)

  1. puppeteer 的硬墙Connection._createSessioncdp/Connection.js:232)在 createCDPSession()无条件Target.attachToTarget {flatten:true},拿不到 sessionIdthrow new Error('CDPSession creation failed.')
  2. chrome.debugger 拒绝:对 Target.attachToTarget 返回 -32000 'Not allowed'(Puppeteer issue #13251,OPEN/confirmed,属 Chrome 安全限制,非 bug)。
  3. 官方原语堵不住:puppeteer 自带的 cdp/ExtensionTransport.js 只合成 4 条命令(Browser.getVersion / Target.getBrowserContexts / Target.setDiscoverTargets / Target.setAutoAttach),没有 attachToTarget 分支,fall-through 到 chrome.debugger.sendCommand → 被拒。
  4. cdp-mcp 把它放在强制启动路径且不吞异常McpContext.from()#init()build/src/McpContext.js:76-83)无条件调 UniverseManager.initServiceWorkerConsoleCollector.init,二者在 DevtoolsUtils.js:166 / ServiceWorkerCollector.js:19page.createCDPSession() → 撞墙 → 在第一个工具被调用前整体启动失败

patch 实测(1.4.0)

createCDPSession 在 cdp-mcp 自己代码里仅 2 处,都在 #init 路径:

文件:行触发点处理
build/src/devtools/DevtoolsUtils.js:166UniverseManager.initMcpContext.#init:81try-catch / 跳过
build/src/ServiceWorkerCollector.js:19ServiceWorkerConsoleCollector.initMcpContext.#init:82try-catch / 跳过

third_party/ 里另有 3 处是 puppeteer/lighthouse 库自带的方法定义,不碰。)

最小 patch:把 McpContext.#init 的第 81、82 行 try-catch 包住(或 stub),核心 2 处改动、patch 文件 ~10-20 行 diff。 代价performance_*(trace/insight)、service-worker console 这类依赖额外 CDP session 的工具降级;page 级核心(snapshot/click/fill/screenshot/network/console/emulation)不受影响。

生命周期对抗审查结论

  • 「MV3 30s idle teardown 掐断 debug session」是假的 —— Chrome 118+ active debugger session 保活 SW + 116+ WS 收发重置 idle 计时器,双覆盖,无需额外 keepalive。
  • ⚠️ 必须自己补的胶水:ExtensionTransport 没注册 chrome.debugger.onDetach,用户开 DevTools / 点 banner Cancel / 页面崩溃时 puppeteer 不被通知 → 须 onDetach → onclose + 重连
  • 其余为可处理降级项:5 分钟单请求硬上限(puppeteer 默认 protocolTimeout=180s 会先报错)、调试 banner(UX,不可避免)、单 tab 单 debugger 互斥(与现有 chrome_network_debugger_* 加互斥门)。

2. 架构

DevTools MCP adapter           qwen serve daemon              扩展(MV3)        真实 tab
 puppeteer.connect   browser级  ┌ /cdp WS endpoint ┐  reverse  cdp-bridge   chrome.
 ({browserWS}) ───CDP───────▶  │ CdpBrowserEmulator│   WS /acp  chrome.      debugger
                               │ ①本地应答browser域│ cdp_command .debugger ──▶ Page/DOM
   page域命令(sessionId=S1)─▶  │ ②合成attachedTo-  │ ────────▶  .sendCommand    /Net/...
   ◀── result/event           │   Target(S1)      │ cdp_event  onEvent ◀──── events
                               │ ③去/贴 sessionId  │ ◀────────
                               └ cdp-reverse-link ┘
 规则:有 sessionId ⇒ 转发给 tab;无 sessionId ⇒ daemon 本地应答(browser 域 ~6 条)

组件:

  • DevTools MCP adapter + browser automation runtime:通过 browserWSEndpoint: 'ws://127.0.0.1:PORT/cdp' 连接 daemon 暴露的 CDP endpoint。
  • daemon /cdp WS endpoint(新增):raw CDP 帧 + CdpBrowserEmulator(browser-level 合成 + sessionId 打/解标签)。
  • daemon 现有 /acp reverse WS:扩展已建立、过了 ACP-initialize 鉴权的那条 socket,新增 cdp_* 帧。
  • cdp-reverse-link:把 /cdp puppeteer socket 与扩展 /acp 连接配对(单 daemon = 单扩展 = 单 browser)。
  • 扩展 cdp-bridge(新增):把现有 network-only 的 chrome.debugger 泛化为全 domain 透传 + onDetach

3. 最小要模拟的 browser-level CDP(全部由 daemon CdpBrowserEmulator 本地应答)

命令/事件作用难度
Target.getBrowserContextsconnect 首条,回 {browserContextIds:[]}boilerplate
Browser.getVersion静态返回 / 从 tab 透传boilerplate
Target.setDiscoverTargetsack {}boilerplate
Target.setAutoAttach {flatten}承重:收到后 daemon 合成 Target.attachedToTarget(新 sessionId=S1 + tab targetInfo)关键
Target.getTargets/getTargetInfo返回唯一 page TargetInfoboilerplate
事件 Target.attachedToTarget给 puppeteer 它的 Page,必需关键
事件 Target.detachedFromTargetonDetach 时补(ExtensionTransport 缺)关键
所有 page 域命令(带 sessionId)扩展 chrome.debugger.sendCommand({tabId})已有先例

路由规则:有顶层 sessionId ⇒ 去标签→cdp_command→扩展 sendCommand→回 cdp_result 重贴 sessionId;onEventcdp_event 贴 sessionId。browser 域回复不带 sessionId。

4. 可复用资产

资产省工
puppeteer 内置 cdp/ExtensionTransport.js4 条 browser 命令合成 + 单 page 拓扑可移植进 daemon。要补 onDetach / detachedFromTarget / attachToTarget。
ConnectionTransport 接口(send/close/onmessage?/onclose?daemon reverse 通道契约极薄。
microsoft/playwright-mcp --extension(Apache-2.0)官方同构先例:MV3 chrome.debugger attach 用户 tab → CDP over WS relay。可参考/fork。
remorses/playwriter(MIT, 3.6k★, 活跃)扩展 + WS CDP relay,外部 client connectOverCDP 驱动真实已登录浏览器。
本仓库 /acp reverse 通道WebSocketServer({noServer:true}) + pathname 分支 + clientMcpOverWs 式 feature-flag + mcp_* 帧拦截 —— cdp_* 帧照搬,鉴权/CSRF/origin/maxPayload 全复用。

5. 历史 patch-package 形态(不再作为主包方案)

早期方案考虑过把上游 adapter 作为 pin 依赖并用 patch-package 修补启动路径。这个形态会把浏览器自动化依赖、依赖 patch 和安装钩子放进主包/安装图,容易触发包扫描器。当前方案不再把 adapter 打进主 CLI 包,而是通过显式外部命令接入。

6. 分阶段实施

  • Phase 0 — spike(0.5–1 天):独立脚本起最小 /cdp + ExtensionTransport 4 命令合成,跑未改的上游 adapter 调一次 take_snapshot确认它在 McpContext.from()CDPSession creation failed. → 钉死 fork 范围。
  • Phase 1 — MVP(5–8 天):单 tab take_snapshot/click 跑通。
    • daemon 新增 packages/cli/src/serve/cdp-tunnel/{cdp-ws,cdp-browser-emulator,cdp-reverse-link}.ts
    • daemon 改 acp-http/index.ts(upgrade 加 /cdp 分支,复用 auth/CSRF/origin)+ reverse WS 加 isCdpFrameType 守卫;run-qwen-serve.ts/server.ts/serve/types.ts/serve/capabilities.tscdpTunnelOverWs flag(仿 clientMcpOverWs,默认 OFF)。
    • 扩展新增 cdp-bridge.ts(attach 活动 tab、全 domain 透传、onDetachcdp_detach),复用现有 daemon-WS client;与 chrome_network_debugger_* 互斥门。
    • 新帧:cdp_attach/cdp_attached/cdp_command/cdp_result/cdp_event/cdp_detach
    • cdp-mcp patch(见 §5)。
  • Phase 2 — 稳定性(3–5 天):detach/reconnect 闭环、protocolTimeout 收紧、banner/DevTools 互斥提示。
  • Phase 3 — 多 tab(5–10 天,按需)list_pages/new_page/select_pagechrome.tabs + 每 tab 一条 transport 多路复用;若需 --browserUrl 再补 GET /json/version

每个 Phase 独立可回退到 Plan A;Phase 1 的 daemon 合成层 + 扩展全 domain 透传对 Plan A 也是底座(A 的工具实现也要 sendCommand 透传),沉没成本可控。

7. 风险

风险严重度缓解
createCDPSession 墙(cdp-mcp 启动即挂)阻断patch §5;Phase 0 先证实
ExtensionTransport 无 onDetach扩展补 onDetachcdp_detach,daemon 合成 detachedFromTarget+onclose
单 tab 单 debugger 互斥chrome_network_debugger_* 互斥门
MV3 SW idle teardown已证伪118+ 保活 + 116+ WS 流量,无需 keepalive
5 分钟单请求硬上限puppeteer 180s 先报错;长工具延后/禁用
调试 banner / DevTools 互斥中(UX)onDetach 重连 + 用户提示
受管 Chrome 策略 DeveloperToolsAvailability中(部署)部署前核查策略允许 chrome.debugger(force-installed 扩展 114+ 默认禁,需值 1)
大 payload(截图/getResponseBody) vs maxPayload/cdp 抬高 maxPayload 或分块
浏览器自动化运行时版本漂移外部 adapter 自己 pin 版本;主 CLI 包不携带该依赖

回退:Phase 0 失败 → 放弃 C 回 A;Phase 1 失败 → 回 A(合成层/透传可作 A 底座);Phase 2/3 失败 → 退守单 tab 只读快照/点击形态,仍省大量工具实现。

8. 建议

C 值得做,但:① 走 patch 路线,把「零改造复用」从目标删掉(已被源码钉死);② 先做 Phase 0 半天 spike。 spike 通过后 C 相比 A 净收益明确(page 级工具一次性接管),代价是维护一个 cdp-mcp patch + detach 胶水(A 没有的长期负担,决策时算进)。若 spike 显示受管策略普遍禁 chrome.debugger,则回 A。