feat: checkpoint m5 and bootstrap m6 asset flow
This commit is contained in:
@@ -4,16 +4,16 @@
|
||||
|
||||
## 0. 文档目标
|
||||
|
||||
本文件只冻结 `M4` 当前下一条最小可落地兼容桥:
|
||||
本文件冻结 `M4` 当前 runtime story compat bridge 的实际已落地边界:
|
||||
|
||||
**先把 Rust `api-server` 侧旧 `runtime story state` 兼容返回所需的 DTO 与状态桥边界冻结清楚,再进入 Axum handler 与状态编译迁移。**
|
||||
**Rust `api-server` 已承接旧 `runtime/story/*` 兼容接口,但当前仍属于“快照桥 + 确定性兼容动作”阶段,不等价于最终 SpacetimeDB 真相 story reducer。**
|
||||
|
||||
当前仓库已经有两条并行现实:
|
||||
|
||||
1. `server-node` 侧旧兼容接口 `POST /api/runtime/story/state/resolve` 仍然在真实服务前端。
|
||||
2. `server-rs` 侧已经有 `story_session / battle_state / npc battle / inventory state` 等真相态接口,但还没有编译成旧前端消费的 `RuntimeStoryActionResponse`。
|
||||
|
||||
因此,本轮不直接宣称“runtime story 已迁完”,而是先把兼容桥 contract 冻结为下一段可编码的工程基线。
|
||||
因此,本文档既记录当前兼容桥为什么存在,也明确它的已完成能力和仍未替换掉的真相态缺口。
|
||||
|
||||
---
|
||||
|
||||
@@ -74,19 +74,19 @@
|
||||
|
||||
## 2. 本轮冻结范围
|
||||
|
||||
本轮只冻结以下兼容桥边界:
|
||||
本轮实际已落地并冻结以下兼容桥边界:
|
||||
|
||||
1. Rust `shared-contracts` 新增旧 `runtime story` 兼容响应 DTO
|
||||
2. Rust `shared-contracts` 新增 `POST /api/runtime/story/state/resolve` 的最小请求 DTO
|
||||
3. 明确 Rust 侧第一段只先承接“状态查询兼容桥”
|
||||
4. 明确 `actions/resolve`、`initial`、`continue` 继续后置
|
||||
2. Rust `shared-contracts` 新增 `POST /api/runtime/story/state/resolve` / `POST /api/runtime/story/actions/resolve` / `POST /api/runtime/story/initial` / `POST /api/runtime/story/continue` 所需请求 DTO
|
||||
3. Rust `api-server` 已挂出全部旧 runtime story 兼容接口
|
||||
4. 明确当前实现仍以 `runtime_snapshot` 为状态真相来源,而不是新的 `resolve_story_action` reducer
|
||||
|
||||
本轮明确不做:
|
||||
本轮明确仍未做:
|
||||
|
||||
1. 不在 `server-rs` 里直接落完整 `resolve_story_action`
|
||||
2. 不迁移 Node 侧全部 story 行为决策
|
||||
3. 不把 `runtime snapshot` 正式持久化真相一次性迁到 Rust
|
||||
4. 不在本轮让前端切到 Rust `api-server`
|
||||
3. 不把 `runtime snapshot projection` 一次性改成全量新真相模型
|
||||
4. 不在本文里宣称前端默认流量已经切到 Rust `api-server`
|
||||
|
||||
---
|
||||
|
||||
@@ -183,6 +183,81 @@
|
||||
|
||||
这与当前 Node `getRuntimeStoryState(...)` 的行为一致,不需要在状态查询时伪造 patch。
|
||||
|
||||
### 4.2.2 `actions/resolve` 首版策略
|
||||
|
||||
当前 Rust compat handler 已按“确定性兼容动作 + snapshot 回写”落地,目标是先覆盖前端实际点击主链,而不是一步到位复刻 Node 全部 story domain。
|
||||
|
||||
当前已覆盖动作:
|
||||
|
||||
1. `story_continue_adventure`
|
||||
2. `story_opening_camp_dialogue`
|
||||
3. `camp_travel_home_scene`
|
||||
4. `idle_call_out`
|
||||
5. `idle_explore_forward`
|
||||
6. `idle_observe_signs`
|
||||
7. `idle_rest_focus`
|
||||
8. `idle_travel_next_scene`
|
||||
9. `npc_preview_talk`
|
||||
10. `npc_chat`
|
||||
11. `npc_help`
|
||||
12. `npc_leave`
|
||||
13. `npc_fight`
|
||||
14. `npc_spar`
|
||||
15. `npc_recruit`
|
||||
16. `battle_attack_basic`
|
||||
17. `battle_use_skill`
|
||||
18. `battle_all_in_crush`
|
||||
19. `battle_escape_breakout`
|
||||
20. `battle_feint_step`
|
||||
21. `battle_finisher_window`
|
||||
22. `battle_guard_break`
|
||||
23. `battle_probe_pressure`
|
||||
24. `battle_recover_breath`
|
||||
25. `treasure_secure`
|
||||
26. `treasure_inspect`
|
||||
27. `treasure_leave`
|
||||
|
||||
统一规则:
|
||||
|
||||
1. 请求带 `snapshot` 时先写入 `runtime_snapshot`
|
||||
2. 请求不带 `snapshot` 时回退读取持久化 `runtime_snapshot`
|
||||
3. `clientVersion` 与 `gameState.runtimeActionVersion` 不一致时返回 `409`
|
||||
4. 动作成功后递增 `runtimeActionVersion`
|
||||
5. 追加 `storyHistory`,并把新的 `currentStory` / `viewModel` / `presentation` / `patches` 回写到 snapshot
|
||||
|
||||
当前已额外对齐的 Node 旧主链细节:
|
||||
|
||||
1. `npc_chat`
|
||||
- 已从最初的固定 `+1 affinity` 修正为 Node 旧规则 `max(2, 6 - chattedCount)`
|
||||
- 例如 `chattedCount = 0` 时首聊会从 `46 -> 52`
|
||||
2. `npc_help`
|
||||
- 已改为一次性援手
|
||||
- 成功时恢复 `10 HP / 8 Mana`
|
||||
- 同时关系 `+4`
|
||||
- 二次调用返回错误
|
||||
3. `npc_recruit`
|
||||
- 已要求 `affinity >= 60`
|
||||
- 当前队伍满员时必须提交 `releaseNpcId`
|
||||
- 当前 compat bridge 也会把换队结果写回 `companions`
|
||||
4. quest compat 主循环
|
||||
- 已补 `npc_chat_quest_offer_view / replace / abandon`
|
||||
- 已补 `npc_quest_accept / npc_quest_turn_in`
|
||||
- `pendingQuestOffer.quest` 会继续写回 `currentStory.npcChatState`
|
||||
- quest offer 选项会继续携带前端面板依赖的 `runtimePayload.npcChatQuestOfferAction`
|
||||
5. `npc_quest_turn_in`
|
||||
- quest 不再被直接从快照中移除,而是保留为 `status = turned_in`
|
||||
- 当前最小奖励闭环已写回 `playerCurrency / playerInventory / playerProgression / npc affinity`
|
||||
- `playerProgression` 当前仍走 compat 侧确定性经验累计,不等价于最终 SpacetimeDB 真相成长链
|
||||
|
||||
### 4.2.3 `initial` / `continue` 首版策略
|
||||
|
||||
当前 Rust compat handler 已提供稳定 `RuntimeStoryAiResponse`:
|
||||
|
||||
1. 优先复用 `requestOptions.availableOptions / optionCatalog`
|
||||
2. 未配置 `platform-llm` 时返回确定性 fallback `storyText`
|
||||
3. 已配置 `platform-llm` 时,允许基于同一请求载荷生成增强版文本
|
||||
4. 当前 `encounter` 仍返回 `null`
|
||||
|
||||
---
|
||||
|
||||
## 5. DTO 分层
|
||||
@@ -223,15 +298,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 6. 第一段工程落地顺序
|
||||
## 6. 当前已落地工程顺序
|
||||
|
||||
建议直接按下面顺序编码:
|
||||
本轮实际完成顺序:
|
||||
|
||||
1. `shared-contracts` 新增 `runtime_story.rs`
|
||||
2. 为 `RuntimeStoryStateResolveRequest / RuntimeStoryActionResponse` 补 camelCase 序列化测试
|
||||
3. `docs/technical/README.md` 与 `shared-contracts/README.md` 更新索引
|
||||
4. `backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md` 追加当前冻结进展
|
||||
5. 下一轮再进入 `api-server` 的 `state/resolve` handler 与兼容 compiler
|
||||
3. 恢复并重建 `api-server/src/runtime_story.rs`
|
||||
4. 接入 `state/resolve`、`GET state`、`actions/resolve`、`initial`、`continue`
|
||||
5. 复用 `runtime_save` 的 SpacetimeDB snapshot 持久化链
|
||||
6. 执行 `cargo test -p shared-contracts`
|
||||
7. 执行 `cargo check -p api-server`
|
||||
8. 执行 `cargo test -p api-server runtime_story`
|
||||
9. 继续把 Node 旧 route boundary 回归平移到 Rust:
|
||||
- `runtime_story_routes_resolve_through_rust_route_boundary`
|
||||
- `runtime_story_action_resolve_rejects_client_version_conflict`
|
||||
10. 继续补关键 NPC compat 行为回归:
|
||||
- `runtime_story_npc_help_is_one_shot_and_restores_resources`
|
||||
- `runtime_story_npc_recruit_requires_threshold_and_release_target_when_party_full`
|
||||
11. 继续补 quest compat 回归:
|
||||
- `runtime_story_quest_offer_replace_updates_pending_offer_and_payload`
|
||||
- `runtime_story_quest_offer_abandon_clears_pending_offer_and_restores_chat_options`
|
||||
- `runtime_story_quest_accept_writes_quest_runtime_stats_and_followup_story`
|
||||
- `runtime_story_quest_turn_in_marks_quest_rewards_and_affinity`
|
||||
|
||||
---
|
||||
|
||||
@@ -239,13 +328,14 @@
|
||||
|
||||
以下内容继续明确后置:
|
||||
|
||||
1. `POST /api/runtime/story/actions/resolve` 的请求 DTO 是否直接复用旧 TS contract 全量字段
|
||||
2. `resolve_story_action` 是否拆成:
|
||||
1. `resolve_story_action` 是否拆成:
|
||||
- `resolve_story_action`
|
||||
- `resolve_story_combat_action`
|
||||
- `resolve_story_interaction_action`
|
||||
3. `snapshot` 缺失时是否允许直接从 Rust 真相表完整恢复旧 `currentStory`
|
||||
4. `LLM` 文本续写是在 Rust bridge 内继续调用,还是继续通过 Node 兼容层兜底
|
||||
2. `snapshot` 缺失时是否允许直接从 Rust 真相表完整恢复旧 `currentStory`
|
||||
3. 当前确定性 compat action 何时被真正的 SpacetimeDB story reducer 替换
|
||||
4. `battle / npc / quest / inventory` patch 是否继续细化成与 Node 完全逐字段一致
|
||||
5. `npc_quest_turn_in` 的经验、物品、情报、章节推进何时切换到真正的 SpacetimeDB progression / inventory / quest 真相链,而不是 compat 侧快照写回
|
||||
|
||||
这些边界在状态桥稳定前都不应提前拍死。
|
||||
|
||||
@@ -258,7 +348,20 @@
|
||||
1. 已有独立技术文档冻结 `state/resolve` 兼容桥边界
|
||||
2. `shared-contracts` 已拥有旧 `runtime story` 兼容 DTO
|
||||
3. DTO 字段名与当前前端消费口径保持一致
|
||||
4. `cargo test -p shared-contracts` 通过
|
||||
5. `npm run check:encoding` 通过,确保新增中文文档与 Rust 源文件编码未损坏
|
||||
4. `api-server` 已挂出:
|
||||
- `POST /api/runtime/story/state/resolve`
|
||||
- `GET /api/runtime/story/state/:sessionId`
|
||||
- `POST /api/runtime/story/actions/resolve`
|
||||
- `POST /api/runtime/story/initial`
|
||||
- `POST /api/runtime/story/continue`
|
||||
5. `cargo test -p shared-contracts` 通过
|
||||
6. `cargo check -p api-server` 通过
|
||||
7. `cargo test -p api-server runtime_story` 通过
|
||||
8. `node scripts/check-encoding.mjs` 通过
|
||||
|
||||
达到以上条件后,下一轮即可直接进入 Rust `state bridge compiler` 与 Axum handler 落地。
|
||||
补充边界:
|
||||
|
||||
1. 当前测试里为 `runtime_snapshot` 加了 `#[cfg(test)]` 下的内存兜底,只用于在未启动本地 SpacetimeDB 时稳定验证 Rust route boundary。
|
||||
2. 该测试兜底不进入生产链路,不改变真实 `runtime_save -> spacetime-client -> SpacetimeDB procedure` 的运行时实现。
|
||||
|
||||
达到以上条件后,兼容桥这一段已不再停留在 DTO / 空壳状态;下一轮重点转向“compat bridge 替换成真相态 reducer / projection”。
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# M6 custom world 资产接入 OSS 第一批设计
|
||||
|
||||
日期:`2026-04-22`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 `M6` 第一批 custom world 资产链的真实落地口径。
|
||||
|
||||
本批只解决一个明确问题:
|
||||
|
||||
1. `POST /api/custom-world/scene-image`
|
||||
2. `POST /api/custom-world/cover-image`
|
||||
3. `POST /api/custom-world/cover-upload`
|
||||
|
||||
不再把图片产物写入仓库 `public/` 本地文件,而是统一接到:
|
||||
|
||||
1. `platform-oss::put_object`
|
||||
2. `asset_object`
|
||||
3. `asset_entity_binding`
|
||||
|
||||
形成正式 `OSS + SpacetimeDB 元数据` 真相链。
|
||||
|
||||
## 2. 当前前提
|
||||
|
||||
当前仓库已经具备以下基础能力:
|
||||
|
||||
1. `POST /api/assets/direct-upload-tickets`
|
||||
2. `GET /api/assets/read-url`
|
||||
3. `POST /api/assets/objects/confirm`
|
||||
4. `POST /api/assets/objects/bind`
|
||||
5. `platform-oss::OssClient::put_object`
|
||||
6. `spacetime-module` 中的 `asset_object / asset_entity_binding`
|
||||
|
||||
因此本批不重新设计新资产系统,只复用既有 `assets` 主链。
|
||||
|
||||
## 3. 本批范围
|
||||
|
||||
### 3.1 要完成的内容
|
||||
|
||||
1. custom world 场景图生成结果写入 OSS
|
||||
2. custom world 封面图生成结果写入 OSS
|
||||
3. custom world 封面上传结果写入 OSS
|
||||
4. 每个写入对象都执行一次正式对象确认
|
||||
5. 每个正式对象都绑定到 custom world 业务实体槽位
|
||||
6. 路由响应继续返回旧前端可消费的 `imageSrc`
|
||||
|
||||
### 3.2 本批不解决的内容
|
||||
|
||||
1. 不补 DashScope 图片模型的完整 Rust 编排
|
||||
2. 不补 `cover-upload` 的裁剪、压缩、16:9 强校验全量能力
|
||||
3. 不新增 `scene_image_asset / character_visual_asset` 强业务表
|
||||
4. 不在本批落 `custom_world_asset_link`
|
||||
5. 不把旧前端响应 contract 改成直接返回 OSS URL
|
||||
|
||||
## 4. 业务实体与槽位约定
|
||||
|
||||
本批统一复用通用 `asset_entity_binding`。
|
||||
|
||||
### 4.1 场景图
|
||||
|
||||
| 字段 | 取值 |
|
||||
| --- | --- |
|
||||
| `entity_kind` | `custom_world_landmark` |
|
||||
| `entity_id` | 优先 `landmarkId`,否则回退 `landmarkName` |
|
||||
| `slot` | `scene_image` |
|
||||
| `asset_kind` | `scene_image` |
|
||||
|
||||
### 4.2 封面图
|
||||
|
||||
| 字段 | 取值 |
|
||||
| --- | --- |
|
||||
| `entity_kind` | `custom_world_profile` |
|
||||
| `entity_id` | 优先 `profileId`,否则回退世界 `id/name` |
|
||||
| `slot` | `cover` |
|
||||
| `asset_kind` | `custom_world_cover` |
|
||||
|
||||
补充口径:
|
||||
|
||||
1. 绑定幂等键仍是 `entity_kind + entity_id + slot`
|
||||
2. 同一 profile 重复生成/上传封面时,允许覆盖到最新对象
|
||||
3. 同一 landmark 重复生成场景图时,允许覆盖到最新对象
|
||||
|
||||
## 5. OSS 对象键与返回 contract
|
||||
|
||||
### 5.1 对象键
|
||||
|
||||
场景图固定写入:
|
||||
|
||||
`generated-custom-world-scenes/{profileSegment}/{landmarkSegment}/{assetId}/scene.{ext}`
|
||||
|
||||
封面图固定写入:
|
||||
|
||||
`generated-custom-world-covers/{profileSegment}/{assetId}/cover.{ext}`
|
||||
|
||||
### 5.2 返回 contract
|
||||
|
||||
路由响应继续沿用旧前端使用的字段:
|
||||
|
||||
1. `imageSrc`
|
||||
2. `assetId`
|
||||
3. `sourceType`
|
||||
4. `model`
|
||||
5. `size`
|
||||
6. `taskId`
|
||||
7. `prompt`
|
||||
8. `actualPrompt`
|
||||
|
||||
其中:
|
||||
|
||||
1. `imageSrc` 固定返回 `legacyPublicPath`,也就是旧 `/generated-*` 路径
|
||||
2. 前端若要真正读取私有 OSS 对象,仍必须通过 `GET /api/assets/read-url` 换签名读 URL
|
||||
3. 不直接把 `signedUrl` 塞进 custom world 业务返回,避免把短期读签名误存成长期业务字段
|
||||
|
||||
## 6. 服务端执行顺序
|
||||
|
||||
每次 custom world 图片产出固定执行以下顺序:
|
||||
|
||||
1. 生成或接收图片字节
|
||||
2. 调 `platform-oss::put_object`
|
||||
3. 通过 `HEAD Object` 真值确认对象
|
||||
4. 写入 `asset_object`
|
||||
5. 写入 `asset_entity_binding`
|
||||
6. 返回 `legacyPublicPath`
|
||||
|
||||
注意:
|
||||
|
||||
1. `put_object` 成功不代表已完成正式落库
|
||||
2. `asset_object` 仍必须经过确认链路写入
|
||||
3. 业务引用真相以 `asset_entity_binding` 为准,不以 OSS 上是否存在 key 为准
|
||||
|
||||
## 7. 与 M5 的衔接
|
||||
|
||||
`M5` 为保证前端不断链,曾允许 `scene-image / cover-image / cover-upload` 先写本地 `public/`。
|
||||
|
||||
从本批开始,这个临时口径失效,统一改为:
|
||||
|
||||
1. 二进制对象只进 OSS
|
||||
2. 元数据只进 `asset_object`
|
||||
3. 业务槽位只进 `asset_entity_binding`
|
||||
|
||||
这样 `Stage9` 的兼容路由就不会继续偏离 `M6` 正式资产主链。
|
||||
|
||||
## 8. 完成定义
|
||||
|
||||
当以下条件满足时,本批视为完成:
|
||||
|
||||
1. custom world 三条图片兼容路由不再写本地 `public/`
|
||||
2. 路由成功返回的 `imageSrc` 全部来自 `OSS legacyPublicPath`
|
||||
3. 每次成功写图后都能在 SpacetimeDB 中形成 `asset_object`
|
||||
4. 每次成功写图后都能形成对应 `asset_entity_binding`
|
||||
5. 旧前端仍可继续使用返回的 `/generated-*` 路径配合读签名服务显示图片
|
||||
|
||||
## 9. 关联文档
|
||||
|
||||
1. [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md)
|
||||
2. [ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](./ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
|
||||
3. [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md)
|
||||
4. [SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md)
|
||||
@@ -42,8 +42,10 @@
|
||||
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md):冻结 `M5` Agent session create / snapshot 的最小 SpacetimeDB 与 Axum facade 闭环,明确本轮不迁移 LLM、SSE、卡片更新和完整 action registry。
|
||||
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md):冻结 `M5` Agent `message submit / operation query` 的 deterministic 最小闭环,明确同步写入 user/assistant 消息、`process_message` operation 与 session 进度推进规则。
|
||||
- [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md):冻结 `M5` Agent `/messages/stream` 的最小兼容 SSE facade,明确复用 Stage 7 的同步写表逻辑,只输出当前前端真实消费的 `reply_delta / session / done / error` 事件。
|
||||
- [RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md](./RUST_API_SERVER_SSE_INFRASTRUCTURE_DESIGN_2026-04-22.md):冻结 `server-rs/crates/api-server` 内部 SSE 基础设施抽取口径,统一 Rust 侧 `text/event-stream` 响应头、事件编码与缓冲式 SSE 输出 helper。
|
||||
- [SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md):补齐 `M5` Stage 5 遗漏的 owner-only `GET /api/runtime/custom-world-library/:profileId` 设计,冻结单条 profile detail 的 SpacetimeDB procedure、client facade、404 语义与 Axum 路由扩展方式。
|
||||
- [SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md):冻结 `M5` 剩余主链的 works、card detail、publish gate、supportedActions、action registry 与 AI/OSS 兼容路由边界,作为 Stage 9 到收口阶段的统一落地依据。
|
||||
- [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批 custom world 场景图、封面图、封面上传从本地 `public/` 临时落地切到 `OSS + asset_object + asset_entity_binding` 正式真相链的边界与槽位约定。
|
||||
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。
|
||||
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
|
||||
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md):把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Rust `api-server` SSE 基础设施设计(2026-04-22)
|
||||
|
||||
日期:`2026-04-22`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于冻结 `server-rs/crates/api-server` 内部的 SSE 基础设施抽取口径。
|
||||
|
||||
本轮目标只有一个:
|
||||
|
||||
1. 把当前散落在业务 handler 中的 `text/event-stream` 响应头与事件文本编码逻辑,收口为 `api-server` 可复用的 Rust 基础设施。
|
||||
|
||||
本轮不做:
|
||||
|
||||
1. 不改前端消费协议
|
||||
2. 不把 custom world message stream 改成真实逐段 token streaming
|
||||
3. 不引入跨 crate 的共享 `shared-contracts` SSE runtime helper
|
||||
4. 不同时重构 story / runtime / txt mode 的未来流式接口
|
||||
|
||||
## 2. 当前问题
|
||||
|
||||
当前 Rust 侧 SSE 能力只在一个地方手写:
|
||||
|
||||
1. `server-rs/crates/api-server/src/custom_world.rs`
|
||||
|
||||
当前实现存在以下问题:
|
||||
|
||||
1. `append_sse_event(...)` 与 `build_event_stream_response(...)` 直接写在业务文件里
|
||||
2. SSE 响应头、事件编码规则没有统一入口
|
||||
3. 后续如果再新增第二条 Rust SSE 路由,极容易复制一份近似实现
|
||||
4. 业务层和传输层耦合在一起,不利于测试
|
||||
|
||||
## 3. 抽取边界
|
||||
|
||||
本轮只抽以下基础能力:
|
||||
|
||||
1. 标准 SSE 响应头构造
|
||||
2. 单条事件编码
|
||||
3. 缓冲式 SSE body builder
|
||||
4. 一次性返回完整 SSE 文本的响应构造
|
||||
|
||||
本轮明确不抽:
|
||||
|
||||
1. `reply_delta / session / done / error` 这些业务事件名
|
||||
2. 事件发送顺序
|
||||
3. custom world session 的查询与回复文本推导
|
||||
4. 业务错误到 SSE `error` 事件的映射策略
|
||||
|
||||
原因固定如下:
|
||||
|
||||
1. 这些内容属于业务协议,而不是通用传输设施
|
||||
2. 当前不同链路未来很可能有不同事件集合
|
||||
3. 先把传输层抽干净,后续真实流式能力才能稳定复用
|
||||
|
||||
## 4. 基础设施 API 口径
|
||||
|
||||
本轮在 `server-rs/crates/api-server/src/sse.rs` 提供:
|
||||
|
||||
1. `SseEventBuffer`
|
||||
- 面向当前最小兼容场景
|
||||
- 内部持有 `String`
|
||||
- 提供 `push_json(event, payload)` 与 `into_response()`
|
||||
2. `build_sse_response(body)`
|
||||
- 统一写入标准 SSE 响应头
|
||||
3. `encode_sse_event(body, event, payload)`
|
||||
- 只负责把事件编码为:
|
||||
```text
|
||||
event: xxx
|
||||
data: {...}
|
||||
|
||||
```
|
||||
|
||||
## 5. 标准响应头
|
||||
|
||||
所有通过本基础设施输出的 SSE 响应,统一包含:
|
||||
|
||||
1. `Content-Type: text/event-stream; charset=utf-8`
|
||||
2. `Cache-Control: no-cache`
|
||||
3. `X-Accel-Buffering: no`
|
||||
|
||||
当前不默认加入:
|
||||
|
||||
1. `Connection: keep-alive`
|
||||
|
||||
原因:
|
||||
|
||||
1. 当前 Rust `axum` 一次性 body 返回场景不依赖显式设置该头
|
||||
2. 保持最小必要头集合,避免提前固化未来长连接策略
|
||||
|
||||
## 6. 与 custom world message stream 的关系
|
||||
|
||||
`POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` 仍然保持 Stage 8 文档冻结的最小语义:
|
||||
|
||||
1. 业务层先完成 deterministic 写表
|
||||
2. 读取最新 session snapshot
|
||||
3. 组装 `reply_delta`
|
||||
4. 组装 `session`
|
||||
5. 组装 `done`
|
||||
6. 一次性返回完整 SSE 文本
|
||||
|
||||
本轮变化只在于:
|
||||
|
||||
1. 事件编码和响应头不再手写在 `custom_world.rs`
|
||||
2. 改由 `sse.rs` 基础设施承接
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
当以下条件满足时,本轮视为完成:
|
||||
|
||||
1. `api-server/src/sse.rs` 已提供可复用 SSE helper
|
||||
2. `custom_world.rs` 不再内联维护 SSE 编码与响应头细节
|
||||
3. `cargo fmt -p api-server` 通过
|
||||
4. `cargo check -p api-server` 通过
|
||||
5. `npm run check:encoding` 通过
|
||||
|
||||
## 8. 一句话结论
|
||||
|
||||
本轮把 Rust `api-server` 里的 SSE 能力收口为“最小传输层基础设施”,统一事件编码与响应头,但不改业务事件协议和当前 custom world 的同步伪流式语义。
|
||||
@@ -832,6 +832,21 @@ workflow-cache/{workflow_type}/{workflow_id}.json
|
||||
1. `editor` 已于 `2026-04-21` 被确认为遗留无用模块,退出本轮 Rust 后端重写范围。
|
||||
2. Phase 5 只覆盖资产与 OSS 主链,不再包含 editor 迁移。
|
||||
|
||||
## Phase 6:联调、回归、部署与切流收口
|
||||
|
||||
交付:
|
||||
|
||||
1. 联调与回归测试体系
|
||||
2. 灰度环境、切流开关、回退方案
|
||||
3. tracing / request id / 关键链路观测
|
||||
4. 拆分 `server-rs/crates/spacetime-module/src/lib.rs`,按业务模块与 SpacetimeDB 的 `table / reducer / procedure / view` 结构重组为 `runtime`、`gameplay::{story/combat/inventory/npc/quest/runtime_item/progression}`、`custom_world`、`asset_metadata`、`ai` 等聚合子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口
|
||||
|
||||
阶段执行补充:
|
||||
|
||||
1. 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。
|
||||
2. 拆分后的目录与模块边界必须对齐 `M0` 已冻结的模块迁移归属,避免 `spacetime-module` 回退成“单大文件 + 单大包”结构。
|
||||
3. 拆分完成后至少要保持 `cargo check`、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。
|
||||
|
||||
## 14. 验收标准
|
||||
|
||||
重写完成至少要满足:
|
||||
|
||||
@@ -331,15 +331,24 @@ session snapshot 中的 `resultPreview` 固定输出:
|
||||
|
||||
#### `scene-image / cover-image`
|
||||
|
||||
1. 当前不直接生成真实图片
|
||||
2. 返回明确 `NOT_IMPLEMENTED` 或最小占位错误会导致前端主链中断
|
||||
3. 因此前端兼容需要的最小可用策略是:创建上传票据或返回可继续上传的对象位置信息
|
||||
1. `M5` 验收时允许先用本地占位产物保证前端主链不断
|
||||
2. 自 `2026-04-22` 的 `M6` 第一批开始,正式口径改为:
|
||||
- `platform-oss::put_object`
|
||||
- `asset_object`
|
||||
- `asset_entity_binding`
|
||||
3. 兼容响应仍返回旧 `/generated-*` 路径,不直接返回裸 OSS URL
|
||||
4. 详细边界见:
|
||||
- [M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
|
||||
|
||||
#### `cover-upload`
|
||||
|
||||
1. 复用 `/api/assets/direct-upload-tickets`
|
||||
2. 生成 OSS 上传票据
|
||||
3. 返回兼容旧前端所需的上传字段
|
||||
1. `M5` 阶段允许先走最小本地上传兼容
|
||||
2. 自 `2026-04-22` 的 `M6` 第一批开始,正式口径与 `cover-image` 一致:
|
||||
- 服务器接收 Data URL
|
||||
- 服务器上传 OSS
|
||||
- 确认 `asset_object`
|
||||
- 绑定 `asset_entity_binding`
|
||||
3. 返回值仍保持旧前端所需的 `imageSrc / assetId / sourceType`
|
||||
|
||||
## 8. crate 级改动范围
|
||||
|
||||
|
||||
Reference in New Issue
Block a user