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/romp | ROMP 即时消息实时推送 | JWT 或 OAuth(mobile:full) | 首帧 auth 消息 | services/romp-ws.ts |
/ws/realtime | 通用数据变更推送(change_events) | Supabase 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/provider的HocuspocusProvider,url指向…/collaboration,name传documentName,token传 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.type、sessionId、nonce、expiry,不含任何身份信息或连接凭证。安全不变量:过期时间 ≤ 10 秒、单次使用(由会话状态机 +nonce唯一约束保证)。 - token 来源:由资产会话 HTTP 接口派发(
apps/backend/src/routes/assets.ts,返回wsUrl+token),前端拿到后立即用于建立隧道。服务端在processConnectionSettings回调里通过claim_asset_sessionRPC 原子校验nonce并将会话标记为open,再解密真实连接凭证连接 guacd(凭证全程不经过客户端)。 - 并发与只读:会话并发上限在 RPC 行锁内判定,超额拒绝;
observe模式会话由服务端强制写入 guacdread-only,客户端无法绕过。
消息协议详见:docs/features/assets/remote-desktop.md(隧道帧由 Guacamole 协议封装,前端用 Guacamole.WebSocketTunnel)。
GET /ws/romp
ROMP 即时消息推送
ROMP 即时通讯的实时推送通道,替代 ROMP 对 Supabase Realtime 的直接依赖,推送 message.new、typing、message.update、调试日志等事件。
鉴权:连接建立后,客户端必须在 5 秒内发送首帧
auth消息完成认证,否则被断开(关闭码4001)。首帧格式:json{ "type": "auth", "token": "<token>", "mode": "<serverMode>" }token接受两类:Supabase JWT(用户登录)或 OAuth access token(wno-前缀且 scope 含mobile:full)。wno-rt-/wno-code-等非 access 类型、Agent Token(wna-)、API Key(wn-)一律拒绝。mode仅做与服务端 env mode 的一致性校验,不一致(含非法值)以关闭码4005断开。
功能门控:认证通过后还会检查站点级开关(
site_settings.romp.enabled)和用户级禁用(grants中romp:disabled),未通过则拒绝。认证后:服务端回
{ "type": "ready", "user_id": "…" }。之后支持subscribe/unsubscribe(按会话)、subscribe_topic/unsubscribe_topic(按 topic,含订阅数量与频率限制)、应用层ping/pong。message.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 客户端不应直接连接。