Skip to content

feat(webagent): WebSocket 桥接器,浏览器前端操作 agent 子进程 - #81

Merged
lloydzhou merged 10 commits into
mainfrom
feat/webagent
Aug 15, 2026
Merged

feat(webagent): WebSocket 桥接器,浏览器前端操作 agent 子进程#81
lloydzhou merged 10 commits into
mainfrom
feat/webagent

Conversation

@lloydzhou

@lloydzhou lloydzhou commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • 新增 webagent:独立的 WebSocket 桥接进程,作为 bash-agent 的浏览器操作前端
  • 每条浏览器输入 spawn 一个 agent 子进程(rustagent/goagent/cagent),stdout stream-json 逐行原样转发给所有 WS 客户端,stderr 套一层事件外壳
  • 参数原样透传(--session/--continue 由用户控制会话),网关只追加 --output-format stream-json
  • 客户端连接时从 events.jsonl 回放最近 500 条(--continue 旧会话也能看到历史)
  • 前端按 DeepSeek Harness 对话展示样式重写(单文件零依赖,不引入前端框架):设计令牌双主题(深/浅色)、用户气泡 + 助手 Markdown、思考/工具折叠行(运行态扫光、IO 卡片输入输出分节)、turnStatus shimmer、StatsLine 统计行(缓存命中率)、Hero 空态、toBottom、composer 圆角输入卡;保留 HUD 累计、ANSI 剥离、重连 replay 去重
  • Makefile 新增 build-webagent / build-webagent-android(aarch64 NDK 交叉编译)

Architecture

  • 同步 tungstenite + 手动 HTTP/WS 握手 + 线程每连接(复用已验证的 serve.rs 方案)
  • 每输入 spawn 子进程模型,完全避免 TTY/PTY 问题
  • --bind/--port 控制监听地址(默认 127.0.0.1:8686)
  • 前端单文件 index.htmlinclude_str! 内嵌进二进制,零构建链、零外部资源(PWA manifest/icon 同样内嵌)

Test plan

  • 端到端对话(单轮 + 多轮会话延续)
  • 工具调用流转(tool_call → tool_result → text → stop)
  • --continue 恢复旧会话 + events.jsonl 回放
  • 页面刷新重连不重复堆积、replay 后 working 状态正确清零
  • replay 密集事件下消息不丢失(同步 flush 修复)
  • playwright + headless chromium:mock agent 覆盖 13 种事件类型,46 项 DOM 断言(含双主题令牌色值、chevron 三态几何测量、真实 WS 链路 E2E)
  • clippy 零警告,Android ARM64 交叉编译通过

每条浏览器输入 spawn 一个 agent 子进程(rustagent/goagent/cagent),
stdout stream-json 逐行原样转发给所有 WS 客户端,stderr 套一层事件外壳。
参数原样透传(--session/--continue 由用户控制会话),网关只追加
--output-format stream-json。

客户端连接时从 events.jsonl 回放最近 500 条(--continue 旧会话也能看到
历史),stop 事件清 working 状态。前端含 HUD 状态条、tool 折叠、
ANSI 剥离。

Makefile 新增 build-webagent / build-webagent-android(aarch64 NDK)。
- webagent matrix job mirrors rustagent (linux amd64/arm64 in Debian 11
  container, macOS arm64/amd64)
- deb-package needs webagent; build-deb.sh adds webagent-linux-* to
  required artifacts and installs webagent to /usr/bin
- release: webagent-* in checksums + asset list
- GLIBC 2.31 baseline check covers webagent-linux-*
- install verification includes webagent
- /manifest.json 端点(standalone display,深色主题)
- /icon.svg 端点,复用项目 logo(docs/assets/logo.svg)
- HTML head 加 manifest link、apple-mobile-web-app meta、theme-color
- 手机浏览器可添加到主屏幕直接进入,无地址栏全屏体验
…andalone)

dvh/vh 在 PWA standalone 和浏览器内行为不一致,standalone 模式下
OS nav bar 出现时内容溢出、input-bar 被遮挡。

改用 JS 读取 window.innerHeight 驱动 CSS --app-h 变量:
- 浏览器内:innerHeight 去掉地址栏/tab 栏,精确
- PWA standalone:innerHeight 是真实可视区域,不含 nav bar
- 键盘弹出:visualViewport resize → innerHeight 缩小 → 布局自适应
- overflow:hidden 防止 body 滚动
@lloydzhou

