Files
Genarrative/backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

11 KiB
Raw Blame History

M0SSE 接口与事件格式冻结基线

日期:2026-04-20

依据来源:

1. 文档目的

这份文档用于完成 M0 的第四条任务:

  • 整理当前所有 SSE 接口与事件格式

这里的“整理”不是只记住有几条 stream 路由,而是要求后续 Axum 重写必须先冻结:

  1. 当前到底有哪几条 SSE 路由。
  2. 每条路由是“透传上游流”还是“项目自定义事件流”。
  3. 每条路由的事件名、结束标记、错误帧和头部约束是什么。
  4. 哪些流的 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

冻结总数:

  1. SSE 接口:6
  2. 上游透传型:3
  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 当前已耗时毫秒数 在准备响应头时写入。

额外冻结约束:

  1. SSE 接口当前也保留普通 API 元数据头,不能因为换成 Axum 就丢掉。
  2. 6 条流式接口都在 requireAuth 之后注册,因此第一阶段默认仍需要 Bearer JWT

4. 协议分型

4.1 上游透传型 SSE3 条)

包含:

  1. POST /api/runtime/chat/character/reply/stream
  2. POST /api/runtime/chat/npc/dialogue/stream
  3. POST /api/runtime/chat/npc/recruit/stream

当前实现特征:

  1. 路由不自己重写事件名,直接把上游模型返回的 SSE 原样管道转发给前端。
  2. 本地只负责:
    • 发起上游流式请求
    • 准备 SSE 头部
    • 处理中断时的请求 abort
  3. llmClient.streamMessageContent(...) 的解析逻辑可以反推,当前上游 SSE 采用 OpenAI 风格:
    • 多个 data: {...} JSON chunk
    • 最终 data: [DONE]

冻结要求:

  1. 第一阶段 Axum 仍要保持这三条接口的“上游透传”语义。
  2. 不要在未发版变更协议前,擅自把它们改成项目自定义 event: reply_delta 格式。

4.2 项目自定义 SSE3 条)

包含:

  1. POST /api/runtime/chat/npc/turn/stream
  2. GET /api/runtime/custom-world/sessions/:sessionId/generate/stream
  3. POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream

当前实现特征:

  1. 路由或 orchestrator 自己写 event:data:
  2. 事件名不是上游协议,而是项目本地约定。
  3. 这三条流的结束方式并不一致,必须分别兼容。

5. 各接口事件格式冻结

5.1 runtime.characterReplyStream

路径:

  • POST /api/runtime/chat/character/reply/stream

冻结格式:

  1. 当前为上游透传流。
  2. 本地不保证固定 event 名。
  3. 前端实际收到的是上游 data: {...} chunk 与最终 data: [DONE]
  4. 失败时当前实现也不是本地 event: error,而是由上游失败或 Express 错误链决定。

5.2 runtime.npcDialogueStream

路径:

  • POST /api/runtime/chat/npc/dialogue/stream

冻结格式:

  1. 当前为上游透传流。
  2. 协议特征与 runtime.characterReplyStream 相同。
  3. 第一阶段不能私自改成项目自定义事件名。

5.3 runtime.npcRecruitStream

路径:

  • POST /api/runtime/chat/npc/recruit/stream

冻结格式:

  1. 当前为上游透传流。
  2. 协议特征与前两条透传 SSE 相同。
  3. 结束标记仍依赖上游 data: [DONE]

5.4 runtime.npcTurnStream

路径:

  • POST /api/runtime/chat/npc/turn/stream

成功事件序列:

  1. event: reply_delta
  2. event: reply_delta
  3. ...
  4. event: complete
  5. data: [DONE]

错误事件:

  1. event: error
  2. data: {"message":"..."}
  3. 之后直接 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 } 仅错误消息,无额外状态。

补充冻结点:

  1. reply_delta.text 每次都是当前累计回复全文。
  2. complete.suggestions 在强制收束场景下可能是空数组。
  3. complete.chatDirective 当前至少可能包含:
    • turnLimit
    • remainingTurns
    • forceExit
    • closingMode
  4. complete.pendingQuestOffer 当前可能包含:
    • quest
    • introText

