refactor: split runtime story compat modules

This commit is contained in:
2026-04-22 18:14:30 +08:00
parent fc6519a7b7
commit 81e59f90ce
11 changed files with 8170 additions and 3737 deletions

View File

@@ -0,0 +1,259 @@
# M4 Runtime Story Rust 文件拆分方案2026-04-22
更新时间:`2026-04-22`
## 0. 文档目标
本文件只解决一个工程问题:
**把 `server-rs/crates/api-server/src/runtime_story.rs` 从当前超大单文件拆成可维护的 Rust 子模块,同时不改变既有 M4 compat bridge 的行为边界。**
当前 `runtime_story.rs` 已超过 `7000` 行,内部同时混杂了:
1. Axum route handler
2. snapshot 持久化与 DTO 组装
3. runtime story compat 动作结算
4. runtime option compiler / currentStory builder
5. LLM 文本增强
6. test fixture 与 route boundary 回归
这已经超出单文件可维护范围,也会直接拖慢后续继续迁移 Node compat 分支的速度。
---
## 1. 本轮拆分原则
本轮拆分坚持以下边界:
1. **先拆“展示编译层”和“AI 增强层”,不先重写规则结算层。**
2. **不改变 `app.rs` 里的路由绑定函数名。**
3. **不改变 `RuntimeStoryActionResponse / RuntimeStoryAiResponse` contract。**
4. **不改变现有 compat bridge 的动作规则、patch、snapshot 写回顺序。**
5. **优先做可验证的文件拆分,不把这轮演变成架构重写。**
原因:
1. 当前 `resolve_runtime_story_choice_action(...)` 仍在持续迁移 Node compat 行为,短期内继续集中在主文件更利于快速补链。
2. `presentation / option compiler / dialogue currentStory / AI payload` 对外依赖相对单纯,更适合先抽走。
3. test module 独立后,可以明显降低主文件噪音,后续再继续拆规则层也更安全。
---
## 2. 首轮拆分目标
首轮只拆以下 3 块:
### 2.1 `runtime_story/presentation.rs`
职责:
1. `viewModel` 编译
2. `availableOptions` 编译
3. `currentStory` builder
4. `dialogue / pendingQuestOffer` 的 story shape helper
5. `story option``interaction` 的组装
这块包含但不限于:
1. `build_runtime_story_view_model`
2. `build_runtime_story_options`
3. `build_fallback_runtime_story_options`
4. `build_dialogue_current_story`
5. `build_pending_quest_offer_story`
6. `build_story_option_from_runtime_option`
### 2.2 `runtime_story/ai.rs`
职责:
1. `initial / continue``RuntimeStoryAiResponse`
2. `actions/resolve` 后的最小 LLM 文本增强
3. 对话 turn 解析
4. AI prompt payload 构造
这块包含但不限于:
1. `build_runtime_story_ai_response`
2. `generate_ai_story_text`
3. `generate_action_story_payload`
4. `generate_npc_dialogue_payload`
5. `generate_reasoned_story_payload`
6. `parse_dialogue_turns`
### 2.3 `runtime_story/tests.rs`
职责:
1. route boundary test
2. 纯函数回归
3. fixture builder
4. 鉴权 token helper
---
## 3. 拆分后目录形态
首轮目标目录:
```text
server-rs/crates/api-server/src/
├─ runtime_story.rs
└─ runtime_story/
├─ ai.rs
├─ presentation.rs
└─ tests.rs
```
其中:
1. `runtime_story.rs` 保留为外层入口模块
2. 子模块通过 `mod ai; mod presentation; #[cfg(test)] mod tests;` 组织
3. `runtime_story.rs` 继续暴露原有 5 个 route handler
- `resolve_runtime_story_state`
- `get_runtime_story_state`
- `resolve_runtime_story_action`
- `generate_runtime_story_initial`
- `generate_runtime_story_continue`
---
## 4. Rust 侧实现策略
## 4.1 不做新的共享 crate
本轮不把 helper 再抽成新的 crate 或全局 util module。
原因:
1. 当前拆分目标是降低单文件复杂度,不是扩展跨模块复用面。
2. `presentation / ai / tests` 仍强依赖 `runtime_story` 内部 helper。
3. 如果过早抽到 crate 级共享层,会额外引入新的 API 稳定面和更大改动范围。
## 4.2 子模块通过 `super::*` 复用内部 helper
首轮允许子模块继续通过 `use super::*;` 访问现有内部函数、结构体和常量。
这是刻意的折中:
1. 优先完成物理拆分
2. 暂不要求所有 helper 立即彻底分层
3. 后续再在第二轮继续把规则层和 state helper 往下切
## 4.3 第二轮候选拆分
本轮完成后,下一轮可继续评估:
1. `runtime_story/actions.rs`
2. `runtime_story/battle.rs`
3. `runtime_story/inventory.rs`
4. `runtime_story/npc_state.rs`
5. `runtime_story/json_state.rs`
但这些都不属于本次提交的必达范围。
---
## 5. 验证要求
拆分后至少必须通过:
1. `cargo test -p api-server runtime_story --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
2. `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
3. `node D:\\Genarrative\\scripts\\check-encoding.mjs`
若以上任一失败,则本轮拆分不算完成。
---
## 6. 本轮明确不做
1. 不改 compat bridge 业务规则
2. 不新增或删除 runtime functionId
3. 不顺手把 quest 里的历史 `inspect_treasure` 字段一并清理
4. 不提前把 `resolve_story_action / sync_runtime_snapshot_projection` 真相 reducer 并入本轮
5. 不修改前端调用边界
---
## 7. 完成标记
本轮拆分完成的判定标准:
1. `runtime_story.rs` 明显缩短,至少不再携带 tests 与 AI/presentation 全量实现
2. `runtime_story/ai.rs``runtime_story/presentation.rs``runtime_story/tests.rs` 已落地
3. route handler 对外签名不变
4. 定向回归全部通过
达到以上条件后,再继续进入下一轮“规则层进一步拆分”。
---
## 8. 2026-04-22 实际落地进度
截至 `2026-04-22` 当前工作区,首轮物理拆分已经进入可继续演进状态:
1. 外层入口 [runtime_story.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story.rs) 已缩成薄壳,只保留原有 5 个 route handler 的导出。
2. 兼容实现主体已迁入 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs),并继续保留规则结算主链。
3. `tests` 已外置到 [tests.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/tests.rs),避免继续堆在主文件内。
4. 本轮进一步把 `compat` 内部再拆成:
- [ai.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/ai.rs)
- [presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs)
5. 当前拆分策略仍然维持 `compat` 内部模块,通过 `use super::*;` 复用共享 helper不提前抽独立 crate。
6. quest replace / fixture 中原本残留的 `inspect_treasure` mock 已同步替换为更中性的 `talk_to_npc`,避免把已废弃的 treasure 概念继续固化进新模块。
下一步不再是继续把文件塞回去,而是沿着当前目录继续把“无 HTTP / 无 AppState”的纯规则与编译逻辑收敛出来为后续独立 crate 做第二阶段准备。
## 9. 第二阶段收敛边界
第二阶段不新增对外入口,只继续整理 `compat` 内部依赖面:
1. 继续保留 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 作为 route handler、快照持久化与 compat action orchestration 的主入口。
2. 优先把“只依赖 `serde_json::Value` / 共享 contract / 纯函数 helper”的部分抽到内部纯模块。
3. 当前最适合先抽的块不是 battle route而是
- NPC 状态补齐
- encounter / inventory / equipment 读写
- quest / trade / recruit 等会复用的 `game_state` 纯变换 helper
4. 这一步的目标不是立刻独立 crate而是先在 `api-server` 内形成清晰的“HTTP 外壳”与“纯状态编译层”分界。
如果第二阶段完成后 `compat` 内已经能明显区分:
1. `AppState / RequestContext / Axum` 相关边界
2. `Value -> Value / DTO` 的纯规则层
那么第三阶段再把后者抽成独立 crate风险会显著低于现在直接新建 crate。
## 10. 第二阶段 battle 收敛进度
截至 `2026-04-22` 当前工作区,第二阶段已经继续向“纯规则内聚”推进一块 battle 逻辑:
1. `compat` 新增 [battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs),专门承接 battle 兼容桥里的纯规则与展示编译 helper。
2. 已迁入 `battle.rs` 的内容包括:
- 战斗数值写回 helper
- 技能 / 物品的 battle action plan 生成
- 战斗技能冷却读写
- battle 选项与推荐物品编译
- battle 胜利经验奖励计算
3. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 当前继续只保留 `resolve_battle_action(...)` 这种动作编排入口,不再堆放大段 battle 纯 helper。
4. [core.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/core.rs) 中原本只服务 battle 链的 skill / inventory 读取与 cooldown helper 已同步移出,避免“纯规则仍散落在多个模块”。
5. 这一步仍然没有改变:
- Axum route handler 签名
- `AppState / RequestContext` 边界
- `RuntimeStoryActionResponse` / patch / snapshot 的写回顺序
这说明第二阶段已经不只是在“补状态 helper”而是开始把 compat 内最独立的一类规则块真正收束成内部纯模块。下一步可以继续沿同样方法处理 `forge`,以及 `trade / gift / companion` 这类不依赖 HTTP 的 helper 群。
同日进一步推进后,这条路线已经从 battle 扩展到 forge
1. `compat` 新增 [forge.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge.rs),把锻造配方、重铸成本、材料消耗、运行时物品生成、拆解产物和重铸产物构造统一收口。
2. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 当前对 forge 只保留:
- `resolve_forge_craft_action(...)`
- `resolve_forge_dismantle_action(...)`
- `resolve_forge_reforge_action(...)`
这些动作编排入口。
3. [game_state.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/game_state.rs) 里的 NPC trade bootstrapping 继续直接复用 `forge.rs` 中的运行时物品构造 helper避免 trade stock 与工坊产物出现两套生成规则。
4. 这意味着第二阶段已经形成一个更清楚的内部形态:
- `battle.rs`:战斗纯规则与战斗选项编译
- `forge.rs`:工坊纯规则与运行时锻造物品生成
- `game_state.rs`:快照态读写与 NPC / inventory / equipment 状态桥
后续再继续迁 `trade / gift / companion` 时,目标就不再是单纯减少行数,而是把 compat bridge 逐步收束成“动作编排壳 + 多个纯规则模块”的明确结构。