Copy link
Copy Markdown
Owner Author

审查发现以下需要修复的问题:

  1. 高:首个 WebSocket 消息可能被静默丢弃webagent/src/server.rs:92-108, 163-169

HTTP Upgrade 请求头使用 BufReader 读取后,代码丢弃该 reader,转而把原始 TcpStream 交给 WebSocket::from_raw_socketBufReader 可以在同一次底层读取中预读到 \r\n\r\n 之后的 WebSocket 帧;这些已缓冲字节随着 reader 销毁而丢失。客户端在一次 TCP 写入中发送握手和合法的首个掩码文本帧时,第一条输入会无报错丢失。建议改用 tungstenite 的服务端握手接口,或确保将 reader 中 HTTP 头后的剩余字节继续交给 WebSocket 解析器。

  1. 高:慢客户端可造成无界内存积压webagent/src/server.rs:50-72, 199-217

每个连接使用无界 mpsc::channel()broadcast() 会为每条 agent 事件向所有连接克隆并入队,客户端又可能因 TCP 写入回压卡住而无法消费。保持连接但不读取响应即可使队列无限增长,最终耗尽内存。应使用有界队列,在积压达到阈值时断开或丢弃慢客户端,并清理失效发送端。

  1. 中:--session 缺少名称时回放错误会话webagent/src/main.rs:86-97, 309-313

resolve_events_path 看到 --session 后无条件读取下一个参数;随后网关追加的 --output-format stream-json 因此会被当作会话名。例如执行 webagent --agent ./dist/agent.sh --session 时,日志显示回放路径是 .../--output-format/events.jsonl,而原 agent 对无名称 --session 的语义是自动创建会话。应仅在紧随参数存在且不以 - 开头时将其作为会话名;无名称情形应等 session_start 中的实际会话标识。

  1. 中:build-webagent 在全新检出目录中失败Makefile:33-36

该规则直接执行 cp ... dist/webagent,却没有创建 dist/。我在只含 Makefilewebagent/ 的临时全新目录运行 make build-webagent,Rust 编译成功后以 cp: dist/webagent: No such file or directory 退出。请在复制前添加 mkdir -p dist。同时建议将该目标纳入 .PHONY、顶层 buildclean,使新增的可发布二进制符合现有标准构建/清理入口。

已验证:cargo check --manifest-path webagent/Cargo.tomlcargo test --manifest-path webagent/Cargo.toml 均通过(当前无单元测试);并完成上述同包握手/首帧和全新目录构建复现。

…ion edge case)

1. 高:首个 WS 帧丢失 — 手动握手时 BufReader 可能预读到 \r\n\r\n 之后的
   WS 帧并随 reader 销毁丢失。改为直接 read 请求头字节,用 PrefixedRead
   包装让 tungstenite::accept 完成握手,不丢任何字节。

2. 高:慢客户端内存积压 — 无界 mpsc::channel 改为 sync_channel(256),
   broadcast 时 send 失败的连接自动 remove,防止队列无限增长。

3. 中:--session 无名称时误把后续 --output-format 当 session_id — 仅在
   下一个参数不以 - 开头时才作为 session 名。

4. 中:build-webagent 缺 mkdir -p dist — 已补;加入 .PHONY 和 clean。
@lloydzhou

Copy link
Copy Markdown
Owner Author

感谢审查,4 个问题已全部修复(7359fae):

1. 高:首个 WS 帧可能被静默丢弃

去掉 BufReader,改用裸 stream.read() 读请求头字节到 Vec<u8>(精确读到 \r\n\r\n)。然后把已读的原始字节通过 PrefixedRead 包装器作为前缀喂给 tungstenite::accept,让 tungstenite 自己完成握手并继续解析后续 WS 帧。PrefixedRead 先返回前缀字节,耗尽后透传底层 TcpStream,不丢任何字节。

2. 高:慢客户端无界内存积压

mpsc::channel()mpsc::sync_channel(256)(有界队列)。broadcast()send 失败(队列满或客户端已断开)的连接会被 remove,自动断开慢客户端。WsHub::clients 类型从 Sender 改为 SyncSender

3. 中:--session 无名称时回放错误会话