5.5 runtime.customWorldSessionGenerateStream

路径:

  • GET /api/runtime/custom-world/sessions/:sessionId/generate/stream

成功事件序列:

  1. event: progresspayload{ "phase": "preparing", "progress": 10 }
  2. event: progresspayload{ "phase": "requesting_llm", "progress": 45 }
  3. event: progresspayloadCustomWorldGenerationProgress
  4. ...
  5. event: progresspayload{ "phase": "completed", "progress": 100 }
  6. event: result
  7. event: done

错误事件:

  1. event: error
  2. data: {"message":"..."}
  3. 之后直接结束,不会再发 done

冻结 payload 规则:

事件名 payload 结构 关键说明
progress 兼容两种结构 这是当前最容易踩坑的混合协议。
result { "profile": object } 返回完整世界 profile。
done { "ok": true } 当前没有 [DONE] 字符串终止帧。
error { "message": string } 当前也没有额外错误码。

progress 事件的两种冻结结构:

  1. 启动/收尾帧:
    • { "phase": "preparing", "progress": 10 }
    • { "phase": "requesting_llm", "progress": 45 }
    • { "phase": "completed", "progress": 100 }
  2. 编排器进度帧 CustomWorldGenerationProgress
    • phaseId
    • phaseLabel
    • phaseDetail
    • overallProgress
    • completedWeight
    • totalWeight
    • elapsedMs
    • estimatedRemainingMs
    • activeStepIndex
    • steps

补充冻结点:

  1. 当前 progress 不是单一 schema而是混合 schema。
  2. 当前实现会在客户端断开时触发 AbortController,这条流具备显式中断处理。

5.6 runtime.customWorldAgentStreamMessage

路径:

  • POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream

成功事件序列:

  1. event: reply_delta
  2. event: reply_delta
  3. ...
  4. event: session
  5. event: done

错误事件:

  1. event: error
  2. data: {"message":"..."}
  3. 之后直接结束,不会再补 done

冻结 payload 规则:

事件名 payload 结构 关键说明
reply_delta { "text": string } 当前也是累计文本,不是 diff patch。
session { "session": CustomWorldAgentSessionSnapshot } 完整会话快照一次性回推。
done { "ok": true } 当前没有 [DONE]
error { "message": string } 仅错误消息。

补充冻结点:

  1. 这条流当前不会在成功结尾补发最终文本帧,只会发 session 快照。
  2. reply_delta.text 同样是“到当前为止的完整回复”。
  3. 当前实现没有像 customWorldSessionGenerateStream 那样显式挂请求断开 abort。

6. 第一阶段 Axum 重写必须兼容的硬约束

后续重写中,不允许出现以下情况:

  1. 把当前 6 条 SSE 路由减少、合并或改掉方法类型。
  2. 把透传型 3 条流直接改写成自定义事件名,而前端却不知情。
  3. npcTurnStreamreply_delta 从“累计文本”改成“真正 delta”导致前端拼接方式失效。
  4. customWorldSessionGenerateStream 的混合 progress schema 静默改成新格式,却没有版本门禁。
  5. customWorldAgentStreamMessagesession 终帧改成局部 patch而前端仍按完整快照消费。
  6. 丢失 x-request-idx-api-versionx-route-versionx-response-time-ms 等当前前端与联调用到的头。

7. 本任务完成定义

当以下条件成立时,这条任务视为完成:

  1. 当前 6 条 SSE 接口已经有书面冻结清单。
  2. 每条 SSE 都已明确:
    • 方法与路径
    • 协议类型
    • 事件名
    • 成功结束标记
    • 错误事件
    • 关键 payload 结构
  3. 后续 Axum SSE 落地、前端 contract 回归、SpacetimeDB 实时链路设计时,可以直接引用本文,不再靠人工回忆事件名。