【免费下载链接】herdr-web-ui
Browser and phone client for herdr: chat and live terminal for every agent pane, remote PCs over SSH, web push alerts
herdr-web-ui 是 herdr 的浏览器与手机客户端,为每个 agent 分屏提供聊天与实时终端,并支持通过 SSH 连接远程 PC、发送 Web 推送提醒。当你在手机上打开一个正在疯狂打印输出的终端时,"谁来保证慢设备不会拖垮整条链路?"答案是它内置的终端流控体系:基于pty-ack的 ACK 确认、256 KiB 高水位背压,以及 4008 慢消费者断连机制。
这套机制的目标很简单:让快的终端不丢字节,让慢的手机不卡死别人,让彻底跟不上的连接被体面地断开。下面把三个核心环节拆开讲。
为什么终端输出必须做流控
herdr-web-ui 里一个 pane 的输出可能被多个客户端共享:桌面的浏览器、另一台 PC 的浏览器、还有手机上装的 PWA。终端输出(ANSI 字节流)有两个特殊之处:
- 丢一个字节就可能破坏整个 ANSI 状态(颜色、光标、表格错位),所以不能像视频那样"丢帧保流畅";
- 浏览器解析速度天差地别:桌面 Chrome 毫秒级渲染,弱网手机可能要几十毫秒一帧。
如果服务端无限往 WebSocket 里灌数据,慢客户端的发送队列会持续膨胀,最终内存失控、拖慢同连接上其他 pane 的收发。所以 herdr-web-ui 采用了一套"生产者-消费者信用"(credit-based)模型,完整决策记录见 docs/terminal-flow-control.md。
ACK 确认:stream_id 与累计 offset
流控的第一步是"确认到底读没读"。浏览器 attach 一个 pane 时会声明flow_control: "ack"(协议定义见 shared/protocol.ts),随后:
- 每次订阅都有全新的
stream_id——防止上一次连接遗留的回调误确认新会话; - 每帧
pty-data携带累计的 UTF-8 负载字节偏移offset(含重放回放); - 浏览器端只在 xterm 的
write(data, callback)回调触发后才发pty-ack——它确认的是"解析完成",而不是"收到了"或"画出来了"。
{ "type": "pty-ack", "pane_id": "...", "stream_id": "...", "offset": 183472 }这个细节很关键:如果按"收到就确认"来算,数据其实还堵在浏览器解析队列里,服务端会误以为链路健康。ACK 只在 xterm 真正消化完之后发出,服务端拿到的才是真实的"消费水位"。
服务端对每个订阅维护一个输出窗口,核心常量集中在 server/output-window.ts:
| 常量 | 值 | 作用 |
|---|---|---|
OUTPUT_HIGH_BYTES | 256 KiB | 高水位:未确认字节达到即暂停 |
OUTPUT_LOW_BYTES | 64 KiB | 低水位:降回此值以下恢复 |
OUTPUT_HARD_BYTES | 1 MiB | 硬预算:超出直接停止(4008) |
OUTPUT_STALL_MS | 2 s | ACK 停滞判定基准时间 |
OUTPUT_PAUSE_MAX_MS | 10 s | 单客户端最多按住共享 PTY 的时长 |
服务端对每个客户端的裁决只有三种,源码里就叫OutputVerdict:"flow"(继续发)、"pause"(暂停共享 PTY 的读取)、"stop"(4008 断开)。
背压:256 KiB 高水位如何"暂停"而不"卡死"
当某个客户端的未确认字节(pending)达到 256 KiB高水位,服务端就暂停 PTY 的输出读取;当所有剩余消费者都降回64 KiB低水位以下、且其传输队列有空间时才恢复。注意三个精心设计的点:
- 暂停输出,但输入照常:终端被打断输出时,你依然可以敲键盘、发 Ctrl+C、改窗口尺寸。输出暂停不等于连接僵死;
- 1 MiB 硬预算兜底:暂停后管道里已读到的数据仍可能到达,所以每个 ACK 订阅有 1 MiB 的硬负载上限,外加一个独立的 1 MiB WebSocket 传输预算(含 JSON 转义开销);
- 防止"一个慢的拖住所有人":WebSocket 发送队列是整条连接共享的,所以某个安静、已全部确认的分屏不会替邻居的积压"受罚";反过来,单个客户端无论如何确认方式,最多只能按住共享 PTY 10 秒,一个"一字节一字节 ACK"的慢手机无法把自己的速率强加给所有同路人。
底层还要满足第二道闸门:PTY 边车(sidecar)本身也响应stdout.write() === false和drain事件,两道闸门都清空才能恢复——这正是当初保留 Node PTY sidecar 的原因:Bun.Terminal 的公开 API 没有输出pause()/resume(),其drain针对的是写入端输入而非子进程输出消费,见 server/pty/session.ts 中pause()/resume()的实现。
4008 断连:判定标准比"超时"更精细
4008是 herdr-web-ui 专属的关闭码,定义在 shared/terminal-flow.ts:
export const OUTPUT_STALLED_CLOSE_CODE = 4008;它不是简单的"2 秒没 ACK 就断"。判定逻辑(OutputWindow.check())综合了几个因素:
- 停滞以 ACK 进度衡量,而不是排队时间——只要 ACK 还在推进,说明客户端在"排空"积压,就不算停摆;每一个推进的 ACK 都会重置计时器,所以正在消化大积压的客户端不会被误杀;
- 叠加最老未确认帧的"过路费":一个 ACK 不可能早于"最老的未确认帧穿过链路"的时间到达,所以死线 = 2 s + 最老帧按 32 KiB/s 慢链路折算的穿越时间。弱网手机不会因为一个巨帧还在路上就被断;
- 迟来者(late joiner)有独立预算:重连或新加入时,服务端会回放最多 128 KiB 的终端尾部(replay tail)。这个帧整体穿过链路并被解析之前,第一个 ACK 不可能到达,所以加入者获得"2 s + 帧大小 ÷ 32 KiB/s"的预算,最长 15 s;预算内的加入者不会暂停其他客户端共享的 PTY;
- 防重复确认:重复、过期、乱序的 ACK 无法二次释放信用,指向未来或非整数的 offset 会得到
invalid_ack错误帧;send() === -1表示帧已被 Bun 入队,重发会造成终端字节重复,所以投递失败即停连。
一旦判定stop,服务端以 4008 关闭该连接,并把它从所有 attach 中移除,让健康的同伴立即释放。原始 herdr pane 不会被杀掉,新的 attach 只需等旧进程释放 attach 槽位。
用户视角:过载提示与 Reconnect 按钮
断连不是无声无息。浏览器端(src/lib/ws.ts 与 src/components/PaneTerminal.tsx)的处理哲学是:宁可让你明确操作,也不假装一切完好。
- 页面显示过载提示横幅 + Reconnect 按钮,且不会自动重连去撞下一次过载——自动重连循环只会掩盖丢失的 ANSI 状态;
- 点 Reconnect 会就地用全新终端重新 attach(无需刷新页面),切换 pane 也会触发重连;
- 一个例外很人性化:如果 4008 发生在页面处于隐藏状态、或刚恢复显示 5 秒内——冻结的页面发不出任何 ACK,这是"页面被挂起"而非"设备太慢"——那么页面重新被使用时会自动重连一次;页面在活跃使用中遭遇的 4008 则是终局的,必须手动恢复;
- 旧版浏览器客户端即使不带
flow_control也保留线缆兼容与传输上限,只是没有 xterm 解析信用,刷新页面即可启用完整路径; - 远程 PC 场景走 relay 转发时适用同样的规则:256 KiB 积压持续 2 秒才停止输出,而不是队列里有一点字节就断(见 changelog.d/relay-stall-threshold.md)。
需要强调的是:过载不会静默丢弃数据块后继续显示一个"看似完整"的实时流。128 KiB 的回放尾部只是"流的尾巴"而非"终端的序列化快照",它按 UTF-8 字节计数、避免切断字符,但任意 ANSI 状态无法从所有可能的尾部可靠重建——这就是为什么恢复只能靠重新 attach。
关键源码导读
| 模块 | 职责 |
|---|---|
| shared/terminal-flow.ts | 4008 关闭码的单一出处 |
| server/output-window.ts | 水位、停滞判定、flow/pause/stop 裁决 |
| shared/protocol.ts | WebSocket 帧协议:attach/pty-data/pty-ack |
| server/pty/session.ts | PTY 会话与 pause/resume 边车 |
| src/lib/ws.ts | 浏览器侧 4008 处理与"不再自动重连" |
| docs/terminal-flow-control.md | 完整决策文档与 Bun vs Node 对比实测 |
小结
herdr-web-ui 的终端流控把"背压"落实到了可审计的每个字节:
- ACK 确认——
stream_id+ 累计 UTF-8offset,只在 xterm 解析完成后确认,信用真实可信; - 背压——256 KiB 高水位暂停读取、64 KiB 低水位恢复,1 MiB 硬预算兜底,输出暂停时输入永不中断;
- 4008 断连——按 ACK 进度 + 帧穿越时间精细判停滞,10 s 上限防止慢客户端绑架共享 PTY,迟来者享 15 s 预算,断连后 UI 明确要求人工 Reconnect,绝不自动撞过载。
对于想理解"浏览器端实时终端如何既快又不丢"的开发者,docs/terminal-flow-control.md 里还有 49,152,000 字节日韩文/emoji 压测的完整数据(SHA-256 逐字节一致),值得细读。
【免费下载链接】herdr-web-ui
Browser and phone client for herdr: chat and live terminal for every agent pane, remote PCs over SSH, web push alerts
相关推荐
Flink CDC背压处理终极指南:流量控制与反压机制深度解析
Flink CDC背压处理终极指南:流量控制与反压机制深度解析 Flink CDC作为Apache Flink生态中强大的流式数据集成工具,在处理实时数据同步时
数据集成变更数据捕获大数据流处理数据同步批处理深入理解node-redis流控制:背压机制与流量控制的终极指南
深入理解node redis流控制:背压机制与流量控制的终极指南 在Node.js应用开发中,高效的Redis客户端是构建高性能数据交互的关键。node red
后端数据库herdr-web-ui与collie、roamgate、herdr-remote横评:herdr手机客户端怎么选?
herdr web ui与collie、roamgate、herdr remote横评:herdr手机客户端怎么选? 在电脑上跑着 Claude Code、Co
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考