11 KiB
11 KiB
M0:SSE 接口与事件格式冻结基线
日期:2026-04-20
依据来源:
- ../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md
- ../server-node/manifests/backend-capability-index.json
server-node/src/http.tsserver-node/src/routes/runtimeRoutes.tsserver-node/src/routes/customWorldAgent.tsserver-node/src/modules/ai/chatOrchestrator.tsserver-node/src/services/customWorldAgentOrchestrator.tsserver-node/src/modules/ai/customWorldOrchestrator.ts
1. 文档目的
这份文档用于完成 M0 的第四条任务:
- 整理当前所有 SSE 接口与事件格式
这里的“整理”不是只记住有几条 stream 路由,而是要求后续 Axum 重写必须先冻结:
- 当前到底有哪几条 SSE 路由。
- 每条路由是“透传上游流”还是“项目自定义事件流”。
- 每条路由的事件名、结束标记、错误帧和头部约束是什么。
- 哪些流的
payload是增量文本,哪些其实是“累计文本”。
2. 冻结结论
当前 Node 后端正式登记的 SSE 接口固定为以下 6 条:
| 路由 ID | 方法/路径 | 当前实现入口 | 协议类型 | 成功结束标记 | 鉴权 |
|---|---|---|---|---|---|
runtime.characterReplyStream |
POST /api/runtime/chat/character/reply/stream |
runtimeRoutes.ts -> streamCharacterChatReplyFromOrchestrator |
上游透传 SSE | 上游 data: [DONE] |
JWT |
runtime.npcDialogueStream |
POST /api/runtime/chat/npc/dialogue/stream |
runtimeRoutes.ts -> streamNpcChatDialogueFromOrchestrator |
上游透传 SSE | 上游 data: [DONE] |
JWT |
runtime.npcRecruitStream |
POST /api/runtime/chat/npc/recruit/stream |
runtimeRoutes.ts -> streamNpcRecruitDialogueFromOrchestrator |
上游透传 SSE | 上游 data: [DONE] |
JWT |
runtime.npcTurnStream |
POST /api/runtime/chat/npc/turn/stream |
runtimeRoutes.ts -> streamNpcChatTurnFromOrchestrator |
项目自定义 SSE | event: complete 后追加 data: [DONE] |
JWT |
runtime.customWorldSessionGenerateStream |
GET /api/runtime/custom-world/sessions/:sessionId/generate/stream |
runtimeRoutes.ts 内联实现 |
项目自定义 SSE | event: done,无 [DONE] |
JWT |
runtime.customWorldAgentStreamMessage |
POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream |
customWorldAgent.ts -> customWorldAgentOrchestrator.streamMessage |
项目自定义 SSE | event: done,无 [DONE] |
JWT |
冻结总数:
- SSE 接口:
6 - 上游透传型:
3 - 本地自定义事件流:
3
3. 全部 SSE 接口共享的响应头约束
当前所有项目内主动准备 SSE 响应的接口,都经过 prepareEventStreamResponse(...),因此至少冻结以下头部行为:
| 响应头 | 当前值/规则 | 说明 |
|---|---|---|
Content-Type |
默认 text/event-stream; charset=utf-8 |
透传型接口可被上游 content-type 覆盖,但仍保持 SSE。 |
Cache-Control |
no-cache |
禁止中间层缓存流式结果。 |
Connection |
keep-alive |
保持 SSE 长连接。 |
X-Accel-Buffering |
no |
禁止代理层缓冲。 |
x-request-id |
透传当前请求 ID | 所有 SSE 都要带请求追踪头。 |
x-api-version |
当前 API 版本号 | 与普通 JSON 接口一致。 |
x-route-version |
当前路由版本号 | 与普通 JSON 接口一致。 |
x-response-time-ms |
当前已耗时毫秒数 | 在准备响应头时写入。 |
额外冻结约束:
SSE接口当前也保留普通 API 元数据头,不能因为换成 Axum 就丢掉。- 这
6条流式接口都在requireAuth之后注册,因此第一阶段默认仍需要Bearer JWT。
4. 协议分型
4.1 上游透传型 SSE(3 条)
包含:
POST /api/runtime/chat/character/reply/streamPOST /api/runtime/chat/npc/dialogue/streamPOST /api/runtime/chat/npc/recruit/stream
当前实现特征:
- 路由不自己重写事件名,直接把上游模型返回的 SSE 原样管道转发给前端。
- 本地只负责:
- 发起上游流式请求
- 准备 SSE 头部
- 处理中断时的请求 abort
- 从
llmClient.streamMessageContent(...)的解析逻辑可以反推,当前上游 SSE 采用 OpenAI 风格:- 多个
data: {...}JSON chunk - 最终
data: [DONE]
- 多个
冻结要求:
- 第一阶段 Axum 仍要保持这三条接口的“上游透传”语义。
- 不要在未发版变更协议前,擅自把它们改成项目自定义
event: reply_delta格式。
4.2 项目自定义 SSE(3 条)
包含:
POST /api/runtime/chat/npc/turn/streamGET /api/runtime/custom-world/sessions/:sessionId/generate/streamPOST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream
当前实现特征:
- 路由或 orchestrator 自己写
event:与data:。 - 事件名不是上游协议,而是项目本地约定。
- 这三条流的结束方式并不一致,必须分别兼容。
5. 各接口事件格式冻结
5.1 runtime.characterReplyStream
路径:
POST /api/runtime/chat/character/reply/stream
冻结格式:
- 当前为上游透传流。
- 本地不保证固定
event名。 - 前端实际收到的是上游
data: {...}chunk 与最终data: [DONE]。 - 失败时当前实现也不是本地
event: error,而是由上游失败或 Express 错误链决定。
5.2 runtime.npcDialogueStream
路径:
POST /api/runtime/chat/npc/dialogue/stream
冻结格式:
- 当前为上游透传流。
- 协议特征与
runtime.characterReplyStream相同。 - 第一阶段不能私自改成项目自定义事件名。
5.3 runtime.npcRecruitStream
路径:
POST /api/runtime/chat/npc/recruit/stream
冻结格式:
- 当前为上游透传流。
- 协议特征与前两条透传 SSE 相同。
- 结束标记仍依赖上游
data: [DONE]。
5.4 runtime.npcTurnStream
路径:
POST /api/runtime/chat/npc/turn/stream
成功事件序列:
event: reply_deltaevent: reply_delta...event: completedata: [DONE]
错误事件:
event: errordata: {"message":"..."}- 之后直接
response.end(),不会再补complete
冻结 payload 规则:
| 事件名 | payload 结构 | 关键说明 |
|---|---|---|
reply_delta |
{ "text": string } |
text 实际是“累计文本”,不是单 token 增量。 |
complete |
{ "npcReply": string, "affinityDelta": number, "affinityText": string, "suggestions": string[], "pendingQuestOffer": object | null, "chatDirective": object | null } |
最终一次性返回业务结算数据。 |
error |
{ "message": string } |
仅错误消息,无额外状态。 |
补充冻结点:
reply_delta.text每次都是当前累计回复全文。complete.suggestions在强制收束场景下可能是空数组。complete.chatDirective当前至少可能包含:turnLimitremainingTurnsforceExitclosingMode
complete.pendingQuestOffer当前可能包含:questintroText
5.5 runtime.customWorldSessionGenerateStream
路径:
GET /api/runtime/custom-world/sessions/:sessionId/generate/stream
成功事件序列:
event: progress,payload:{ "phase": "preparing", "progress": 10 }event: progress,payload:{ "phase": "requesting_llm", "progress": 45 }event: progress,payload:CustomWorldGenerationProgress...event: progress,payload:{ "phase": "completed", "progress": 100 }event: resultevent: done
错误事件:
event: errordata: {"message":"..."}- 之后直接结束,不会再发
done
冻结 payload 规则:
| 事件名 | payload 结构 | 关键说明 |
|---|---|---|
progress |
兼容两种结构 | 这是当前最容易踩坑的混合协议。 |
result |
{ "profile": object } |
返回完整世界 profile。 |
done |
{ "ok": true } |
当前没有 [DONE] 字符串终止帧。 |
error |
{ "message": string } |
当前也没有额外错误码。 |
progress 事件的两种冻结结构:
- 启动/收尾帧:
{ "phase": "preparing", "progress": 10 }{ "phase": "requesting_llm", "progress": 45 }{ "phase": "completed", "progress": 100 }
- 编排器进度帧
CustomWorldGenerationProgress:phaseIdphaseLabelphaseDetailoverallProgresscompletedWeighttotalWeightelapsedMsestimatedRemainingMsactiveStepIndexsteps
补充冻结点:
- 当前
progress不是单一 schema,而是混合 schema。 - 当前实现会在客户端断开时触发
AbortController,这条流具备显式中断处理。
5.6 runtime.customWorldAgentStreamMessage
路径:
POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream
成功事件序列:
event: reply_deltaevent: reply_delta...event: sessionevent: done
错误事件:
event: errordata: {"message":"..."}- 之后直接结束,不会再补
done
冻结 payload 规则:
| 事件名 | payload 结构 | 关键说明 |
|---|---|---|
reply_delta |
{ "text": string } |
当前也是累计文本,不是 diff patch。 |
session |
{ "session": CustomWorldAgentSessionSnapshot } |
完整会话快照一次性回推。 |
done |
{ "ok": true } |
当前没有 [DONE]。 |
error |
{ "message": string } |
仅错误消息。 |
补充冻结点:
- 这条流当前不会在成功结尾补发最终文本帧,只会发
session快照。 reply_delta.text同样是“到当前为止的完整回复”。- 当前实现没有像
customWorldSessionGenerateStream那样显式挂请求断开 abort。
6. 第一阶段 Axum 重写必须兼容的硬约束
后续重写中,不允许出现以下情况:
- 把当前
6条 SSE 路由减少、合并或改掉方法类型。 - 把透传型
3条流直接改写成自定义事件名,而前端却不知情。 - 把
npcTurnStream的reply_delta从“累计文本”改成“真正 delta”,导致前端拼接方式失效。 - 把
customWorldSessionGenerateStream的混合progressschema 静默改成新格式,却没有版本门禁。 - 把
customWorldAgentStreamMessage的session终帧改成局部 patch,而前端仍按完整快照消费。 - 丢失
x-request-id、x-api-version、x-route-version、x-response-time-ms等当前前端与联调用到的头。
7. 本任务完成定义
当以下条件成立时,这条任务视为完成:
- 当前
6条 SSE 接口已经有书面冻结清单。 - 每条 SSE 都已明确:
- 方法与路径
- 协议类型
- 事件名
- 成功结束标记
- 错误事件
- 关键 payload 结构
- 后续 Axum SSE 落地、前端 contract 回归、SpacetimeDB 实时链路设计时,可以直接引用本文,不再靠人工回忆事件名。