Skip to content

WebSocket 实时连接

外脑编辑器后端提供 4 个 WebSocket 长连接端点,用于协同编辑、远程桌面、即时消息推送和通用数据变更推送。所有端点都通过标准 HTTP Upgrade: websocket 握手建立连接。

本页定位:这是一份「有哪些实时连接 + 怎么连 + 鉴权方式 + 去哪看消息协议」的导航页。每种连接的完整消息协议、事件类型、订阅语义请按各节末尾的链接查阅对应详细文档,本页不重复搬运。

源码apps/backend/src/index.ts(WebSocket 升级路由分发,绕过 Hono 中间件栈)

升级与分发机制

后端使用 Bun 原生 Bun.serve 处理 WebSocket。请求进入时,若 Upgrade 头为 websocket,则按 url.pathname 分发到对应服务,不经过 Hono 的 CORS / rateLimiter / serverMode 中间件。未匹配的 WS 路径返回 404

连接 URL 形如:

wss://<后端域名>/<path>

本地开发时将 wss:// 换为 ws://,域名换为后端开发地址。前端统一把后端 HTTP base URL 的 http 前缀替换为 ws 得到 WebSocket base。

端点总览

端点用途鉴权方式Token 传递位置对应服务
/collaboration协同编辑(Hocuspocus / Yjs)Supabase JWT + 文档权限校验Yjs 协议认证帧services/hocuspocus.ts
/ws/guacamole远程桌面(Guacamole)一次性加密 token(≤10 秒、单次使用)URL query ?token=services/guacamole.ts
/ws/rompROMP 即时消息实时推送JWT 或 OAuth(mobile:full首帧 auth 消息services/romp-ws.ts
/ws/realtime通用数据变更推送(change_eventsSupabase JWT首帧 auth 消息services/realtime-ws.ts

此外还有一个 /ws/terminal 端点(资产 SSH 终端),其鉴权依赖一次性 sessionId + nonce,由资产连接 API 派发,不在本页范围;详见计算资产相关文档。

所有端点都要求登录态身份(JWT / OAuth / 一次性会话 token),匿名连接会被拒绝。Agent Token(wna-)和 API Key(wn-)一律禁止进入 WebSocket,这类调用请走 HTTP 通道。


GET /collaboration

协同编辑(Hocuspocus / Yjs)

基于 Hocuspocus v3 的 Yjs 协同服务器,用于富文本文档的多人实时协同(替代早期的 Supabase Realtime peer-to-peer 同步)。

  • 鉴权:Supabase JWT,经 Hocuspocus 协议的认证帧传递(不走 query / header)。服务端在 onAuthenticate 中验证 JWT 签名与过期,并强制做文档访问权限检查;无 write 权限的用户被降级为只读连接(框架在协议层自动拒绝文档修改)。
  • 文档标识:连接需提供 documentName,格式为 {serverMode}:{documentId}(例如 selfhosted:abc-123)。前缀 serverMode 仅做与服务端 env mode 的一致性校验,不一致(含非法值)直接拒绝连接。
  • 持久化:PostgreSQL 双写(yjs_state + content),可选 Redis 扩展用于多实例同步。
  • 典型客户端:前端 @hocuspocus/providerHocuspocusProviderurl 指向 …/collaborationnamedocumentNametoken 传 Supabase access token。

消息协议详见docs/CLAUDE/hocuspocus.md(Yjs 同步协议由 Hocuspocus / ProseMirror 封装,业务无需手写帧)。


GET /ws/guacamole

远程桌面(Guacamole)

基于 guacamole-lite 的远程桌面隧道,承载计算资产的 RDP / VNC / SSH 远程会话。

  • 可用性门禁:Guacamole 服务未初始化(如未配置 guacd)时,升级请求直接返回 503 Guacamole not available
  • 鉴权:一次性加密 token,拼接在 URL query 上,形如 …/ws/guacamole?token=<encrypted>。token 用 AES-256-CBC 加密,payload 只含 connection.typesessionIdnonceexpiry不含任何身份信息或连接凭证。安全不变量:过期时间 ≤ 10 秒、单次使用(由会话状态机 + nonce 唯一约束保证)。
  • token 来源:由资产会话 HTTP 接口派发(apps/backend/src/routes/assets.ts,返回 wsUrl + token),前端拿到后立即用于建立隧道。服务端在 processConnectionSettings 回调里通过 claim_asset_session RPC 原子校验 nonce 并将会话标记为 open,再解密真实连接凭证连接 guacd(凭证全程不经过客户端)。
  • 并发与只读:会话并发上限在 RPC 行锁内判定,超额拒绝;observe 模式会话由服务端强制写入 guacd read-only,客户端无法绕过。

消息协议详见docs/features/assets/remote-desktop.md(隧道帧由 Guacamole 协议封装,前端用 Guacamole.WebSocketTunnel)。


GET /ws/romp

ROMP 即时消息推送

ROMP 即时通讯的实时推送通道,替代 ROMP 对 Supabase Realtime 的直接依赖,推送 message.newtypingmessage.update、调试日志等事件。

  • 鉴权:连接建立后,客户端必须在 5 秒内发送首帧 auth 消息完成认证,否则被断开(关闭码 4001)。首帧格式:

    json
    { "type": "auth", "token": "<token>", "mode": "<serverMode>" }
    • token 接受两类:Supabase JWT(用户登录)或 OAuth access tokenwno- 前缀且 scope 含 mobile:full)。wno-rt- / wno-code- 等非 access 类型、Agent Token(wna-)、API Key(wn-)一律拒绝。
    • mode 仅做与服务端 env mode 的一致性校验,不一致(含非法值)以关闭码 4005 断开。
  • 功能门控:认证通过后还会检查站点级开关(site_settings.romp.enabled)和用户级禁用(grantsromp:disabled),未通过则拒绝。

  • 认证后:服务端回 { "type": "ready", "user_id": "…" }。之后支持 subscribe / unsubscribe(按会话)、subscribe_topic / unsubscribe_topic(按 topic,含订阅数量与频率限制)、应用层 ping / pongmessage.new 等用户级事件登录即推送给该用户全部连接,无需显式订阅。

  • 心跳:服务端每 30 秒发 WS 层 ping,连续 2 个周期无响应即断开。

消息协议详见docs/romp/realtime.md


GET /ws/realtime

通用数据变更推送

通用实时数据推送通道,后端通过 PostgreSQL LISTEN change_events 监听数据库变更,广播给订阅了对应 project / team 的客户端(替代 change_events 对 Supabase Realtime 的依赖)。

  • 鉴权:与 ROMP 类似,连接后须在 5 秒内发送首帧 auth 消息:

    json
    { "type": "auth", "token": "<supabase-jwt>", "mode": "<serverMode>" }
    • token 仅接受 Supabase JWT(无 OAuth 通道,无 ROMP 那样的功能门控)。Agent Token(wna-)/ API Key(wn-)拒绝;缺失或非字符串 sub 的 token(如 service role JWT)也被拒绝。
    • mode 同样只做 env mode 一致性校验,不一致以关闭码 4005 断开。
  • 认证后:服务端回 { "type": "ready", "user_id": "…" }。客户端通过 subscribe_project / subscribe_team(及对应 unsubscribe_*)订阅,订阅前服务端校验该用户对 project / team 的成员权限;订阅总数有上限,每用户每 mode 连接数也有上限(超限淘汰最旧连接)。

  • 权限漂移防护team_members 发生变更时,服务端会重新校验受影响订阅者的权限,失权者被强制退订并收到 SUBSCRIPTION_REVOKED 通知。

  • 事件:匹配的变更以 { "type": "change_event", "data": … } 推送;部分应用层变更(计费 / 文档保存等)以 { "type": "app_event", "data": … } 推送。心跳机制与 ROMP 一致(30 秒 ping,连续 2 周期无响应断开)。

消息协议详见docs/features/realtime/README.md


关闭码参考

关闭码含义
4001认证超时(5 秒内未发送首帧 auth,适用于 ROMP / Realtime)
4002首帧不是合法的 auth 消息
4003认证失败 / 功能被禁用
4004连接被新连接替换(Realtime 每用户连接数超限时淘汰最旧连接)
4005客户端声明的 mode 与服务端 env mode 不一致
4408协同编辑空闲超时(Hocuspocus)

Guacamole 端点的鉴权失败不走上述自定义关闭码,而是在 token 校验阶段拒绝建连或由隧道层报错。

设备 WebSocket

GET /api/ws/record

一代机录音上行 WebSocket。升级前校验设备 ID 绑定与可选 legacy JWT;同域反代去掉 /api 前缀时后端也接受 /ws/record。消息协议与验证边界见一代机录音接入

GET /ws/agent-tunnel

本地设备连接器主动建立的反向隧道。升级前校验 Device 凭据、设备绑定和协议版本;同域反代场景也接受 /api/ws/agent-tunnel。浏览器与普通 API 客户端不应直接连接。

AI Workflow Editor