View File

@@ -81,6 +81,7 @@
- [M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md](./M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md):记录 `server-rs``M4` 首轮已落地的 `story_session / story_event` SpacetimeDB 基座、`begin_story_session / continue_story` reducer、同步返回快照的 story procedure、`spacetime-client` facade 与新的 `/api/story/sessions*` Axum 接口,以及当前尚未兼容旧 `runtime story` 路由的边界。
- [M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md](./M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md):冻结 `GET /api/story/sessions/:storySessionId/state` 这条最小 story state 查询切片,明确当前只返回 `storySession + storyEvents`,不等价于旧 `runtime story state` 兼容完成。
- [M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md](./M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md):冻结旧 `POST /api/runtime/story/state/resolve` 兼容桥的首版边界,明确先补 `RuntimeStoryActionResponse` DTO 与状态桥,再继续进入 Rust `actions/resolve` 与正式 snapshot projection。
- [M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md](./M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md):冻结 `runtime_story.rs` 从超大单文件拆到 `compat/ai/presentation/tests/battle/core/game_state/forge` 子模块的收口策略、验证要求与下一阶段纯规则下沉边界。
- [M4_MODULE_AI_BASELINE_DESIGN_2026-04-21.md](./M4_MODULE_AI_BASELINE_DESIGN_2026-04-21.md):冻结 `module-ai` 首版的任务/阶段/流式片段/结果引用领域模型、最小内存服务与后续 `platform-llm` / `api-server` / `spacetime-module` 的边界。
- [M4_MODULE_AI_SPACETIMEDB_BASELINE_2026-04-21.md](./M4_MODULE_AI_SPACETIMEDB_BASELINE_2026-04-21.md):记录 `module-ai``spacetime-module` 中首轮已落地的 `ai_task / ai_task_stage / ai_text_chunk / ai_result_reference` 真相表、最小 reducer/procedure 与当前仍未扩到真实模型调用和 Axum facade 的边界。
- [M4_MODULE_AI_AXUM_FACADE_DESIGN_2026-04-22.md](./M4_MODULE_AI_AXUM_FACADE_DESIGN_2026-04-22.md):冻结 `module-ai``shared-contracts``spacetime-client``api-server` 的最小 AI task mutation facade明确 `start` 路由当前只返回 `202 Accepted`