架构说明

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_refbrowser_scroll_into_view 用内部 CSS 路径、元素指纹、URL 和文档代次验证目标,变化后明确要求重新快照;
  • 人类工具栏是页面注入 chrome:通过 Page.addScriptToEvaluateOnNewDocument 在顶层文档挂载 closed Shadow DOM,不创建第二个 WebContentsView;
  • 可见性不重挂: showView 只切换 setVisible,导航、加载、标题和 resize 路径不得执行 removeChildView / addChildView;
  • 截图优先走宿主原生 capturePage(新增 capture 通道):CDP captureScreenshot 在窗口存在多个(隐藏)视图时会挂起,原生捕获对可见视图快速可靠,失败时自动回退 CDP(临时摘除其他视图保证单视图状态);
  • 所有 CDP 调用都有超时兜底(withTimeout),避免卡死工具调用;
  • 历史记录单调递增的 seq,截断(500 条)后不回绕;失败导航只记一条。

工具层(src/tool-browser/)

35 个 browser_* 工具,按调用方任务(exec.agent.id)维护独立浏览器会话:

  • 会话缓存 sessionsByTask:同一任务复用同一会话,并发首开去重;
  • 变更型调用经过每任务 FIFO 操作通道;相同 in-flight snapshot/content/无落盘截图会合并,避免重复 CDP 与渲染工作;
  • browser_tasksbrowser_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.3 optional dependency;其次只接受经 package metadata 验证为 42.9.3ELECTRON_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.343.4.1 合成器故障会导致截图/白屏;找不到 pin 时明确失败
独立 userData多实例争用默认目录导致 GPU 缓存/会话锁冲突
withTimeout 全覆盖卡死的 CDP 调用必须能被工具超时兜底
输出 schema 严格匹配DSH 运行时会校验返回值,多字段即报错