resolve_events_path 现在检查 --session 后的下一个参数是否以 - 开头。webagent ... --session --output-format stream-json--output-format 不再被当作 session 名,回放路径会等 session_start 事件中的实际 session_id 动态设置。

4. 中:build-webagent 在全新检出目录失败

build-webagent 目标已补 mkdir -p dist,并加入 .PHONY 列表和 clean 目标(清理 webagent/target)。

clippy 零警告,端到端冒烟测试通过。

从 DeepSeek Harness Web GUI 的编译产物中提取设计令牌与组件样式,
单文件零依赖重写 index.html(不引入任何前端框架):

样式对齐(dsw palette + conversation/tool UI 模式):
- 设计令牌:neutral-bluish 色板、deepseek 品牌蓝、red/amber 状态色,
  深色默认 + html[data-theme=light] 浅色切换(跟随系统,可手动,持久化)
- 用户消息:右侧气泡 radius 22px、max-width min(525px,82%),
  深色 rgb(44,44,46) / 浅色 rgb(237,243,254)
- 助手消息:手写 ~90 行极简 Markdown 渲染器(纯 DOM 构建 +
  textContent 赋值,天然防 XSS),支持标题/列表/表格/代码块/引用/
  链接/加粗,748px 居中列,16px/28px 正文
- 思考/工具行:折叠行结构 [icon] 标题 · 摘要 … 状态 chevron,
  running 态扫光(sweep)动画,完成后自动折叠
- 工具 IO 卡片:输入/输出分节、等宽字体、radius 12px,
  tool_call 与 tool_result 合并进同一张卡片,运行中/完成/失败状态
- turnStatus shimmer 渐变文字(思考中/执行 Bash/回复中)
- StatsLine 居中统计行(对话/请求/输入缓存命中率/输出/压缩)
- Hero 空态、toBottom 悬浮按钮(仅离底时出现)、composer 圆角
  22px 输入卡、textarea 自动增高
- prefers-reduced-motion 降级

修复两个重写过程中引入的 bug:
1. replay 丢消息 — appendText 用 rAF 合并渲染,replay 时事件同步密集
   到达,stop/user_input 里的 resetStream 会在 rAF 触发前 cancel 掉
   pending 渲染,整条 text 消息丢失。resetStream 改为先同步 flush 再
   清状态。
2. chevron 裁切 — .rowline 的 overflow:hidden(扫光动画用)会把旋转
   45° 后超出 8px 布局盒 1.66px 的箭头尖端裁掉。改为仅 running 态
   裁切,chevron 加 margin-right 3px 余量。

行为语义全部保留:WS 重连 replay 清空、HUD 累计、ANSI 剥离、
PWA 高度适配(window.innerHeight + visualViewport)、贴底自动滚动策略。

server.rs: manifest 主题色 #0f1115 → #151517 对齐新背景色。

验证:playwright + headless chromium,mock agent 覆盖全部 13 种事件
类型,46 项 DOM 断言 + chevron 三态几何测量 + 真实 WS 链路 E2E
全部通过,零页面报错。
@lloydzhou

Copy link
Copy Markdown
Owner Author

前端已按 DeepSeek Harness(DSH)的对话展示样式完整重写(9620048),单文件零依赖,没有引入任何前端框架——这个场景是"只追加的事件流",直接 DOM 操作本身就是最优解,VDM/响应式的收益发挥不出来。

样式对齐来源

从 DSH Web GUI 运行时的编译产物(dsh-client-ui-conversation / dsh-client-ui-tool 插件 bundle 内嵌的 CSS module 与设计令牌)逐项提取复刻:

