# 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 逐步收束成“动作编排壳 + 多个纯规则模块”的明确结构。 在此基础上,同日又继续把 NPC 交互侧的一批纯 helper 收到独立模块: 1. `compat` 新增 [npc_support.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/npc_support.rs)。 2. 已迁入的内容包括: - 赠礼好感收益与赠礼结果文本 - 交易价格、折扣档位、货币文本、数量后缀 - 队伍招募与满员换队 helper 3. [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 现在对 `npc_trade / npc_gift / npc_recruit` 仍只保留动作编排,不再承担底层价格计算和队伍变换逻辑。 4. 到这一步,`compat.rs` 的主要职责已经更接近: - route handler / snapshot bridge - action orchestration - 少量尚未迁出的共享 glue code 这为后续把“无 HTTP / 无 `AppState`”的剩余 glue code 再往下收,提供了更明确的拆分方向。 第二阶段继续推进到 action resolver 编排后,当前又新增动作编排模块: 1. [battle_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle_actions.rs)。 2. [equipment_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/equipment_actions.rs)。 3. [forge_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge_actions.rs)。 4. [npc_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs)。 5. [quest_actions.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/quest_actions.rs)。 已迁入的内容包括: 1. `battle_*` 2. `equipment_equip / equipment_unequip` 3. `forge_craft / forge_dismantle / forge_reforge` 4. `npc_preview_talk / npc_chat / npc_help / npc_fight / npc_spar` 5. `npc_trade / npc_gift / npc_recruit` 6. `npc_chat_quest_offer_view` 7. `npc_chat_quest_offer_replace` 8. `npc_chat_quest_offer_abandon` 9. `npc_quest_accept` 10. `npc_quest_turn_in` 这组 resolver 虽然仍是 action orchestration,但已经不依赖 HTTP / `AppState`,只依赖快照 `Value`、当前故事 `currentStory`、共享 DTO 与内部 helper,因此适合先作为 `api-server` 内部模块沉淀。 迁移后 [compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 对这些动作只保留 functionId 分发、快照桥接与少量共享 glue code,不再承载 battle / equipment / forge / NPC / quest 的具体结算细节。 ## 11. 独立 crate 抽取边界 完成第二阶段后,已经可以进入第三阶段,但独立 crate 仍按最小安全边界推进: 1. 新 crate 命名为 `module-runtime-story-compat`。 2. `module-runtime-story-compat` 只承接“无 HTTP / 无 `AppState`”的 compat 核心: - runtime story action 分发与确定性结算 - battle / equipment / forge / NPC / quest action resolver - `Value` 快照态读写 helper - `RuntimeStoryActionResponse` 的 view model / presentation 编译 3. `api-server` 继续保留: - Axum route handler - `RequestContext / AuthenticatedAccessToken` - `runtime_snapshot` 持久化与读取 - `clientVersion` 校验到 HTTP error 的映射 - `platform-llm` 动作后文本增强 4. 首批迁移不把 AI 文本增强放进新 crate,因为它依赖 `AppState` 和 `platform-llm`。 5. 首批迁移不把 test route boundary 放进新 crate,route boundary 仍属于 `api-server`。 这一步完成后,`api-server` 的 `runtime_story/compat.rs` 应该只负责: 1. 从 HTTP 请求恢复 / 持久化 snapshot 2. 调用 `module-runtime-story-compat` 产出确定性动作结果或状态响应 3. 需要时调用本地 AI 增强 4. 将最终响应包回 `Json` 这就是从“`api-server` 内部模块”到“独立 crate”的首个可验证切片。 截至当前工作区,第三阶段首批独立 crate 已落地: 1. 已新增 [module-runtime-story-compat](D:/Genarrative/server-rs/crates/module-runtime-story-compat)。 2. 已接入 [server-rs/Cargo.toml](D:/Genarrative/server-rs/Cargo.toml) workspace。 3. [api-server/Cargo.toml](D:/Genarrative/server-rs/crates/api-server/Cargo.toml) 已新增对 `module-runtime-story-compat` 的依赖。 4. 首批迁入新 crate 的内容包括: - `StoryResolution` - `GeneratedStoryPayload` - `CurrentEncounterNpcQuestContext` - `PendingQuestOfferContext` - `RuntimeStoryActionResponseParts` - `CONTINUE_ADVENTURE_FUNCTION_ID` - `MAX_TASK5_COMPANIONS` - `simple_story_resolution` - `resolve_action_text` - `build_status_patch` - `current_world_type` 5. 第三阶段继续推进后,当前已经从 `api-server` 抽到独立 crate 的纯逻辑还包括: - `core.rs`:JSON 快照读写、runtime stat、story history、progression、encounter 清理 - `game_state.rs`:encounter / inventory / equipment 的基础 helper - `forge.rs`:锻造配方、重铸成本、材料消耗、拆解产物、重铸产物、货币文本 - `forge_actions.rs`:`forge_craft / forge_dismantle / forge_reforge` 三条动作结算 - `npc_support.rs`:赠礼好感收益、交易价格、数量文案、满员换队招募 helper - `battle.rs`:`battle_* / inventory_use` 的纯动作结算、patch 生成与胜负写回 6. 当前 [api-server 的 compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 已经不再内嵌上述纯逻辑,只保留: - Axum handler - snapshot 读写 - `clientVersion` 校验 - functionId 分发 - HTTP error 映射 - 动作后 AI 文本增强 7. 当前 [api-server 的 forge.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/forge.rs) 已收缩成极薄 bridge,只为 NPC trade bootstrap 复用新 crate 暴露的运行时物品构造 helper,锻造规则主体不再保留本地副本。 8. 当前 [api-server 的 battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs) 也已从“结算 + 展示”收缩成“展示编译 + 少量本地 helper”: - battle 动作结算主链已经迁入 `module-runtime-story-compat` - `api-server` 本地仅继续保留 `build_battle_runtime_story_options(...)` 与 `restore_player_resource(...)` 这类仍被 presentation / NPC 辅助逻辑直接依赖的部分 - 这为下一步继续把 battle option compiler 收进独立 crate 做好了边界准备 这意味着第三阶段已经不只是“创建了新 crate”,而是完成了第一批真正跨 crate 的 compat 纯逻辑迁移,并且保持 route boundary 与既有测试口径不变。 同日继续推进后,battle 这块已经完成从“先迁结算主链”到“连展示编译一起迁”的下一步: 1. [module-runtime-story-compat 的 battle.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/battle.rs) 当前已同时承接: - `resolve_battle_action(...)` - `restore_player_resource(...)` - `build_battle_runtime_story_options(...)` - 技能冷却读取、推荐物品挑选、战斗技能 option compiler 等 battle 展示辅助 2. [api-server 的 compat.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat.rs) 已直接从 `module-runtime-story-compat` 导入 battle 展示编译与资源恢复 helper。 3. [api-server 本地的 compat/battle.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/battle.rs) 已删除,不再保留 battle 规则的本地副本。 4. 到这一步,`api-server` 在 runtime story compat 上对 battle 的职责已经只剩: - functionId 分发 - route handler / snapshot bridge - AI 文本增强后的最终响应拼装 这说明第三阶段已经不只是在“拆 crate”,而是在真实压缩 `api-server` 的 compat 规则面。接下来更合理的推进方向将不再是 battle,而是继续评估 `presentation` 中还能进一步抽到独立 crate 的纯 view model / option compiler 边界。 同日继续推进后,`presentation` 中最通用的一层 option DTO 编译也已经开始抽离: 1. 已新增 [options.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/options.rs),统一承接: - `build_static_runtime_story_option(...)` - `build_runtime_story_option_with_payload(...)` - `build_disabled_runtime_story_option(...)` - `build_runtime_story_option_from_story_option(...)` - `build_story_option_from_runtime_option(...)` - `infer_option_scope(...)` 2. [module-runtime-story-compat 的 lib.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/lib.rs) 已对外 re-export 这些 option helper,供 `api-server` 直接复用。 3. [api-server 的 presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs) 已删除本地重复实现,只保留 NPC option 组合、view model 组装、quest currentStory 等尚未完全独立的部分。 这一步的意义不是单纯减少行数,而是先把 `RuntimeStoryOptionView` 的最小稳定编译面收敛到独立 crate。后续若继续外提 `view model` 与 `fallback option compiler`,将不需要再重复搬运这些 option 基础件。 同日继续推进后,`presentation` 中的纯 view-model builder 也已经抽到独立 crate: 1. 已新增 [view_model.rs](D:/Genarrative/server-rs/crates/module-runtime-story-compat/src/view_model.rs),统一承接: - `build_runtime_story_view_model(...)` - `build_runtime_story_companions(...)` - `build_runtime_story_encounter(...)` - `resolve_current_encounter_npc_state(...)` 2. [api-server 的 presentation.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/presentation.rs) 已删除本地 view-model 组装实现,继续只负责状态响应 orchestration、dialogue currentStory、fallback option compiler 与 quest 辅助。 3. [api-server 的 game_state.rs](D:/Genarrative/server-rs/crates/api-server/src/runtime_story/compat/game_state.rs) 当前也直接复用 crate 导出的 `resolve_current_encounter_npc_state(...)`,避免 NPC 状态查询 helper 在 `api-server` 和 crate 之间出现两套实现。 至此,`module-runtime-story-compat` 已经覆盖了 runtime story 兼容层的以下纯逻辑面: 1. JSON 快照读写与基础状态 helper 2. battle / forge / npc support 的纯规则结算 3. battle option compiler 4. runtime story option DTO 编译 5. runtime story view-model 编译 `api-server` 当前的剩余重点已经更集中在: 1. HTTP / snapshot bridge 2. functionId 分发 3. AI 文本增强 4. NPC / quest fallback option 与 currentStory 组合逻辑