diff --git a/docs/technical/README.md b/docs/technical/README.md index 115be2a9..935c3c30 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -12,6 +12,7 @@ - [STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md](./STDB_AUTH_TAIL_PHASE1_AUTO_GUEST_CREDENTIAL_REMOVAL_2026-04-20.md):Auth 尾巴清理第一段,删除前端自动游客用户名/密码残留。 - [STDB_AUTH_TAIL_PHASE2_TOKEN_SLOT_SPLIT_2026-04-20.md](./STDB_AUTH_TAIL_PHASE2_TOKEN_SLOT_SPLIT_2026-04-20.md):将 STDB token 与旧 HTTP Bearer token 拆成独立存储槽。 - [RUNTIME_STORY_TO_STDB_PHASE1_TRANSPORT_ABSTRACTION_2026-04-20.md](./RUNTIME_STORY_TO_STDB_PHASE1_TRANSPORT_ABSTRACTION_2026-04-20.md):把 `runtimeStoryService` 改成可替换 transport,为后续 STDB provider 接入预留稳定边界。 +- [RUNTIME_STORY_TO_STDB_PHASE2_CONTRACT_DESIGN_2026-04-20.md](./RUNTIME_STORY_TO_STDB_PHASE2_CONTRACT_DESIGN_2026-04-20.md):梳理 runtime story 从 Express 迁到 STDB 所需的聚合 view、procedure、mapper 与前端 provider 设计。 - [TASK_AUTO_COMMIT_WORKFLOW_2026-04-20.md](./TASK_AUTO_COMMIT_WORKFLOW_2026-04-20.md):任务完成后按文件边界自动提交的脚本与协作约定。 - [NODE_DEV_STARTUP_HOTFIX_2026-04-20.md](./NODE_DEV_STARTUP_HOTFIX_2026-04-20.md):`npm run dev` 启动失败的热修记录、根因与验证结果。 - [NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md](./NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md):当前 Node 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。 diff --git a/docs/technical/RUNTIME_STORY_TO_STDB_PHASE2_CONTRACT_DESIGN_2026-04-20.md b/docs/technical/RUNTIME_STORY_TO_STDB_PHASE2_CONTRACT_DESIGN_2026-04-20.md new file mode 100644 index 00000000..a8ad21f8 --- /dev/null +++ b/docs/technical/RUNTIME_STORY_TO_STDB_PHASE2_CONTRACT_DESIGN_2026-04-20.md @@ -0,0 +1,338 @@ +# Runtime Story 迁移到 STDB Phase 2:后端 Contract 与 Provider 设计(2026-04-20) + +更新时间:`2026-04-20` + +## 1. 本轮定位 + +`Phase 1` 已经把 [`runtimeStoryService.ts`](/home/Genarrative/src/services/runtimeStoryService.ts) 改造成可替换 transport,但当前仓库里还没有真正承接 `runtime story` 的 SpacetimeDB schema / procedure / view。 + +因此本轮不直接写 STDB provider 代码,而是先把 `Phase 2` 需要落地的 contract 补成可直接编码的设计,避免下一轮实现时范围漂移。 + +## 2. 当前真实现状 + +### 2.1 Express 已承接的 runtime story 能力 + +当前 Express 运行时主链位于: + +1. [`storyActionRoutes.ts`](/home/Genarrative/server-node/src/modules/story/storyActionRoutes.ts) +2. [`storyActionService.ts`](/home/Genarrative/server-node/src/modules/story/storyActionService.ts) +3. [`runtimeSession.ts`](/home/Genarrative/server-node/src/modules/story/runtimeSession.ts) + +当前实际对外 contract: + +1. `GET /api/runtime/story/state/:sessionId` +2. `POST /api/runtime/story/actions/resolve` + +统一响应类型来自 [`packages/shared/src/contracts/story.ts`](/home/Genarrative/packages/shared/src/contracts/story.ts): + +1. `RuntimeStoryActionResponse` +2. `RuntimeStoryViewModel` +3. `RuntimeStoryPresentation` +4. `RuntimeStoryPatch` + +这条链路已经承接的业务范围不只是“读一个故事文本”,而是: + +1. NPC 交互 +2. 战斗动作 +3. inventory use +4. quest accept / turn in +5. treasure inspect / secure / leave +6. runtime snapshot version 冲突检查 + +### 2.2 STDB 当前已具备但还不够的能力 + +当前 STDB 已有: + +1. `my_snapshot` view +2. `save_snapshot` / `delete_snapshot` +3. `my_runtime_settings` +4. 一批认证、资料库、浏览历史相关 view / procedure + +代码位置: + +1. [`spacetimedb/src/runtime.rs`](/home/Genarrative/spacetimedb/src/runtime.rs) +2. [`spacetimedb/src/types.rs`](/home/Genarrative/spacetimedb/src/types.rs) +3. [`src/spacetime/generated/index.ts`](/home/Genarrative/src/spacetime/generated/index.ts) + +当前 STDB **没有**的关键能力: + +1. 没有 `runtime story state` view +2. 没有 `resolve runtime story action` procedure +3. 没有 `runtime action` 的版本冲突返回 +4. 没有 `RuntimeStoryViewModel / Presentation / Patch` 对应的 STDB 类型 +5. 没有面向客户端订阅的 story action result / event 承接面 + +结论: + +1. 现在直接写前端 STDB transport/provider 没有后端可对接 +2. 先补清楚 STDB contract 才能进入下一轮编码 + +## 3. Phase 2 的目标 + +`Phase 2` 只做“让前端有能力通过 STDB 读取/提交 runtime story”,不在这一阶段重写全部业务规则。 + +建议目标: + +1. STDB 先承接 `runtime story state get` +2. STDB 先承接 `runtime story action resolve` +3. 前端新增 STDB transport/provider +4. 允许 provider 通过 feature flag 或初始化注入切换 +5. 在 STDB provider 能返回与 Express 同形 contract 之前,不改 `runtimeStoryCoordinator` + +## 4. 建议的 STDB schema 设计 + +### 4.1 不要把完整 runtime story state 拆成大量订阅碎表 + +本阶段不建议一上来把: + +1. player status +2. encounter +3. companions +4. available options +5. patches +6. presentation + +全部拆成大量 public table。 + +原因: + +1. 当前前端上层消费的是真正的“聚合响应” +2. 这条链路还在迁移期,过早拆散会放大前端改动面 +3. 现有 STDB 运行时基础设施本身就是以 `snapshot json + view` 为主 + +因此 Phase 2 先采用“聚合 view + 聚合 procedure return/event”的收口方式更稳。 + +### 4.2 建议新增的自定义类型 + +建议在 [`spacetimedb/src/types.rs`](/home/Genarrative/spacetimedb/src/types.rs) 中新增: + +1. `RuntimeStoryPlayerView` +2. `RuntimeStoryEncounterView` +3. `RuntimeStoryCompanionView` +4. `RuntimeStoryStatusView` +5. `RuntimeStoryOptionInteraction` +6. `RuntimeStoryOptionView` +7. `RuntimeBattlePresentation` +8. `RuntimeStoryPresentationView` +9. `RuntimeStoryPatchView` +10. `RuntimeStoryAggregateView` +11. `RuntimeStoryActionInput` +12. `RuntimeStoryActionResult` + +设计原则: + +1. 字段名尽量对齐前端已有 shared contract +2. 如果 Rust 命名必须使用 snake_case,则在 TypeScript mapper 层做一次转换 +3. 不要在前端重新推导 interaction / option legality / patch 语义 + +### 4.3 建议新增的 view + +建议新增: + +1. `my_runtime_story_state(session_id: String) -> Option` + +字段建议至少包含: + +1. `session_id` +2. `server_version` +3. `player` +4. `encounter` +5. `companions` +6. `available_options` +7. `status` +8. `story_text` +9. `presentation_options` +10. `toast` +11. `battle` +12. `snapshot_version` +13. `snapshot_saved_at_ms` +14. `snapshot_bottom_tab` +15. `snapshot_game_state_json` +16. `snapshot_current_story_json` + +说明: + +1. 这一版 view 直接返回“聚合态” +2. 其职责等价于 Express 的 `getRuntimeStoryState(...)` +3. 前端 transport 从这一个 view 就能拼出 `RuntimeStoryActionResponse` + +### 4.4 建议新增的 procedure + +建议新增: + +1. `resolve_runtime_story_action(meta, session_id, client_version, action) -> RuntimeStoryActionResult` + +返回字段建议至少包含: + +1. `ok` +2. `message` +3. `code` +4. `session_id` +5. `server_version` +6. `player` +7. `encounter` +8. `companions` +9. `available_options` +10. `action_text` +11. `result_text` +12. `story_text` +13. `presentation_options` +14. `toast` +15. `battle` +16. `patches` +17. `snapshot_version` +18. `snapshot_saved_at_ms` +19. `snapshot_bottom_tab` +20. `snapshot_game_state_json` +21. `snapshot_current_story_json` +22. `conflict_client_version` +23. `conflict_server_version` + +为什么 procedure 先返回聚合结果而不是只写表: + +1. 当前前端 `resolveServerRuntimeChoice(...)` 依赖“一次提交,一次拿回完整结果” +2. STDB reducer 不返回值,当前仓库已在其他 runtime 写链路上采用 `procedure + with_tx` 模式 +3. 这能最小化前端改动,且与现有 `save_snapshot` 做法一致 + +## 5. 业务实现建议 + +### 5.1 先复用 Express 现有运行时算法,不要一上来双写两份规则 + +当前最危险的做法是: + +1. 在 Rust 里全量重写一遍 `runtimeSession.ts + storyActionService.ts` +2. 同时还保留 Express 版 + +这样会立刻引入双份规则漂移。 + +建议 Phase 2 先做: + +1. 把 `runtime story` 的领域算法抽到 shared-friendly contract 文档层 +2. 先在 STDB 里承接最小版本 +3. 以 Express 现有行为为基线做回归比对 + +如果下一轮必须直接编码,优先顺序建议是: + +1. `get state` +2. `story / npc / combat` 的核心 option pool +3. `inventory_use` +4. `npc_trade / npc_gift` +5. `npc_quest_accept / npc_quest_turn_in` +6. `treasure_*` + +### 5.2 快照仍然是当前真相源 + +在完成真正的细粒度 runtime 表设计前,建议继续以 `saved_snapshot_row` 中的: + +1. `game_state_json` +2. `current_story_json` + +作为 runtime story 的输入与输出真相源。 + +也就是说 Phase 2 的 STDB runtime story 过程应当: + +1. 读取当前账号 `saved_snapshot_row` +2. 在 procedure 内解析 JSON +3. 完成 runtime story 结算 +4. 把新快照重新写回 `saved_snapshot_row` +5. 同时返回聚合 story response + +这样与 Express 当前语义最接近,也最利于迁移验证。 + +## 6. 前端 provider 设计 + +### 6.1 transport 入口 + +前端下一轮建议新增: + +1. `src/services/runtimeStoryStdbTransport.ts` + +职责: + +1. 通过 `ensureSpacetimeConnection()` 建连 +2. 读取 `my_runtime_story_state(...)` 或对应聚合 view +3. 调用 `resolveRuntimeStoryAction(...)` procedure +4. 映射成现有 `RuntimeStoryResponse` + +### 6.2 mapper 入口 + +建议新增: + +1. `src/spacetime/runtimeStoryMappers.ts` + +职责: + +1. 将 STDB 生成绑定类型映射为前端 shared contract +2. 保持 `RuntimeStoryResponse` 结构与 Express 路径一致 +3. 统一做 `snake_case -> camelCase` +4. 统一解析 snapshot JSON + +### 6.3 provider 切换方式 + +建议不要在页面层直接判断用 HTTP 还是 STDB。 + +建议由初始化层统一注入: + +1. 默认仍是 HTTP transport +2. 在 STDB 后端 contract 完成并验证通过后,再显式执行 `setRuntimeStoryTransport(stdbTransport)` + +注入点可选: + +1. `src/main.tsx` +2. `src/components/auth/AuthGate.tsx` +3. 单独的 runtime bootstrap 模块 + +推荐: + +1. 单独 bootstrap 模块 + +原因: + +1. 不把 provider 选择逻辑塞进 UI 组件 +2. 便于测试环境替换 + +## 7. 验证方案 + +下一轮实现后至少要补这些验证: + +1. STDB `get state` 返回的 `availableOptions / interaction / snapshot` 与 Express 基线一致 +2. STDB `resolve action` 能正确处理 version conflict +3. `runtimeStoryService` 在切到 STDB transport 后,`runtimeStoryCoordinator` 单测无需改契约 +4. `save_snapshot -> get state -> resolve action -> snapshot persisted` 全链路可重复 + +建议加一组“Express vs STDB 同输入同输出”的基线测试,优先覆盖: + +1. `npc_chat` +2. `battle_attack_basic` +3. `inventory_use` +4. `npc_trade` +5. `npc_quest_accept` + +## 8. 下一块最小实现建议 + +在这份设计之后,最小可执行实现块建议是: + +1. 只在 STDB 中新增 `runtime story get state` 聚合 view +2. 前端只实现只读 STDB transport 的 `getState` +3. `resolveAction` 仍暂时走 HTTP + +原因: + +1. 这能先验证 view / mapper / provider 注入是否稳定 +2. 改动面小于一次性把 `resolve action` 也切过去 +3. 能把风险从“全链路迁移”拆成“先读后写” + +当这一步稳定后,再做: + +1. STDB `resolve_runtime_story_action` procedure +2. 前端 `resolveAction` 切换 + +## 9. 本轮涉及的关键参考 + +1. [`src/services/runtimeStoryService.ts`](/home/Genarrative/src/services/runtimeStoryService.ts) +2. [`src/hooks/story/runtimeStoryCoordinator.ts`](/home/Genarrative/src/hooks/story/runtimeStoryCoordinator.ts) +3. [`server-node/src/modules/story/storyActionService.ts`](/home/Genarrative/server-node/src/modules/story/storyActionService.ts) +4. [`server-node/src/modules/story/runtimeSession.ts`](/home/Genarrative/server-node/src/modules/story/runtimeSession.ts) +5. [`spacetimedb/src/runtime.rs`](/home/Genarrative/spacetimedb/src/runtime.rs) +6. [`spacetimedb/src/types.rs`](/home/Genarrative/spacetimedb/src/types.rs) +7. [`packages/shared/src/contracts/story.ts`](/home/Genarrative/packages/shared/src/contracts/story.ts)