Files
claw/docs/superpowers/specs/2026-04-02-ws-browser-backend-auth-design.md
木炎 bdf8e12246 feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:44:53 +08:00

507 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WS 浏览器后端认证替换设计
## 背景
当前 `sg_claw` 的 websocket service 路径已经能接收 `sg_claw_client` 请求、复用共享 task runner、连接真实浏览器 websocket 地址 `browser_ws_url`,并进入真实 skill 执行链路。但真实联调时,所有浏览器相关调用都会失败并返回:
- `invalid hmac seed: session key must not be empty`
根因已经定位:
- pipe 模式在 [src/lib.rs](src/lib.rs) 中通过 handshake 拿到 `session_key`,并用它构造 `BrowserPipeTool`
- ws service 模式在 [src/service/server.rs](src/service/server.rs) 中仍然构造 `BrowserPipeTool::new(..., vec![])`
- `BrowserPipeTool` 的认证模型要求非空 session key因此 ws service 路径虽然使用的是浏览器 websocket 协议,仍错误地依赖了 pipe 特有的 HMAC/session-key 语义
这会导致:
1. `sg_claw_client -> sg_claw` 连接正常
2. skill 加载与模型调用正常
3. 真实浏览器动作开始执行
4. 但所有 browser tool 调用在认证层统一失败
## 目标
**仅限 ws 模式改动** 的前提下,让 `sg_claw` service 路径改为使用 **ws-native browser backend**,不再依赖 `BrowserPipeTool` 的 pipe session-key 认证模型,从而让真实浏览器联调可用。
## 约束
必须满足:
- 只改 ws 模式相关实现
- 不破坏 legacy pipe 模式
- 不修改 pipe handshake 语义
- 不修改 `src/lib.rs` 的 pipe 主入口行为
- 不引入临时绕过认证或 fake seed
- 不扩大到多客户端、多任务、队列、守护进程管理
## 非目标
本次不做:
- 自动拉起 sgBrowser
- 浏览器进程管理
- 多浏览器实例支持
- service/client UX 优化
- browser ws 协议扩展
- pipe 模式重构
- 统一重构所有 runtime 层去完全依赖 `BrowserBackend`
## 现状分析
### 正常 pipe 路径
pipe 模式当前在 [src/lib.rs](src/lib.rs) 中:
1. 通过 `perform_handshake(...)` 读取浏览器侧初始化消息
2. 从 handshake 中拿到 `session_key`
3.`BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)` 构造浏览器工具
4. 后续 browser action 使用 pipe/HMAC 语义
该路径已经可用,本次不能动。
### 当前 ws service 路径
当前 ws 模式在 [src/service/server.rs](src/service/server.rs) 中:
1. `sg_claw_client` 将任务发给 `sg_claw` service
2. service 构造 `ServiceBrowserTransport`
3. service 用 `BrowserPipeTool::new(transport.clone(), mac_policy.clone(), vec![])`
4. browser action 经 `ServiceBrowserTransport` 编码为 browser websocket 请求并发给 `browser_ws_url`
问题在于第 3 步:
- service 走的是 browser websocket 协议
- 但却仍使用 `BrowserPipeTool`
- `BrowserPipeTool` 内部仍坚持要求 pipe session key
- 因此真实 ws 联调时直接失败
### 现有 ws-native 能力
代码中已经存在:
- [src/browser/ws_protocol.rs](src/browser/ws_protocol.rs):固定 browser websocket 协议 codec
- [src/browser/ws_backend.rs](src/browser/ws_backend.rs)`WsBrowserBackend`
- [src/browser/mod.rs](src/browser/mod.rs):已导出 `WsBrowserBackend`
`WsBrowserBackend` 本身不依赖 pipe session key而是
- 使用 `WsClient` 发送/接收文本帧
- 使用 `MacPolicy` 做动作级校验
- 通过 `encode_v1_action(...)``decode_callback_frame(...)` 处理 ws 协议
这正是 ws service 模式应该使用的模型。
## 关键集成缝隙
当前共享 runner 的真实缝隙已经确认:
- [src/agent/task_runner.rs](src/agent/task_runner.rs) 的 `run_submit_task(...)` 仍直接要求 `&BrowserPipeTool<T>`
- [src/compat/runtime.rs](src/compat/runtime.rs) 与 [src/compat/orchestration.rs](src/compat/orchestration.rs) 也继续以 `BrowserPipeTool<T>` 作为主浏览器调用对象
- 同时 compat runtime 内部已经存在 `Arc<dyn BrowserBackend>` 的工具适配层,只是它目前是从 `PipeBrowserBackend::from_inner(browser_tool)` 包出来的
这意味着本次实现不能只在 `src/service/server.rs` 里替换构造逻辑,而必须在 **ws 专用调用面** 增加一个最小适配缝隙,让 service 模式能把 `WsBrowserBackend` 传入 compat/runtime/orchestration而 pipe 继续保持 `BrowserPipeTool` 原样。
允许的最小缝隙定义如下:
1. `run_submit_task(...)` 的 pipe 版本保持不动,供 pipe 入口继续使用
2. 新增一个 **仅供 ws service 使用** 的并行入口,例如:
- `run_submit_task_with_browser_backend(...)`
- 或 service 侧调用的等价 ws-only adapter
3. ws-only 入口内部允许把浏览器依赖类型降到 `Arc<dyn BrowserBackend>`
4. `src/lib.rs`、pipe handshake、pipe `BrowserPipeTool` 构造逻辑不允许改行为
## 设计决策
### 决策 1ws service 路径弃用 `BrowserPipeTool`
在 ws service 路径中,不再构造 `BrowserPipeTool`
替代方案:
- service 侧提供一个 `WsClient` 实现
- 直接构造 `WsBrowserBackend`
- 让 ws service 的 browser action 通过 `WsBrowserBackend` 执行
### 决策 2pipe 路径保持原样
pipe 模式继续:
- handshake
- `session_key`
- `BrowserPipeTool`
不做语义调整,不引入兼容层,不改动已存在的验证路径。
### 决策 3runner 只在 ws 调用面做最小接线
当前共享 task runner 复用已经存在,本次不做大重构。
策略是:
- 只在 ws service 用到的调用面,改成可使用 `WsBrowserBackend`
- 如果必须扩共享调用接口,则仅做**最小、兼容、对 pipe 零影响**的改动
- 任何涉及 pipe 行为变更的改动都不允许
### 决策 4保留现有 browser websocket 连接生命周期
本次不重做连接管理架构。
继续维持:
- 单客户端
- 单任务串行
- 按现有 service 生命周期维护 browser websocket 连接
只替换认证错误的执行路径,不顺手做生命周期优化。
## 目标架构
### 目标调用链
```text
sg_claw_client
-> sg_claw service
-> ws-native browser backend
-> browser_ws_url
-> sgBrowser
```
### 与 pipe 的并行关系
```text
pipe mode:
browser process <-> stdio/pipe <-> sgclaw::run() <-> BrowserPipeTool
ws mode:
sg_claw_client <-> sg_claw service <-> WsBrowserBackend <-> sgBrowser websocket
```
两条路径并行存在,互不混用认证模型。
## 模块设计
### 1. `src/service/server.rs`
这是本次核心改动文件。
#### 当前职责
- 管理 service client websocket 收发
- 将 service 请求转入共享 runner
- 维护 service->browser 的 websocket 传输桥
#### 本次改动
- 将“service->browser 的桥”从 `Transport + BrowserPipeTool` 组合改为 `WsClient + WsBrowserBackend`
- 删除 ws service 路径中对空 `session_key` 的依赖
- 继续保留 service socket 生命周期与 session 状态机
#### 目标结构
可接受的目标形态:
- `ServiceBrowserWsClient`:实现 `WsClient`
- 内部继续维护真实 browser websocket 连接
- `serve_client(...)` 在处理任务时构造 `WsBrowserBackend`
- 共享 runner 或其 ws 调用包装层通过该 backend 执行 browser action
### 2. 共享 runner / ws 调用包装层
本次不要求把全项目统一改成 `BrowserBackend`
但 ws service 模式必须能把 browser action 接到 `WsBrowserBackend`
可接受的最小方案:
- 在 ws service 使用的一层引入一个只服务 ws 模式的 adapter
- 该 adapter 把 runner 所需的 browser 调用能力委托给 `WsBrowserBackend`
要求:
- pipe 现有调用签名不变,或即使扩展也必须保证 pipe 行为完全一致
- 不允许为了 ws 把 pipe 入口重写
### 3. `src/browser/ws_backend.rs`
原则上复用现有实现。
只有在以下情况下才允许最小补改:
- service 真实联调发现它缺一个 ws service 必需但当前未暴露的能力
- 该补改只服务 ws-native 路径
- 不影响现有测试语义
## 连接职责与边界
为避免 service 侧与 `WsBrowserBackend` 重复实现责任,本次显式约束如下:
### `WsBrowserBackend` 负责
- 单次 `invoke(...)` 的请求串行化
- 调用 `encode_v1_action(...)`
- 发送 websocket 文本帧
- 等待即时状态帧
- 如有 callback等待 callback 帧并做名称匹配
- 将结果统一为 `CommandOutput`
- 按现有 `WsBrowserBackend` 语义产出 timeout / protocol 错误
### service 侧 `WsClient` 适配器负责
- 持有真实 browser websocket 连接
- 在第一次请求时建立到 `browser_ws_url` 的连接
-`send_text(...)` / `recv_text_timeout(...)` 委托到真实 websocket
- 将底层关闭、reset、timeout 统一映射为既有 `PipeError` 语义
- 不实现 request/response correlation不解析 browser ws 协议 payload
### 明确不允许
- service 侧继续手写 callback 轮询逻辑
- service 侧继续直接调用 `encode_v1_action(...)` 组包作为主路径
- 在 service 侧复制 `WsBrowserBackend` 的协议处理逻辑
这样可以保证:
- `src/service/server.rs` 只负责“连线”
- `src/browser/ws_backend.rs` 继续负责“ws 浏览器调用语义”
## 数据流设计
### 成功路径
1. `sg_claw_client``sg_claw``SubmitTask`
2. service 收到任务并进入共享 runner
3. 当 runner 需要浏览器动作时:
- ws service 调用 `WsBrowserBackend.invoke(...)`
4. `WsBrowserBackend`
-`MacPolicy` 校验动作
-`encode_v1_action(...)` 编码请求
- 发往 `browser_ws_url`
- 等待状态帧
- 如有 callback继续等 callback 帧
5. 结果返回到 runner
6. runner 继续执行并向 client 流式输出日志和 completion
### 失败路径
#### browser websocket 不可连
- 返回明确的 browser websocket connect 错误
- 不冒充认证错误
#### 浏览器返回非 0 状态
- 返回明确协议错误:`browser returned non-zero status`
#### callback 超时
- 返回 timeout
#### websocket 断开
- 返回 `PipeError::PipeClosed`
- 由 service 生命周期逻辑处理
#### 不再允许的错误
- `invalid hmac seed: session key must not be empty`
该错误在 ws 模式下应彻底消失。
## 失败语义
为便于测试与实现ws-only 路径的 outward error 语义固定如下:
### browser websocket connect 失败
- outward: `PipeError::Protocol("browser websocket connect failed: ...")`
### 浏览器返回非 0 状态码
- outward: `PipeError::Protocol("browser returned non-zero status: ...")`
### callback 超时
- outward: `PipeError::Timeout`
- timeout 来源:沿用 `WsBrowserBackend` / ws service 当前 response timeout 配置,默认 30 秒
### websocket 被对端正常关闭或 reset
- outward: `PipeError::PipeClosed`
- 不允许使用“等价错误”这类不精确表述
### 本次必须消除的错误
- `invalid hmac seed: session key must not be empty`
任何 ws service 联调路径再出现该错误,都视为实现未完成。
## 测试设计
### 分层测试策略
为避免依赖 LLM/planner 的非确定性行为,本次测试必须分成两层,且各自断言不同目标:
#### A. backend / adapter 层测试(确定性)
这一层不经过 `sg_claw_client`、不经过真实模型规划,直接验证 ws-only 技术行为。
目标:
1. `ServiceBrowserWsClient``WsBrowserBackend` 的组合可以:
- 发送 `Navigate`
- 接收 `0` 状态
- 在 callback 场景下读取 callback 文本
2. 当 fake browser server 主动关闭/reset 时:
-`WsClient` / `WsBrowserBackend.invoke(...)` 观察层断言 outward error 必须是 `PipeError::PipeClosed`
3. 当 fake browser server 不返回 callback 时:
-`WsBrowserBackend.invoke(...)` 观察层断言 outward error 必须是 `PipeError::Timeout`
4. 该层测试完全不依赖 LLM、planner、skills 路由
建议:
- 新增 focused ws service/backend test
- 输入动作固定为代码直接调用 `invoke(Action::Navigate, ...)` 等,而不是自然语言任务
#### B. client -> service 集成测试(链路验证)
这一层验证 ws-only 接线已经替换掉空 session key 路径,但不承担细粒度协议语义断言。
目标:
1. 通过真实 `sg_claw_client -> sg_claw service` 发起一个最小自然语言任务
2. fake browser websocket server 至少收到一个来自 ws-only 路径的文本帧
3. client/service 输出中不再出现:
- `invalid hmac seed: session key must not be empty`
4. 该层只证明:
- ws service 已不再走空 session key 的 pipe 认证路径
- 真实端到端链路已能到达 browser websocket
该层不用于断言精确 enum 身份,也不用于覆盖 callback timeout / reset 细节。
### 新增红测 1ws-only backend/adapter 基本调用可用
目标:
- 不走自然语言任务
- 直接构造 ws service 使用的 `WsClient` + `WsBrowserBackend`
- 调用固定动作:`Action::Navigate`,目标 url 固定为 `https://www.zhihu.com/hot`
- fake browser websocket server 返回 `0`
- 断言:
- `invoke(...)` 成功
- fake server 收到的首个文本帧可按 `ws_protocol` 语义解释为 `Navigate`
### 新增红测 2ws-only backend/adapter 断链语义固定
目标:
- 不走自然语言任务
- fake browser websocket server 在接受请求后主动关闭或 reset
-`invoke(...)` 观察层断言:
- outward error 固定为 `PipeError::PipeClosed`
### 新增红测 3ws-only backend/adapter callback timeout 语义固定
目标:
- 不走自然语言任务
- fake browser websocket server 返回 `0` 但不返回 callback 帧
-`invoke(...)` 观察层断言:
- outward error 固定为 `PipeError::Timeout`
### 新增红测 4client->service 链路不再触发空 session key 错误
目标:
- 通过真实 `sg_claw_client -> sg_claw service` 链路触发浏览器动作
- 用 fake browser websocket 服务端接住请求
- 任务输入固定为:`打开知乎热榜并读取页面主区域文本`
- 断言 client/service 输出中不再出现:
- `invalid hmac seed: session key must not be empty`
- 断言 fake browser server 至少收到了一个文本帧
### 回归测试
必须重新运行并保持通过:
#### pipe 回归
```bash
cargo test --test pipe_handshake_test -- --nocapture
```
如实现涉及 browser tool 上层接线,还需补跑:
```bash
cargo test --test browser_tool_test --test compat_browser_tool_test --test runtime_task_flow_test -- --nocapture
```
#### ws 回归
```bash
cargo test --test service_ws_session_test --test service_task_flow_test --test browser_ws_protocol_test --test browser_ws_backend_test -- --nocapture
```
## 手工验收
使用真实配置和真实已启动 sgBrowser
1. 启动 sgBrowser并确保 `browserWsUrl` 可用
2. 启动 `sg_claw`
3. 运行:
- `sg_claw_client`
4. 发送知乎最小任务:
- 打开知乎热榜并读取页面主区域文本
5. 观察:
- 不再出现 `invalid hmac seed`
- 出现真实 browser action 日志
- 能返回单次 completion
6. 再运行旧知乎 skill
- `读取知乎热榜数据,并导出 excel 文件`
7. 验证旧知乎 skill 进入真实 browser 执行路径
8. 最后确认 legacy pipe 入口仍可启动(仅验证,不允许为此修改 pipe 实现)
## 风险
### 风险 1ws service 与共享 runner 接口耦合过深
控制:
- 只在 ws 使用面做 adapter
- 不对 pipe 主入口做结构性改造
### 风险 2为适配 ws-native backend 误改 pipe 调用链
控制:
- 所有 pipe 回归必须在每轮修改后重跑
- `src/lib.rs` 不允许改行为
### 风险 3ws service 内联连接逻辑与 `WsBrowserBackend` 责任重复
控制:
- 本次先以最小变更消除认证阻塞
- 不顺手做大规模整理
## 通过标准
满足以下全部条件才算完成:
1. ws service 路径不再依赖空 session key
2. 不再出现 `invalid hmac seed: session key must not be empty`
3. 真实 browser websocket 请求能发到 sgBrowser/fake browser server
4. 旧知乎 skill 至少能进入真实 browser action 执行链路
5. pipe 模式零回归
6. 所有新增/相关测试通过
## 实施建议
按以下顺序实施:
1. 先补红测锁定“ws 不再触发 invalid hmac seed”
2. 再把 ws service 路径切到 `WsBrowserBackend`
3. 跑 ws 测试
4. 跑 pipe 回归
5. 做真实知乎最小任务 smoke
6. 再做旧知乎 skill smoke