跳转到内容
WebMux

WebSocket 接口

基于 main@306d253 的文档在 GitHub 上查看编辑此页

会话列表,文本帧 JSON。连接后先收到 snapshot,之后为增量事件:

{"type": "snapshot", "sessions": [SessionInfo, ...]}
{"type": "created", "session": SessionInfo}
{"type": "updated", "session": SessionInfo}
{"type": "removed", "id": "abc123"}

updated 每会话最多 500 ms 一次;仅预览变化最多 2 秒一次。客户端无需发送消息。

终端流,二进制帧,首字节为类型(internal/contract/terminal.go 中的 Frame* 常量)。JSON 控制帧由 internal/hub 编码,会话服务只产出类型化消息。

可选查询参数:

参数说明
client客户端 ID,[A-Za-z0-9_-]{8,64};前端每个标签页一个,刷新与重连保持
label设备名,去除控制字符后最多 40 个字符,用于显示控制端
generation续接点:客户端终端所属的会话代际
offset续接点:客户端已渲染到的输出 offset(十进制)

格式不符的参数被忽略:缺少 client 时服务端为连接生成随机 ID;续接点无效时全量回放。

服务端 → 客户端:

首字节含义载荷
0x00输出PTY 原始字节
0x01快照可安全回放的缓冲;客户端应先重置终端再写入,截断恢复时仅初始化终端模式,不包含旧文本
0x02状态JSON SessionInfo
0x03退出JSON {"exitCode": n},随后关闭连接
0x04位置JSON {"generation", "mode": "reset"|"append", "start", "end", "truncated"},先于回放
0x05提交状态JSON {"id", "state", "error"?}
0x06控制JSON {"controller": {"clientId", "label", "since"} | null, "cols", "rows", "colorSchemeReports"}

客户端 → 服务端:

首字节含义载荷
0x00输入写入 PTY 的 UTF-8 字节
0x01resizeJSON {"cols": n, "rows": n};仅控制端生效
0x02重新同步空;服务端回复 reset 位置帧与 0x01 快照
0x03提交JSON 头 {"id", "generation", "bodyBytes"}、\n、正文与提交键字节
0x04查询提交JSON {"ids": [...]},最多 64 个
0x05接管控制空
0x06自动回复xterm 对设备查询的应答字节;仅负责回复的连接写入,不取得控制

colorSchemeReports 表示程序已开启 DEC 私有模式 2031(CSI ? 2031 h)。服务端持续跟踪该模式,不受回放缓冲淘汰影响,变化时在对应输出之后广播 0x06。开启期间,两类客户端的行为如下:

  • 浏览器:在主题切换、附着或取得控制时以自动回复发送 CSI ? 997 ; 1 n(暗色)或 CSI ? 997 ; 2 n(亮色),并应答 CSI ? 996 n 查询;程序据此重新查询 OSC 10/11 颜色。
  • 原生终端附着(webmux attach/run):在取得控制或程序开启该模式时,向本地终端发送 OSC 11 背景色查询,按应答亮度以自动回复发送同样的报告。
    • 查询应答本身不写入 PTY。
    • 本地终端不应答 OSC 11 时不发送报告。
    • 分离时关闭本地终端的 2031 模式。

使用二进制帧而非 JSON,是为了避免 base64 开销以及在多字节字符中间截断时的 UTF-8 问题;提交帧同样只把元数据放进 JSON 头。

每次回放(附着或 0x02)依次发送 0x06 控制帧和 0x04 位置帧:观察端先按控制端网格调整终端,再解析按光标定位的历史。offset 为会话自启动以来累计输出的字节数。0x02 清空队列时保留尚未送出的 0x05 提交状态,排在状态帧之后、0x03 退出帧之前;退出帧始终是最后一帧。

  • mode: "reset":随后一帧 0x01 快照。缓冲未截断时覆盖 [0, end);缓冲已截断时 start = end,快照仅包含模式初始化序列,不计入原始流 offset。客户端清空不可恢复的历史,不解析原始缓冲的任意字节后缀。
  • mode: "append":续接点的代际一致且仍在保留范围内时使用,随后(如有)一帧 0x00,内容恰为 [offset, end);即使更早历史被覆盖,已有终端状态仍可安全续接,不清空画面、不请求重绘。
  • truncated:最早输出已被环形缓冲覆盖;不能由重置后的 start 推断保留范围。

