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>
16 KiB
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 语义
这会导致:
sg_claw_client -> sg_claw连接正常- skill 加载与模型调用正常
- 真实浏览器动作开始执行
- 但所有 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 中:
- 通过
perform_handshake(...)读取浏览器侧初始化消息 - 从 handshake 中拿到
session_key - 用
BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)构造浏览器工具 - 后续 browser action 使用 pipe/HMAC 语义
该路径已经可用,本次不能动。
当前 ws service 路径
当前 ws 模式在 src/service/server.rs 中:
sg_claw_client将任务发给sg_clawservice- service 构造
ServiceBrowserTransport - service 用
BrowserPipeTool::new(transport.clone(), mac_policy.clone(), vec![]) - 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:固定 browser websocket 协议 codec
- src/browser/ws_backend.rs:
WsBrowserBackend - 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 的
run_submit_task(...)仍直接要求&BrowserPipeTool<T> - src/compat/runtime.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 原样。
允许的最小缝隙定义如下:
run_submit_task(...)的 pipe 版本保持不动,供 pipe 入口继续使用- 新增一个 仅供 ws service 使用 的并行入口,例如:
run_submit_task_with_browser_backend(...)- 或 service 侧调用的等价 ws-only adapter
- ws-only 入口内部允许把浏览器依赖类型降到
Arc<dyn BrowserBackend> src/lib.rs、pipe handshake、pipeBrowserPipeTool构造逻辑不允许改行为
设计决策
决策 1:ws service 路径弃用 BrowserPipeTool
在 ws service 路径中,不再构造 BrowserPipeTool。
替代方案:
- service 侧提供一个
WsClient实现 - 直接构造
WsBrowserBackend - 让 ws service 的 browser action 通过
WsBrowserBackend执行
决策 2:pipe 路径保持原样
pipe 模式继续:
- handshake
session_keyBrowserPipeTool
不做语义调整,不引入兼容层,不改动已存在的验证路径。
决策 3:runner 只在 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 浏览器调用语义”
数据流设计
成功路径
sg_claw_client向sg_claw发SubmitTask- service 收到任务并进入共享 runner
- 当 runner 需要浏览器动作时:
- ws service 调用
WsBrowserBackend.invoke(...)
- ws service 调用
WsBrowserBackend:- 用
MacPolicy校验动作 - 用
encode_v1_action(...)编码请求 - 发往
browser_ws_url - 等待状态帧
- 如有 callback,继续等 callback 帧
- 用
- 结果返回到 runner
- 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 技术行为。
目标:
ServiceBrowserWsClient与WsBrowserBackend的组合可以:- 发送
Navigate - 接收
0状态 - 在 callback 场景下读取 callback 文本
- 发送
- 当 fake browser server 主动关闭/reset 时:
- 在
WsClient/WsBrowserBackend.invoke(...)观察层断言 outward error 必须是PipeError::PipeClosed
- 在
- 当 fake browser server 不返回 callback 时:
- 在
WsBrowserBackend.invoke(...)观察层断言 outward error 必须是PipeError::Timeout
- 在
- 该层测试完全不依赖 LLM、planner、skills 路由
建议:
- 新增 focused ws service/backend test
- 输入动作固定为代码直接调用
invoke(Action::Navigate, ...)等,而不是自然语言任务
B. client -> service 集成测试(链路验证)
这一层验证 ws-only 接线已经替换掉空 session key 路径,但不承担细粒度协议语义断言。
目标:
- 通过真实
sg_claw_client -> sg_claw service发起一个最小自然语言任务 - fake browser websocket server 至少收到一个来自 ws-only 路径的文本帧
- client/service 输出中不再出现:
invalid hmac seed: session key must not be empty
- 该层只证明:
- ws service 已不再走空 session key 的 pipe 认证路径
- 真实端到端链路已能到达 browser websocket
该层不用于断言精确 enum 身份,也不用于覆盖 callback timeout / reset 细节。
新增红测 1:ws-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
新增红测 2:ws-only backend/adapter 断链语义固定
目标:
- 不走自然语言任务
- fake browser websocket server 在接受请求后主动关闭或 reset
- 在
invoke(...)观察层断言:- outward error 固定为
PipeError::PipeClosed
- outward error 固定为
新增红测 3:ws-only backend/adapter callback timeout 语义固定
目标:
- 不走自然语言任务
- fake browser websocket server 返回
0但不返回 callback 帧 - 在
invoke(...)观察层断言:- outward error 固定为
PipeError::Timeout
- outward error 固定为
新增红测 4:client->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:
- 启动 sgBrowser,并确保
browserWsUrl可用 - 启动
sg_claw - 运行:
sg_claw_client
- 发送知乎最小任务:
- 打开知乎热榜并读取页面主区域文本
- 观察:
- 不再出现
invalid hmac seed - 出现真实 browser action 日志
- 能返回单次 completion
- 不再出现
- 再运行旧知乎 skill:
读取知乎热榜数据,并导出 excel 文件
- 验证旧知乎 skill 进入真实 browser 执行路径
- 最后确认 legacy pipe 入口仍可启动(仅验证,不允许为此修改 pipe 实现)
风险
风险 1:ws service 与共享 runner 接口耦合过深
控制:
- 只在 ws 使用面做 adapter
- 不对 pipe 主入口做结构性改造
风险 2:为适配 ws-native backend 误改 pipe 调用链
控制:
- 所有 pipe 回归必须在每轮修改后重跑
src/lib.rs不允许改行为
风险 3:ws service 内联连接逻辑与 WsBrowserBackend 责任重复
控制:
- 本次先以最小变更消除认证阻塞
- 不顺手做大规模整理
通过标准
满足以下全部条件才算完成:
- ws service 路径不再依赖空 session key
- 不再出现
invalid hmac seed: session key must not be empty - 真实 browser websocket 请求能发到 sgBrowser/fake browser server
- 旧知乎 skill 至少能进入真实 browser action 执行链路
- pipe 模式零回归
- 所有新增/相关测试通过
实施建议
按以下顺序实施:
- 先补红测,锁定“ws 不再触发 invalid hmac seed”
- 再把 ws service 路径切到
WsBrowserBackend - 跑 ws 测试
- 跑 pipe 回归
- 做真实知乎最小任务 smoke
- 再做旧知乎 skill smoke