DSH 组件 webagent 对应
设计令牌 --dsw-*(neutral-bluish / deepseek / red / amber) :root 深色 + [data-theme=light] 浅色
用户消息气泡(radius 22px、min(525px,82%) 同参数
助手 Markdown(748px 列、16px/28px) 手写 ~90 行纯 DOM 渲染器(防 XSS)
ReasoningRow / ToolRow 折叠行 + 扫光动画 同结构 [icon] 标题 · 摘要 … 状态 chevron
ioCard(输入/输出分节、等宽、radius 12px) tool_call + tool_result 合并进同一卡片
turnStatus shimmer / StatsLine / Hero / toBottom / composer 全部对齐

顺带修复的两个 bug

  1. replay 丢消息:rAF 合并渲染在 replay 密集事件下会被 resetStream 提前 cancel,整条 text 丢失 → resetStream 先同步 flush
  2. chevron 裁切.rowlineoverflow:hidden(扫光用)切掉旋转后超出 1.66px 的箭头尖端 → 仅 running 态裁切 + margin-right 3px 余量

验证

playwright + headless chromium,mock agent 覆盖全部 13 种事件类型:46 项 DOM 断言(含双主题令牌色值精确匹配、chevron 三态几何测量、elementFromPoint 尖端命中测试、真实 WS 链路 E2E)全部通过,零页面报错。截图:/tmp/final-dark.png/tmp/final-light.png

另外:期间发现 rust 版 sanitize_utf8 把合法多字节 UTF-8 逐字节 b as char 产生 mojibake 的问题,已在 origin/main 侧修复(本地合并后验证通过)。

不引入 KaTeX/MathJax(压缩 ~300KB JS + ~1MB 字体,与单文件零依赖
架构冲突),改用浏览器原生能力做词法级 LaTeX → Unicode/HTML 转换:

- 行内 $...$ 与块级 $$...$$(单行/跨行,居中 serif 排版)
- 上下标 ^/_ → 原生 <sup>/<sub>,支持嵌套(e^{x^2}、\sum_{i=1}^{n})
- ~120 个常用命令 → Unicode 符号:希腊字母、关系/算子(≤ ≥ ≠ ≈
  ∈ ⊂ ∪ ∩ ∀ ∃)、大算子(∑ ∏ ∫ ∮)、箭头、函数名(sin/log/lim)
  黑板粗体(\mathbb{R} → ℝ)、重音(\hat/\bar/\vec → 组合字符)
- \frac 降级为 (a)/(b),单 token 侧不加括号(\frac{1}{2} → 1/2)
- \sqrt → √,单字符/单命令不加括号,复合表达式加括号
- \text/\mathrm/\operatorname 直立文本;\left/\right/\displaystyle
  等排版命令剥除;未识别命令原样保留(安全降级,不丢内容)
- 货币守卫:$100 不误判为公式;拒绝的 token 只消费开头 $ 并从下
  一位重扫,避免吞掉后续真公式的起始 $($100 下标 $a_1$ 场景)

公式用 Georgia/Times serif 与正文区分;流式分片下公式在 Markdown
重渲染时自然自愈(完整 buffer 每帧重建)。

验证:playwright 24 项公式断言(符号映射、嵌套上下标、分数/根号
括号规则、重音组合字符、货币守卫、块级居中)+ 46 项通用回归全部
通过,零页面报错。
@lloydzhou

Copy link
Copy Markdown
Owner Author

新增:零依赖数学公式渲染(a0c4d9b)

关于"要不要上 KaTeX"的结论:不上。KaTeX 是 ~300KB JS + ~1MB 字体,与 webagent 单文件零依赖(include_str! 内嵌)的架构冲突。替代方案是Unicode 数学符号 + 浏览器原生 <sup>/<sub>,纯词法级转换 ~200 行:

能力 实现 示例
行内/块级 $...$ / $$...$$(居中 serif)
上下标 原生 sup/sub,支持嵌套 \sum_{i=1}^{n} x_i → ∑ 上 n 下 i=1 的 xᵢ
符号 ~120 个命令 → Unicode \alpha\geq 0.05 → α ≥ 0.05、\mathbb{R} → ℝ
分数 降级 (a)/(b),单 token 不加括号 \frac{1}{2} → 1/2、\frac{a+b}{c-d} → (a+b)/(c-d)
根号 √ + 智能括号 \sqrt{2} → √2、\sqrt{a+b} → √(a+b)
重音 Unicode 组合字符 \hat{y} → y+combining-circumflex
降级 未识别命令原样保留文本 \begin{pmatrix} 字面显示

细节:货币守卫($100 不误判)、被拒 token 只消费开头 $ 重扫(避免吞掉真公式的起始 $)、流式分片下公式随 Markdown 全量重建自然自愈。

验证:24 项公式断言 + 46 项通用回归全过,零页面报错。矩阵/多行对齐等复杂结构不支持(会字面降级),对聊天场景够用。

@lloydzhou
lloydzhou merged commit 0d902fa into main Aug 15, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant