补充runtime story迁移Phase2契约设计

This commit is contained in:
2026-04-20 10:00:00 +00:00
parent e8beb0a988
commit 00edcfe121
2 changed files with 339 additions and 0 deletions

View File

@@ -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 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。

View File

@@ -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<RuntimeStoryAggregateView>`
字段建议至少包含:
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)