跳转到内容

实时事件与通知

Chorus 让每个打开的浏览器与人和智能体正在做的事保持同步。每当有变化发生(任务被移动、 想法被认领、提案被批准),该变化会通过 Server-Sent Events(SSE)广播出去,受影响的页面会 自动刷新。智能体不消费该数据流;它们通过通知接收同样的工作。本页记录这两条路径:SSE 端点 及其事件结构,以及智能体(和浏览器客户端)调用的通知 REST API。

每次变更,无论是通过 MCP 工具、REST API 还是 Web 界面做出的,保存后都会发布一条变更事件。SSE 端点 把匹配的事件推送给每个已连接的浏览器,浏览器稍等片刻后重新加载当前页面的数据:

变更已保存(MCP 工具 / REST API / Web 界面)
→ 发布变更事件
→ GET /api/events 按工作区 + 项目过滤
→ 浏览器 EventSource 收到该事件
→ 500ms 后,页面重新加载数据

这种刷新刻意做得粗粒度:单条事件只告诉页面“你正在看的某样东西变了”,随后页面会从服务器 拉取最新数据。任何事件都不携带变更后的记录本身。

有两个端点向浏览器推送事件。二者都是 GET,都通过请求的 Cookie 完成认证, 且响应都不会被缓存。

端点推送内容
GET /api/events?projectUuid=<uuid>实体变更事件,限定在你所在的工作区;提供 projectUuid 时,进一步限定到该项目。
GET /api/events/notifications已认证用户的通知事件,从而让应用内的通知指示器实时更新。

每个响应都带有以下响应头与行为:

  • Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive。
  • 连接建立时,数据流先写入一条 : connected 注释,随后以 data: <json> 帧的形式推送事件。
  • 心跳(Heartbeat): 每 30 秒发送一条 : heartbeat 注释,以便在空闲期与中间层超时的 情况下保持连接打开。
  • 断开时清理: 当客户端中止请求(标签页关闭、页面跳转、网络断开)时,端点会停止推送 并清除心跳定时器。
  • 多租户: 变更数据流会丢弃任何 companyUuid 与调用者不匹配的事件,因此客户端不会 收到其他工作区的活动。

GET /api/events 上的变更事件是结构固定的 JSON 对象:

interface RealtimeEvent {
companyUuid: string; // 多租户隔离——数据流会丢弃不匹配的事件
projectUuid: string; // 用于可选的 projectUuid 过滤
entityType: "task" | "idea" | "proposal" | "document";
entityUuid: string; // 发生变更的实体
action: "created" | "updated" | "deleted";
}

一条事件只说明哪个实体变了、如何变的,从不携带该实体的字段。客户端仅把它当作 刷新信号。

浏览器会向 /api/events?projectUuid=<uuid> 打开一个 EventSource,并在收到任何消息时, 等待 500ms 再刷新页面数据。这段等待会把来自同一个逻辑动作的一连串事件合并为单次刷新(例如 批准一个提案会发出一条提案更新事件,外加每个被创建任务各一条事件)。标签页隐藏时连接会被 关闭,重新可见时会重新打开(并触发一次刷新);离开页面时连接彻底关闭。

通知是智能体与浏览器客户端共用的持久化“轮询并行动”路径。接收者由认证上下文推导 得出:以用户身份认证的请求读取该用户的通知,以智能体 API 密钥认证的请求读取该智能体的通知。 同一组路由服务两者,且没有接收者参数。

方法与路径用途关键参数
GET /api/notifications列出调用者(用户或智能体)的通知。返回 { notifications, unreadCount }。查询参数:limit(1–100,默认 50)、offset(默认 0)、unreadOnly(设为 true 只返回未读)、projectUuid(限定到一个项目)。
GET /api/notifications/unread-count以 { count } 返回调用者的未读数量。无
POST /api/notifications/read-all将调用者的所有通知标记为已读。可选 JSON body { projectUuid },把范围限定到一个项目。
PATCH /api/notifications/[uuid]/read将一条通知标记为已读。路径参数:通知 uuid。
PATCH /api/notifications/[uuid]/archive归档一条通知。路径参数:通知 uuid。
GET /api/notifications/preferences读取调用者的通知偏好。无
PUT /api/notifications/preferences更新调用者的通知偏好。偏好字段的 JSON body。

limit 会在服务端被限制在 1–100 之间,offset 小于 0 时按 0 处理。对不属于调用者的通知执行 标记或归档会返回 404,而不会透露它是否存在。

每个通知 REST 路由都返回标准的 Chorus JSON 信封。成功时把载荷放在 data 之下:

{ "success": true, "data": { "notifications": [], "unreadCount": 0 } }

出错时改为携带一个结构化的 error 对象:

{ "success": false, "error": { "code": "NOT_FOUND", "message": "Notification not found" } }

常见失败使用错误码 NOT_FOUND、BAD_REQUEST、UNAUTHORIZED 和 FORBIDDEN,分别对应 HTTP 状态码 404、400、401、403。没有有效会话或智能体密钥的请求会被拒绝:

{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } }

HTTP 状态码为 401。(SSE 端点不使用这一信封格式:对未认证的请求,它们返回纯文本 Unauthorized 和状态码 401。)

默认情况下,变更事件在内存中传递,这对单实例部署已经足够:处理变更的进程正是持有每个 SSE 连接的 进程,因此在本地发出一条事件即可到达每个已连接的浏览器。

运行超过一个实例会打破这一前提:发生在实例 A 上的变更仍必须到达连接在实例 B 上的浏览器。 对于多实例部署,应通过 Redis pub/sub 投递事件:每个实例订阅一个共享频道,变更 发布到 Redis 而非仅在本地发出,每个实例的 SSE 端点再把收到的内容转发给自己已连接的浏览器。 SSE 端点与浏览器端保持不变,只是事件在实例之间的传递方式不同。

本页只涵盖投递机制。关于哪些事件会生成通知(各个类别、@提及、每类活动会通知谁),以及 接收者如何选择接收或屏蔽,参阅协作参考。