Skip to content

WebSocket 事件 ​

Halo WebSocket 事件参考:连接与鉴权、订阅会话、哪些事件发给谁、事件信封,以及全部事件频道和终端实时流。

Halo 通过 WebSocket 实时推送进度:正在生成的回复、思考与工具调用、等待回答的问题,以及数字人、团队、文件和终端的变化。HTTP API 负责发起操作,WebSocket 负责告诉你接下来发生了什么。Halo 的网页版和安卓客户端用的就是这套协议。

连接 ​

WebSocket 和 HTTP API 在同一个地址和端口上,路径是 /ws:

text
ws://<HOST>:<PORT>/ws

把 HTTP API 的基础地址 中的 http 换成 ws(https 换成 wss),再加上 /ws。如果服务挂在反向代理的路径前缀下,前缀要保留,例如 wss://<HOST>/<PATH_PREFIX>/ws。桌面版只有在开启 启用远程访问 时才接受连接。

鉴权 ​

连接建立后还没有鉴权。第一条消息发送访问密码:

json
{ "type": "auth", "payload": { "token": "<ACCESS_PASSWORD>" } }

成功时收到:

json
{ "type": "auth:success" }

失败时收到下面这条消息,约 100 毫秒后服务端关闭连接:

json
{ "type": "auth:failed", "error": "Invalid token" }
  • 密码放在消息里,不放在请求头里;它和 HTTP 请求用的是同一个访问密码。
  • 鉴权失败与 HTTP 登录共用锁定规则:同一 IP 连续失败会被暂时锁定,锁定期间的鉴权一律失败。
  • 鉴权之前发送 subscribe 或终端消息,会收到 { "type": "error", "error": "Not authenticated" }。
  • 修改访问密码后,请重启 Halo,已经建立的连接才会断开。

消息格式 ​

双方都发送 JSON 文本帧。客户端发出的消息形如 { "type": "…", "payload": { … } }:

typepayload作用
auth{ "token": "<ACCESS_PASSWORD>" }鉴权,必须是第一条消息
subscribe{ "conversationId": "…" }接收这个会话的全部事件
unsubscribe{ "conversationId": "…" }停止接收这个会话的详细事件
ping—服务端回复 { "type": "pong" }
terminal-input{ "sessionId": "…", "data": "…" }向终端会话发送按键,见终端实时流
terminal-resize{ "sessionId": "…", "cols": 120, "rows": 32 }调整终端大小
terminal-attach{ "sessionId": "…" }作为终端的查看者参与流量控制
terminal-detach{ "sessionId": "…" }退出流量控制
terminal-ack{ "sessionId": "…", "charCount": 4096 }确认已经渲染的字符数

服务端发出的消息有 auth:success、auth:failed、error、pong 和 event 五种。无法解析的消息和未知类型会被忽略。

跨电脑组队时,Halo 实例之间也走这个连接,但用的是团队邀请凭据和另外的消息类型(auth:challenge、federation)。用访问密码登录的客户端收不到这些消息,本页也不涉及。

哪些事件发给谁 ​

每个已鉴权的连接收到的事件分三类:

  1. 订阅了的会话:这个会话的全部事件,包括逐字生成的回复、思考和工具调用。
  2. 所有会话的状态事件:agent:turn-start、agent:complete、agent:error、agent:ask-question、agent:goal-updated 不需要订阅就会发给每个连接,便于显示“谁在运行、谁在等回答”。处理时按 data.conversationId 过滤。
  3. 全局事件:数字人、团队、文件、终端、通知等变化,发给每个连接。

订阅属于连接本身。断线重连后,要重新发送 auth 和每个 subscribe。

conversationId 用哪个:

事件信封 ​

服务端推送的每个事件都是:

json
{ "type": "event", "channel": "agent:message", "data": { } }

会话事件的 data 里总带有 spaceId 和 conversationId,可以据此把事件归到对应的会话和用户。

事件内容可能很敏感

事件会带上文件路径、工具的输入输出、终端里打印的内容和通道配置。只在可信的服务端处理它们,不要原样转发给最终用户的浏览器。

会话事件 ​

