# 架构设计 ## 设计目标 1. DSH tool、传输、Chrome API、DOM runtime 相互解耦。 2. 所有可执行能力采用白名单 action,不暴露任意 JavaScript 或任意 CDP 命令。 3. 连接中断、service worker 重启和页面导航都以显式状态表示,不隐式重试副作用动作。 4. 代码按领域拆分;公共 JSON、错误、协议和脱敏逻辑复用,避免单文件承担多个生命周期。 5. 本机安装准备必须通过 DSH 原生一次性审批;Chrome 自己的安装确认不得被绕过。 6. 桌面内置部署的安装引导属于标准 Harness Web Client 插件面,不在桌面壳或 Host 中复制设置 UI。 ## 分层 ```mermaid flowchart TD A["DeepSeek Harness / 主 Agent"] --> W["Browser Worker coordinator"] A --> R["Trusted artifact delivery / retry"] W --> B["Worker-only DSH tools"] A --> I["Chrome setup / approval gate"] I --> J["Chrome locator + verified bundle installer"] B --> C["BrowserController"] C --> K["Opaque artifact store"] C --> L["Verified locator store"] W --> R K --> R L --> W C --> D["Loopback WebSocket bridge"] D --> E["MV3 service worker"] E --> F["Tabs / bounded CDP Screenshot"] E --> G["Frame-aware DOM runtime"] E --> H["Debugger Network runtime"] ``` | 层 | 职责 | 不负责 | |---|---|---| | DSH tools | 参数 schema、工具说明、canonical JSON 输出 | Chrome API 细节 | | Browser Worker | 独立 Session、低层工具编排、确定性 checkpoint、rollover | 向父会话暴露页面数据 | | Parent browser circuit | 通过 Session 公开的不可变历史快照按当前回合累计委派/失败,并返回结构化 blocker | 读取私有 Session 状态、取消主 Agent、限制 DSH 其他工具或跨用户回合永久封锁 | | Artifact delivery | 验证 Worker-owned opaque ID、限定父 session cwd、安全复制与 delivery-only retry | 重新执行浏览器动作、覆盖既有文件或接受页面提供的路径 | | Locator store | 校验扩展返回的选择器 bundle、绑定 Worker 所有权、在 Worker 结束后解析给父 Agent | 接受模型编写的选择器或把 selector text 暴露给 Worker | | BrowserController | action 调度、artifact 后处理 | WebSocket 生命周期 | | WebSocket bridge | 鉴权、会话、超时、取消、请求关联 | 页面定位 | | Service worker | 命令路由、重连、Chrome API 编排 | 长期保存 DOM 节点 | | Content runtime | observe、可见性、快照引用、页面动作 | 网络捕获 | | Network runtime | 按需 CDP attach、环形缓冲、脱敏 | 任意 CDP 透传 | | Chrome setup | 启动检测、状态机、DSH 审批门禁、原子部署、打开扩展页 | 静默修改 Chrome profile 或绕过 Chrome 确认 | | Web settings | `settings.plugins.tab` 页面、同源状态读取、扩展 ZIP 下载和配对引导 | 直接写安装目录或替代 Chrome 确认 | ## Chrome 安装状态机 ```text platform-unsupported / chrome-missing -> install-required -> prepared-not-connected -> connected prepared-not-connected -> update-required -> prepared-not-connected unowned / tampered -> blocked ``` - Chrome 定位只探测标准绝对路径、`PATH` 或显式配置,不执行 shell。 - 安装目录带有 bundle hash receipt;未知目录或被修改的自有目录不自动覆盖。 - 更新先复制到同目录临时区,再通过 rename 交换;失败时回滚,避免留下半包。 - 正式版 Chrome 不使用 `--load-extension`。安装助手只打开 `chrome://extensions`,最终确认属于 Chrome。 - 初始化时没有开放的 Agent turn,不能调用 `ctx.approval`;`agent/session-start` 只注入非唤醒状态说明。真正的安装调用由 `tools/pre-execute` 返回 `ask`,交给 DSH 审批 seam 审计。 ## Harness Web 设置面 同一个 npm 包现在发布两个运行入口: ```text dsh-auto-chrome-tool ├─ . Host:bridge、Browser Worker、Tools、Chrome setup、只读 Web routes └─ ./client Web:Settings > Plugins > Chrome 浏览器 ``` `package.json#dsh.client` 声明 Web 平台及 `ui-renderer`、`ui-settings`、`locale` 依赖。`lib/client.js` 使用 Harness 模块加载器要求的 lazy-CJS closure factory;React 与 JSX runtime 从共享模块表取得,不复制 Cordis 或 React 单例。Client 通过 `settings.plugins.tab` 注册独立页签,通过 Slot inject 只接收 `loadStatus`、`copyText` 和下载 URL,React 组件不持有 Host `ctx`。 Host 在可选的 `ctx.webServer` 存在时注册两个 exact route: - `GET|HEAD /dsh-auto-chrome-tool/status.json`:重新执行 Chrome 定位和扩展准备目录检查,投影 bridge 会话与当前配对参数。 - `GET|HEAD /dsh-auto-chrome-tool/extension.zip`:从当前 npm 包的 `extension/dist` 懒生成带单一顶层目录的 ZIP,供用户解压后执行“加载已解压”。 两条 route 只接受 loopback Host,拒绝 `Sec-Fetch-Site: cross-site`、不匹配的 Origin 和非 `GET|HEAD` 方法,且不发送 CORS 许可。状态响应 `no-store`;下载只包含随包发布的公开 MV3 文件。设置页不调用 `ExtensionBundleInstaller.install()`,因此不会绕过现有 Agent 审批路径;用户从设置页下载与在 Chrome 中确认安装是另一条纯手工路径。 ## 协议 - 协议版本:`2`。 - 扩展第一条消息必须是 `hello`,并携带协议版本、扩展 ID、版本和 256-bit 客户端随机数;共享 token 永不出现在 WebSocket 帧或 URL 中。 - 服务端返回随机 challenge 和 server HMAC proof;扩展验证后再返回 client HMAC proof。proof 绑定协议版本、扩展 ID 与两个随机数,双方都能证明持有同一 token。 - 服务端校验 `Origin: chrome-extension://`;可选 allowlist 再绑定具体扩展 ID。 - 每个 command 使用 UUID `requestId`;result 可乱序返回。 - AbortSignal 会发送 `cancel`;超时、本地停止、断连会清理 pending 请求。 - 默认最大消息 8 MiB、最大 64 个 pending、最多 4 个扩展会话。 ## DOM 引用 模型看到的元素引用是短期 opaque token,不是 CSS selector。扩展内部关联: ```text tabId + frameId + documentId + snapshotId + elementKey ``` 每次 observe/find 生成新快照。导航、文档替换、下一次显式快照、元素脱离 DOM,或目标自身的标签、role、accessible name、type、href/action 等语义指纹变化后,旧引用返回 `STALE_REF`。页面其他区域的时钟、轮询或 class/style 更新不会全局作废仍连接且语义未变的目标。动作前仍重新校验可见、enabled、连接状态和命中点,避免“看见 A、实际点击 B”。 `page.clickNamed` 用于高频更新页面:在目标主 frame 内同步完成语义查找、唯一精确名称校验和点击,不把 ref 带回模型再等待下一步。零命中或多命中均 fail closed。 `page.locator` 只接受当前快照中的 opaque ref。Content runtime 在元素所属 Document/开放 Shadow Root 内生成 CSS/XPath 候选,并用 `querySelectorAll`/XPath snapshot 证明候选只命中同一元素。动态 ID/哈希降级为结构路径;Shadow DOM 返回 host CSS chain 而不伪造 XPath;子 frame selector 明确标为 frame-local。扩展返回的 selector text 先进入 Host `LocatorStore`,Worker 只看到随机 ID,父 Agent 仅能通过当前 Worker 的 structured claim 获得已验证 bundle。 service worker 通过 `chrome.webNavigation.getAllFrames()` 获取标签页内当前真实 frame/document 清单,再以 `documentId` 精确投递 content-script 命令;不依赖只面向扩展运行上下文的 `runtime.getContexts()`。截图不开放任意 CDP 命令,只使用白名单化的 `Page.captureScreenshot`;若网络 runtime 已持有该标签页调试会话则复用,否则截图后立即 detach。 ## 连接生命周期 Chrome 147+ 对 localhost WebSocket 也应用 Local Network Access。首次配对由 popup/options 页面中的用户点击发起,授权成功后写入本地配对状态,再通知 service worker 建立长期连接。后台使用 WebSocket 心跳和 `chrome.alarms` 双重恢复,但不会在用户未配对时主动探测本地网络。 ## Artifact 生成与交付 截图和完整网络正文由 Host 写入受 TTL/总配额约束的内部目录。低层工具结果只包含 `id/kind/mimeType/byteLength/suggestedName`,不包含绝对路径。Browser Worker 在 structured output 中把 opaque ID 绑定到父任务预先声明的 purpose;Host 只接受当前 Worker 实际创建且仍为普通文件的 artifact。 可信交付阶段在 Worker 结束后执行:以父 Session header 的 cwd 为唯一根目录,逐段拒绝 traversal、符号链接和非目录祖先,先复制到同目录临时文件,再通过 hard-link 原子发布且默认不覆盖。浏览器执行与交付分别记录为 `executionStatus`、`deliveryStatus`;必需交付失败时总体为 `partial`。已验证 artifact 只授权给原始 top-level Session,后续 `browser_artifact_deliver` 可重试文件交付而不启动 Worker。 ## 可扩展点 - `BridgeTransport` 边界可加入 Native Messaging 实现。 - action union 与 command router 一一对应;增加能力需同时增加协议、tool、router 和测试。 - artifact store 可替换为加密、短期或外部存储实现。 - locator store 可加入跨导航失效检测或经验证的 iframe selector chain;当前不把 Chrome frameId 当作 Selenium selector。 - network policy 可按域名、MIME、大小和保留时长细化。 - Browser Worker 的上下文防火墙、工具隔离与 rollover 设计见 [PAGE_ANALYST_DESIGN.md](PAGE_ANALYST_DESIGN.md)。