架构说明
August 26, 2026 · View on GitHub
三层结构
agent (browser_* 工具)
→ ctx.browser (seam, dsh-browser-plus/browser)
→ dsh-browser-plus/browser-electron (provider)
→ ElectronBrowserViewHost (由宿主外壳提供)
→ WebContentsView + webContents.debugger (CDP)
seam 层(src/browser/)
BrowserRuntime 以 Cordis Service 形式注册为 ctx.browser:
- provider 注册:
registerBrowserProvider(provider)登记一个实现BrowserProvider接口的 provider,重名抛BROWSER_DUPLICATE_PROVIDER;disposer 挂在注册方自身的 fiber 上,插件重载时正确清理。 - provider 选择(执行期解析,不依赖顺序):
- 配置了 id 且注册且可用 → 该 provider
- 配置了 id 但未注册 →
BROWSER_PROVIDER_CONFIGURED_MISSING - 配置了 id 但不可用 →
BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE - 未配置、恰一个可用 → 自动选择
- 未配置、多个可用 →
BROWSER_PROVIDER_AMBIGUOUS - 未配置、无可用 →
BROWSER_PROVIDER_UNAVAILABLE
- 所有请求/结果类型(
BrowserProvider接口、BrowserError错误码)定义在src/browser/types.ts。
provider 层(src/browser-electron/)
ElectronBrowserProvider 通过 ElectronBrowserViewHost 接缝操作视图,与 Electron 解耦:
- 会话 = 有序标签列表 + 历史;每次
open()新建会话(工具层按任务缓存复用); - 每个标签对应一个视图(handle);
showActive让宿主把活动标签的视图置顶; - 页面驱动全部走 CDP:
Page.navigate/ history navigation /Page.reload/Page.stopLoading/Runtime.evaluate/Input.dispatchMouseEvent/Input.insertText/Page.captureScreenshot(兜底); - 每个 tab 保留最近 10 个短生命周期快照引用;
browser_click_ref与browser_scroll_into_view用内部 CSS 路径、元素指纹、URL 和文档代次验证目标,变化后明确要求重新快照; - 人类工具栏是页面注入 chrome:通过
Page.addScriptToEvaluateOnNewDocument在顶层文档挂载 closed Shadow DOM,不创建第二个WebContentsView; - 可见性不重挂:
showView只切换setVisible,导航、加载、标题和 resize 路径不得执行removeChildView/addChildView; - 截图优先走宿主原生
capturePage(新增capture通道):CDPcaptureScreenshot在窗口存在多个(隐藏)视图时会挂起,原生捕获对可见视图快速可靠,失败时自动回退 CDP(临时摘除其他视图保证单视图状态); - 所有 CDP 调用都有超时兜底(
withTimeout),避免卡死工具调用; - 历史记录单调递增的 seq,截断(500 条)后不回绕;失败导航只记一条。
工具层(src/tool-browser/)
35 个 browser_* 工具,按调用方任务(exec.agent.id)维护独立浏览器会话:
- 会话缓存
sessionsByTask:同一任务复用同一会话,并发首开去重; - 变更型调用经过每任务 FIFO 操作通道;相同 in-flight snapshot/content/无落盘截图会合并,避免重复 CDP 与渲染工作;
browser_tasks与browser_handoff暴露运行、等待用户、用户接管、失败和空闲状态;用户接管后新的变更型 Agent 调用会等待交还;browser_reset_session关闭本任务会话并遗忘映射(即使 close 抛错也清除,下次调用重建);browser_restrict维护模块级白名单,守卫所有非只读工具;- 输出 schema 与返回值严格一致(DSH 运行时会校验,
additionalProperties: false下多一个字段都会报错)。
自托管实现(纯 dsh web)
没有桌面外壳时,RemoteElectronViewHost 接管:
父进程(DSH) 子进程(Electron main)
RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
resolveElectronPath() BrowserWindow('dsh-browser-plus')
ElectronChildClient WebContentsView × N
DeferredRemoteView(物化缓存) webContents.debugger(CDP)
- 协议:本机 loopback TCP,每行一个 JSON(
{ id, op, ... }↔{ id, ok, result|err }); - Electron 定位:优先 package-local 的精确
42.9.3optional dependency;其次只接受经 package metadata 验证为42.9.3的ELECTRON_PATH、DSH 锚点或 pnpm store 候选;找不到即失败,绝不回退到 43.x。 - 稳健性:子进程/套接字都有
error监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启;物化失败可重试;下载有 256MB 上限与 60s 超时;cookie 导出/恢复有 30s 超时; - 视图可见性:所有任务键(DSH 会话)共用一个
BrowserWindow,每个任务有隔离视图;页面任务管理器选择可见任务,showView对后台任务只更新其活动视图,不改变用户当前选择。切换仅用setVisible,绝不 remove/re-add(capture 的 CDP 兜底仍只临时 detach/restore 同窗口兄弟视图); - 任务状态传递:页面首次挂载、导航重装 chrome 或任务切换时接收完整 bootstrap;常规状态、任务卡、面板和轨迹变化使用带 epoch/revision 的增量 patch。摘要中的 URL 只保留 origin,避免泄露完整路径与查询参数;
- 任务缩略图:缩略图使用原生
capturePage生成 JPEG data URL,最长边限制为 288px、质量 58、上限 180 KiB。仅在任务面板打开时为可见任务按需捕获,单飞、最短 2 秒间隔、32 项缓存;后台任务保留最后成功图像。 - 孤儿防护:父进程断开时子进程自动退出,不留僵尸窗口;
- cookie 落盘:子进程使用独立 userData 目录(
<DSH_HOME>/dsh-browser-plus-host),登录态跨重启保留(另有browser_auth手动导出/恢复)。
关键设计决策
| 决策 | 原因 |
|---|---|
| 任务级会话隔离,共享 cookie | 并行任务不抢页面;登录一次到处可用 |
| 原生 capturePage 优先,CDP 兜底 | CDP 截图多视图挂起;原生捕获窗口未激活时失败——两通道互补 |
| 页面注入 chrome | 保留单视图合成树,避免第二个 WebContentsView 引发的人眼白屏 |
| 一个共享窗口 + 页面任务管理器 | 任务视图、标签与历史隔离;browser_space 命名浏览器任务,后台更新不抢可见页面 |
| 固定 Electron 42.9.3 | 43.4.1 合成器故障会导致截图/白屏;找不到 pin 时明确失败 |
| 独立 userData | 多实例争用默认目录导致 GPU 缓存/会话锁冲突 |
| withTimeout 全覆盖 | 卡死的 CDP 调用必须能被工具超时兜底 |
| 输出 schema 严格匹配 | DSH 运行时会校验返回值,多字段即报错 |