频道何时发送data 中的主要字段
agent:turn-start一轮任务开始type: "turn-start"
agent:message回复文本:新文本块开始、逐段增量、文本块结束、整轮最终回复delta、content、isStreaming、isNewTextBlock、isComplete,见拼接回复文本
agent:thought一个思考块或工具调用块开始,或一条完整的中间步骤thought:id、type(thinking、text、tool_use、tool_result、system、result、error)、content、timestamp,以及可选的 toolName、toolInput、toolOutput、isError
agent:thought-delta思考内容的增量,或某个思考块、工具输入、工具结果完成thoughtId,以及 delta,或 content 加 isComplete,或 toolInput、toolResult
agent:tool-call一次工具调用的参数已完整id、name、status: "running"、input
agent:tool-result工具调用返回toolId、result(字符串)、isError
agent:ask-question这一轮停下来向用户提问id、questions:每个问题有 question、header、options(label、description)和 multiSelect
agent:complete一轮结束,无论完成、失败还是被停止duration(毫秒),可选 tokenUsage:inputTokens、outputTokens、cacheReadTokens、cacheCreationTokens、totalCostUsd、contextWindow
agent:error出错error,可选 errorCode;errorType 为 interrupted 表示被中断,为 working_dir_unavailable 表示工作目录不可用(附 workDirIssue)
agent:session-info会话初始化slashCommands、skills、agents(都是字符串数组)
agent:compact上下文被压缩trigger(manual 或 auto)、preTokens
agent:goal-updated会话目标被设置或更新goal(objective、doneWhen、status 等,或 null)、source、seenByModel
agent:api-retry模型接口出错后开始重试,以及重试结束retry:attempt、maxRetries、delayMs、errorStatus、errorKind;结束时为 null
agent:input-arrived用户或即时通讯消息被记录进会话messages、live
agent:input-stopped停止操作作废了尚未处理的输入messages
toolsets:changed会话的某项能力被打开或关闭toolsetId、action(open 或 close)、openedBy、open(当前打开的能力)
toolsets:requestedAI 请用户打开某项能力toolsetId、displayName

拼接回复文本 ​

agent:message 按以下规则组成一轮的回复:

  1. 带 delta 的事件:把 delta 追加到当前文本。
  2. 不带 delta 的事件:用 content 替换当前文本。isNewTextBlock: true 的事件 content 为空,表示新的文本块开始。
  3. isComplete: true 的事件:content 就是这一轮的完整回复。
  4. 随后的 agent:complete 表示这一轮结束。

一轮回复的典型事件序列(省略了 spaceId 和 conversationId):

json
{"type":"event","channel":"agent:message","data":{"type":"message","content":"","isComplete":false,"isStreaming":false,"isNewTextBlock":true}}
{"type":"event","channel":"agent:message","data":{"type":"message","delta":"这个工作区里有","isComplete":false,"isStreaming":true}}
{"type":"event","channel":"agent:message","data":{"type":"message","delta":" 3 个文件……","isComplete":false,"isStreaming":true}}
{"type":"event","channel":"agent:message","data":{"type":"message","content":"这个工作区里有 3 个文件……","isComplete":true}}
{"type":"event","channel":"agent:complete","data":{"type":"complete","duration":8421}}

只需要最终答复时,取 isComplete: true 那条的 content;需要打字机效果时,按规则 1 和 2 实时更新显示。

全局事件 ​

数字人 ​

频道何时发送data 中的主要字段
app:status_changed数字人的运行状态变化appId、state(status、runningCount、nextRunAtMs、lastError 等)
app:list_changed数字人被安装或卸载appId、change(installed 或 uninstalled)
app:deleted数字人被永久删除appId
app:activity_entry:new新增或更新一条活动记录appId、entry(id、runId、type、ts、content)
app:escalation:new数字人在运行中提出问题,等人回答appId、entryId、question、choices;团队场景还有 teamId、epochId
app:escalation:resolved问题已被回答appId、entryId、response(choice、text、answers)
app:im-session-updated数字人的某个聊天线程有新消息或变化appId、channel、chatId、chatType、instanceId、lastMessage(截取前 50 个字符)

团队 ​

频道何时发送data 中的主要字段
team:updated团队、成员或运行状态变化teamId,以及 team、changed、liveStatus、removed 等
team:blackboard看板新增任务、发现或活动teamId、epochId、kind(task、finding、activity)
team:message成员之间发了一条消息(只含元数据,不含正文)teamId、epochId、fromAppId、toAppId、messageId、ts
team:presence跨电脑团队中电脑的在线状态变化teamId、nodes
team:office-status跨电脑团队暂停、恢复或需要更新等teamId、kind
team:member-history某位成员的聊天记录有更新,客户端应重新读取teamId、appId、epochId

