docs: add backend sse interface baseline

This commit is contained in:
2026-04-21 00:08:41 +08:00
parent e6d7df1ecb
commit 0e454b889b
3 changed files with 303 additions and 1 deletions

View File

@@ -10,7 +10,8 @@
交付物:[M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md)
- [x] 整理当前 12 个内部模块并锁定迁移归属
交付物:[M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md)
- [ ] 整理当前所有 SSE 接口与事件格式
- [x] 整理当前所有 SSE 接口与事件格式
交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md)
- [ ] 整理当前所有 `/generated-*` 静态资源前缀
- [ ] 整理当前前端直接依赖的响应头、envelope、错误格式

View File

@@ -0,0 +1,300 @@
# M0SSE 接口与事件格式冻结基线
日期:`2026-04-20`
依据来源:
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json)
- `server-node/src/http.ts`
- `server-node/src/routes/runtimeRoutes.ts`
- `server-node/src/routes/customWorldAgent.ts`
- `server-node/src/modules/ai/chatOrchestrator.ts`
- `server-node/src/services/customWorldAgentOrchestrator.ts`
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
## 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: progress`payload`{ "phase": "preparing", "progress": 10 }`
2. `event: progress`payload`{ "phase": "requesting_llm", "progress": 45 }`
3. `event: progress`payload`CustomWorldGenerationProgress`
4. `...`
5. `event: progress`payload`{ "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.`npcTurnStream``reply_delta` 从“累计文本”改成“真正 delta”导致前端拼接方式失效。
4.`customWorldSessionGenerateStream` 的混合 `progress` schema 静默改成新格式,却没有版本门禁。
5.`customWorldAgentStreamMessage``session` 终帧改成局部 patch而前端仍按完整快照消费。
6. 丢失 `x-request-id``x-api-version``x-route-version``x-response-time-ms` 等当前前端与联调用到的头。
## 7. 本任务完成定义
当以下条件成立时,这条任务视为完成:
1. 当前 `6` 条 SSE 接口已经有书面冻结清单。
2. 每条 SSE 都已明确:
- 方法与路径
- 协议类型
- 事件名
- 成功结束标记
- 错误事件
- 关键 payload 结构
3. 后续 Axum SSE 落地、前端 contract 回归、SpacetimeDB 实时链路设计时,可以直接引用本文,不再靠人工回忆事件名。

View File

@@ -17,6 +17,7 @@
- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md):当前 Node 后端 `6` 个挂载面的冻结基线,用于后续接口映射、模块迁移与验收对照。
- [M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md):当前 `96` 条后端路由的“旧接口 -> 新实现”迁移矩阵,用于 Axum 路由树和 application service 落位。
- [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md):当前 `12` 个内部模块的迁移归属基线,用于锁定 Rust crate、SpacetimeDB bounded context 与 Axum/application 分工。
- [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md):当前 `6` 条 SSE 接口及其事件格式冻结基线,用于 Axum SSE 兼容和前端 contract 回归。
## 维护规则