补充runtime story迁移Phase2契约设计
This commit is contained in:
@@ -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 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user