截断后的重置会请求仍运行的程序重新绘制:

  • 做法:短暂改变 PTY 的实际列数,再恢复控制端尺寸;不发送按键、不转移控制权。逻辑控制网格保持不变,并发控制端 resize 优先于待恢复的尺寸。
  • 原因:仅发送同尺寸 SIGWINCH 不足以使部分 TUI(包括 opencode v2.1.4)重绘。
  • 限制:程序不响应时不保证恢复成功,应使用程序自身的重绘操作。已退出程序不请求重绘,不能恢复被覆盖的历史。
  • 浏览器不显示历史丢弃或重绘请求提示,查找范围与复制缓冲中的历史截断标记仍保留。

GET /api/sessions/{id}/buffer 仍返回诊断用原始字节,不是可恢复的屏幕快照;复制全部缓冲沿用此接口。

  • 退出后的浏览器恢复也使用 WebSocket 的安全回放路径,不将该接口的截断字节送入 xterm。
  • 这里不保存完整 VT 状态:若恢复点恰在一个控制序列中间,后续实时字节仍可能包含该序列的尾部,程序完整重绘才是画面恢复依据。

回放与实时订阅在同一把锁内登记,此后的 0x00 帧按字节累加即为客户端的 offset。

浏览器在断线重连期间显示不遮挡终端内容的“正在恢复…”提示,连接恢复后隐藏;全量回放期间使用“正在恢复…”遮罩。

提交 ID 为 [A-Za-z0-9_-]{8,64}。服务端在一次输入锁持有期间写入正文与提交键,状态依次为:

state含义
received已登记,正在写入
written全部字节已写入 PTY(不代表 agent 已处理)
partial部分字节已写入后出错
failed未写入;error 为 generation、invalid 或 write
missing仅用于查询:本次运行无此 ID 的记录(未送达或已过期)

重复 ID 不会再次写入,服务端直接回报已记录状态。状态发往提交客户端(同一 client)的全部连接;在 received 期间查询过该 ID 的其他客户端(例如在新标签页恢复的草稿)也会收到最终状态。记录保留最近 256 条、30 分钟。

  • 会话无控制端时,首个附着的客户端成为控制端;同一 client 重连保持控制。
    • 控制端客户端最近附着的连接是唯一有效连接(复制的标签页可能带着相同 ID),只有它的 resize 与自动回复生效。
    • 它断开时由该客户端的其他连接接替。
    • 同一客户端的另一条连接成为有效连接时(接替或用户输入),服务端向它单独发送 0x06,使它重新发送尺寸。
  • 0x00 输入、0x03 提交与 0x05 把控制权交给发送方;控制端的最后一个连接断开后控制权空缺,不自动转交。
  • 只有控制端连接的 0x01 生效,所有权检查与 resize 在同一临界区内完成;尺寸或控制变化时广播 0x06。
  • 输入来源由客户端标明:xterm 的用户输入走 0x00,对设备查询的自动应答走 0x06(同样的字节可能来自按键,如 Shift+F3 与光标位置报告都是 ESC [ 1 ; 2 R)。0x06 只接受控制端连接,无控制端时只接受最早附着的连接,且不取得控制、不清除关注提醒。
  • 前端复制标签页时,若在 sessionStorage 中看到原标签页的存活标记,会换用新的客户端 ID。
关闭码含义
1011服务端内部消息非法或编码失败;仅关闭当前连接,会话继续运行
4404会话不存在
4408客户端过慢或写超时;客户端应立即重连并回放
4409混合会话已移到另一端,原因为 session moved: <所在端>;终止状态,客户端不得自动重连。附着一个没有终端运行的混合会话时也立即以它关闭;webmux attach 视为被动分离,以退出码 0 结束
4410会话已退出(先发送快照与退出帧)

认证失败在握手阶段直接返回 HTTP 401,不升级连接。

所有 WebSocket 路径(/ws/sessions、/ws/s/{id}、/ws/s/{id}/chat、/ws/settings)接受查询参数 compress=1。 带此参数且客户端提议 permessage-deflate 时,服务端以上下文复用模式协商该扩展;不带参数时不协商。 浏览器总是提议该扩展且脚本无法撤回,因此由参数表达意图;前端按设备开关追加它(设置页“网络”,US-H10)。 小于 128 字节的消息不压缩,帧格式与语义不变。