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

16 KiB
Raw Permalink Blame History

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 中通过 handshake 拿到 session_key,并用它构造 BrowserPipeTool
  • ws service 模式在 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 中:

  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 中:

  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 能力

代码中已经存在:

WsBrowserBackend 本身不依赖 pipe session key而是

  • 使用 WsClient 发送/接收文本帧
  • 使用 MacPolicy 做动作级校验
  • 通过 encode_v1_action(...)decode_callback_frame(...) 处理 ws 协议

这正是 ws service 模式应该使用的模型。

关键集成缝隙

当前共享 runner 的真实缝隙已经确认:

  • src/agent/task_runner.rsrun_submit_task(...) 仍直接要求 &BrowserPipeTool<T>
  • src/compat/runtime.rssrc/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 连接

只替换认证错误的执行路径,不顺手做生命周期优化。

目标架构

目标调用链

sg_claw_client
  -> sg_claw service
    -> ws-native browser backend
      -> browser_ws_url
        -> sgBrowser

与 pipe 的并行关系

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_clientsg_clawSubmitTask
  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. ServiceBrowserWsClientWsBrowserBackend 的组合可以:
    • 发送 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 回归

cargo test --test pipe_handshake_test -- --nocapture

如实现涉及 browser tool 上层接线,还需补跑:

cargo test --test browser_tool_test --test compat_browser_tool_test --test runtime_task_flow_test -- --nocapture

ws 回归

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