文件、知识库与系统 ​

频道何时发送data 中的主要字段
artifact:tree-update工作区文件树的某些目录有变化spaceId、updatedDirs
artifact:changed-batch一批文件被新建、修改或删除spaceId、changes(type、path、relativePath)
tlon:ingest-progress知识库索引进度kbId、total、completed、phase、current、errors
tlon:stats-updated知识库统计更新kbId、stats
task:state_changed任务面板的数据变化,没有负载,请重新读取—
notification:toastHalo 显示一条通知title、body、variant、duration,以及相关的 appId、teamId 等
agent:mcp-statusMCP 服务器状态变化servers(name、status、tools、error 等)、timestamp
ai-sources:catalog-changed模型来源的模型列表变化sourceIds
credential:decrypt-failed有凭据无法解密,需要用户重新填写failures(path、label),不含密文
im-channels:instance-updated消息通道的配置被更新instanceId、instance(完整的通道配置)
app:memory-pressure内存压力级别变化level(normal、low、critical)
ai-browser:active-view-changedAI 浏览器当前显示的页面变化conversationId、spaceId、viewId、url、title
ai-browser:view-goneAI 浏览器的一个页面被关闭viewId
ai-browser:conversation-released某个会话释放了它的 AI 浏览器conversationId

终端实时流 ​

终端会话的输出用 terminal:data 事件推送给每个连接:data 里有 sessionId 和一段原始终端输出(含 ANSI 转义序列)。terminal:lifecycle 报告会话的创建、退出、标题变化和移除,data 里有 sessionId、type 和 info。终端只在 macOS 和 Windows 上提供。

  • 输入:发送 terminal-input,data 是原始按键,\r 表示回车。它和 POST /api/terminal/input 效果相同,但每次按键不用发一个 HTTP 请求。
  • 调整大小:发送 terminal-resize,cols 和 rows 是正整数。
  • 流量控制:发送 terminal-attach 后,这个连接成为该会话的查看者,必须在渲染输出后用 terminal-ack 报告已经渲染的字符数。查看者积压的未确认输出太多时,终端会暂停输出,直到最慢的查看者跟上。不需要流量控制时不要发送 terminal-attach;用完后发送 terminal-detach。连接断开时会自动退出。
  • 补齐历史:连接之前已经输出的内容用 POST /api/terminal/replay 读取。

终端输出可能包含 shell 里打印的任何内容,包括密钥。

心跳与重连 ​

  • 服务端每 15 秒发送一次 WebSocket 协议层的 ping,没有回应 pong 的连接会被断开。常见的 WebSocket 客户端会自动回应。
  • 应用层可以发送 { "type": "ping" },服务端回复 { "type": "pong" }。
  • 断线后用退避间隔重连(Halo 网页版从 1 秒开始,每次加倍,最长 30 秒),然后重新鉴权并重新订阅。

完整示例 ​

下面的 Node.js 脚本(Node.js 22 及以上自带 WebSocket)鉴权、订阅一个会话,并实时打印回复:

js
const conversationId = process.argv[2]
const ws = new WebSocket(process.env.HALO_URL.replace(/^http/, 'ws') + '/ws')
let text = ''

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', payload: { token: process.env.HALO_TOKEN } }))
}

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  if (msg.type === 'auth:failed') {
    console.error('Access password rejected')
    process.exit(1)
  }
  if (msg.type === 'auth:success') {
    ws.send(JSON.stringify({ type: 'subscribe', payload: { conversationId } }))
    return
  }
  if (msg.type !== 'event' || msg.data.conversationId !== conversationId) return

  if (msg.channel === 'agent:message') {
    if (typeof msg.data.delta === 'string') {
      text += msg.data.delta
      process.stdout.write(msg.data.delta)
    } else {
      text = msg.data.content ?? ''
    }
  }
  if (msg.channel === 'agent:complete') {
    console.log('\n[done]')
    ws.close()
  }
  if (msg.channel === 'agent:error') {
    console.error('\n[error]', msg.data.error)
    ws.close()
  }
}

运行前按快速上手设置 HALO_URL 和 HALO_TOKEN,再执行 node watch.mjs <CONVERSATION_ID>。

基于 MIT 协议开源 · Open source under the MIT license