# 浏览器控制插件:设计与落地顺序 > 状态:Phase 0 进行中 —— S2(Host↔Client 桥)**已验证通过**(2026-10-22); > S1(侧边栏 webview 控制)代码完成、等待重启验证;测试先行流程已建立(§10)。 > 事故复盘见 §0。 > 最后更新:2026-10-22。 ## 0. 事故复盘:重启后无限加载(2026-10-22) - **现象**:spike 插件启用时重启 DSH 桌面版,启动停在加载页;从 UI 禁用两个插件后恢复正常。 - **根因**:Client 半 `apply()` 末尾 `return loop;`,而 `loop` 是永不 resolve 的 bridge 轮询 Promise。 Cordis 把 apply 返回的 thenable 视为"仍在加载",client 启动审计会等待每个 entry 就绪 → 永远等不到 → 无限加载。 - **为什么之前没暴露**:首版 client 是通过运行期 HMR 加载的,不走启动审计;只有重启后按 boot graph 加载才会等 apply 完成。 - **修复**:`apply()` 同步返回 `undefined`;bridge 轮询改为 fire-and-forget(`loop.catch(...)` 兜住异常); 整个 apply 包 try/catch;侧边栏 tab 打开改为重试(启动早期会报 `no session surface is mounted`)。 - **防护**:新增 `tests/browser-control-client.test.mjs` 的 **BOOT CONTRACT** 回归测试 (`apply()` 必须返回 `undefined`),并做过变异验证:把 `void loop` 改回 `return loop` 时测试必红。 - **另一条环境事实**:打包版桌面 App 的 client 模块只在**页面启动时**从 boot graph 加载; 运行中新增/替换 client 插件不会生效(graph/rebuilt 帧不落地)。因此 **client 代码每次改动都必须重启 App 验证**, 不要指望 HMR。 目标(用户 2026-10-22 提出): - **A. 侧边栏内置浏览器 + Agent 操控 + 独立虚拟光标** Agent 能打开/点击/输入/滚动;用户能看到 Agent 的“鼠标”与点击反馈;这个鼠标不抢占用户真实系统指针。 - **B. Chrome 扩展 + 配套控制插件** Agent 连接用户真实 Chrome,控制指定标签页;标签页切到后台后仍可继续操作。 原则:不依赖 Playwright/Puppeteer/额外 Chromium 下载(宿主机 F: 仅剩约 11.8 GB), 优先复用 DSH 自身与 Electron 已带的能力;每一步都可安装、可实测、可回滚。 ## 1. 外部参考(OpenAI Codex / ChatGPT 桌面版) 已验证(来源): - Codex CLI 与 IDE 扩展**没有**内置浏览器;内置浏览器位于 ChatGPT/Codex 桌面 App 中, 作为对话旁的浏览器标签页(分栏/全屏)。来源:、 相关 issue 。 - 桌面 App 中由 **Computer Use** 驱动内置浏览器(打开页面、点击、输入、读取渲染状态、截图、验证结果); Windows 上 Computer Use 会接管真实桌面指针、不能后台运行。来源: 、。 - CDP 是**需要显式开启**的开发/检查通道(Settings → Browser → Developer mode → Enable full CDP access, 且可由管理员策略关闭)。来源同上。 - Agent 光标的实现线索:注入到页面里的 shadow-DOM 覆盖层 + 自定义光标元素, 原生光标被隐藏(`cursor: none !important`)。证据: 、; 另有“光标形状的控制标记”在停止后残留的报告 。 - 已知难点:坐标映射(DPI/缩放下点击位置与可见光标错位); 因此实现时必须做点击前命中测试(`elementFromPoint` + 可见/可用/未被遮挡检查)。 可对比的开源实现: - **Nanobrowser**(Apache-2.0,Chrome 侧边栏):只通过 `chrome.debugger` 操作页面, Puppeteer 仅用于 frame 管理;点击/输入是真实浏览器输入;一个任务一个标签组; 操作前检查元素可见、未移动、可用、未被遮挡。无视觉光标。来源:。 - **Stagehand / browser-use / Chrome DevTools MCP / Playwright MCP**:均无公开的“页面内 Agent 光标”实现; Playwright MCP 通过浏览器扩展接管用户已打开的标签页。来源: 、、 、 。 关于 Chrom 扩展的关键结论(来自调研): - OpenAI 的扩展权限里同时出现 “Access the page debugger” 与 “Communicate with cooperating native applications”, 推断其生产实现是 **`chrome.debugger` + native messaging**,而不是 `--remote-debugging-port`。 来源:(权限文案见该页)。 - Chrome 144+ 起,普通 `--remote-debugging-port` 需要用户在 `chrome://inspect` 里手动勾选并允许, 端口动态;因此“直接开调试端口”不再是零摩擦方案(Nanobrowser 因此只走扩展内 `chrome.debugger`)。 - `chrome.debugger` 能操作**后台标签页**(按标签页 attach,不要求前台),这是 Goal B 的核心机制; 代价是 Chrome 会显示“正在调试此浏览器”的提示条(不可屏蔽),权限范围按标签页、可随时撤销。 ## 2. 本机 DSH 可行性(已验证,来自当前安装的 0.2.1-rc.2) - DSH 已自带侧边栏浏览器:`@deepseek-ai/dsh-client-ui-sidebar-browser`。 Desktop 使用 Electron ``;主进程 `DesktopBrowserGuests` 用 **lease** 管控每个 guest: 渲染进程通过 preload 暴露的 `window.dshDesktop.browser.acquire(workspace)` 拿到 `{ lease, partition }`, 然后创建 ``; 主进程在 `will-attach-webview` 校验 lease/partition 并固定安全参数(sandbox、无 node、devTools 打包后关闭), `did-attach-webview` 后才允许页面存活。参考实现位置(app.asar 内): `lib/main.js` 的 `DesktopBrowserGuests`、`dsh-client-ui-sidebar-browser/lib/client.js`、 `lib/preload-app.cjs` 的 `createProductApi().browser`。 - 该 webview 是**用户向**的:DSH 自身不提供 Agent 控制,也没有对外暴露“控制已有 guest”的 API。 - 侧边栏提供了**公开扩展点**,第三方插件可以注册自己的标签页类型与内容: `ctx.sidebarRightTabs.register({ id, kind, title, ... })` + 在 `sidebar.right.pane.tab` 槽位注册 body (`ctx.slots.register`);参考 `@deepseek-ai/dsh-client-ui-sidebar-right/README.md`。 - Host 插件可以: - 注册 Agent 工具(`ctx.tools.register`); - 监听普通 Node HTTP 服务(插件是普通 Node 进程内代码); - 使用 `ctx.webServer.register/registerUpgrade`、`ctx.connection.admit` 等公开服务(备用通道方案)。 - 因此 Goal A 的合理做法:**新建我们自己的“Agent Browser”标签类型,拥有自己的 leased webview**, 由我们自己的客户端插件驱动;不去接管 DSH 自带 Browser 标签的 guest(那是私有实现,跨版本会碎)。 ## 3. 总体架构 ``` ┌───────────────────────── DSH Host 进程 (Node) ─────────────────────────┐ │ plugin: sidebar-browser-control (Host half) │ Agent tools ◄──┤ ctx.tools.register("browser", actions) │ (模型调用) │ ├─ per-session panel registry / command queue / timeout / policy │ │ └─ bridge server: 127.0.0.1: + token │ │ ├─ /panel ←→ 侧边栏客户端插件(自定义 webview) │ │ └─ /ext ←→ Chrome 扩展(Goal B,Phase 3) │ └──────────────────────────────────────────────────────────────────────┘ ▲ 长期轮询/WS(JSON 命令) │ ┌──────────────────────┴──────── DSH 客户端 (renderer) ─────────────────┐ │ plugin: sidebar-browser-control (Client half) │ │ 侧边栏 tab type "Agent Browser" + body slot │ │ ├─ dshDesktop.browser.acquire → (我们自己的 guest) │ │ ├─ webview.executeJavaScript:DOM/AX 快照、注入光标覆盖层 │ │ ├─ webview.sendInputEvent:真实鼠标/键盘输入(不移动系统指针) │ │ └─ 操作状态 UI:Agent 控制中 / 用户接管 / 停止 │ └──────────────────────────────────────────────────────────────────────┘ ``` 为什么桥使用**独立 loopback 服务**而不是 DSH 内部 RPC: - Host→Client(工具给面板下命令)在 DSH 的公开插件 API 里没有直接的“调用客户端”入口; 内部 Typert Remote/Client 适配器需要代码生成,本地纯 JS 插件用不了(详见 §7 风险)。 - 独立 127.0.0.1 服务 + 随机/固定 token 与 DSH 版本解耦,Host/Client 两边都是我们自己的代码, 失败模式简单;不给 DSH 升级引入私有依赖。代价是需要处理 CORS/PNA 与端口发现(Phase 0 验证)。 ## 4. Goal A 交互与实现要点 工具(首版,模型可一次传 action): | action | 参数 | 说明 | |---|---|---| | `open` | `url` | 在侧边栏打开/复用 Agent Browser 标签并导航 | | `snapshot` | `maxChars?` | 返回可交互元素列表:`ref`、角色/标签、文本、坐标、是否可见;作为模型点击的依据 | | `click` | `ref` 或 `x,y` | 先 `elementFromPoint` 校验,再在元素中心派发真实鼠标事件 | | `type` | `ref`/`text` | 先聚焦元素再逐键输入(或 `input` 事件) | | `key` | `key` | Enter/Tab/Escape/方向键等 | | `scroll` | `deltaY` / `ref` | 滚轮事件 | | `navigate` | `url` | 地址导航 | | `wait` | `ms` / `selector` | 等待加载/选择器 | | `screenshot` | — | 若 webview 可截屏则返回图片,否则暂不提供(Phase 2 决定) | 前端光标(对齐 Codex 的做法,但更克制): - 注入一个 `position:fixed; inset:0; pointer-events:none; z-index:max` 的覆盖层(open shadow root), 内部一个“Agent 光标”元素与点击涟漪;用 CSS transition/transform 移动到目标点,不移动系统指针。 - 覆盖层只在 Agent 操作时出现,`pointer-events:none` 保证用户仍可直接操作页面; 用户一旦直接操作(面板获得焦点/真实输入)即视为“接管”,暂停 Agent 操作并提示。 - 页面 CSP 可能拦截注入 `