Files
Genarrative/docs/technical/M4_RUNTIME_STORY_RS_SPLIT_PLAN_2026-04-22.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

21 KiB
Raw Blame History

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 optioninteraction 的组装

这块包含但不限于:

  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 / continueRuntimeStoryAiResponse
  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. 拆分后目录形态

首轮目标目录:

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.rsruntime_story/presentation.rsruntime_story/tests.rs 已落地
  3. route handler 对外签名不变
  4. 定向回归全部通过

达到以上条件后,再继续进入下一轮“规则层进一步拆分”。


8. 2026-04-22 实际落地进度

截至 2026-04-22 当前工作区,首轮物理拆分已经进入可继续演进状态:

  1. 外层入口 runtime_story.rs 已缩成薄壳,只保留原有 5 个 route handler 的导出。
  2. 兼容实现主体已迁入 compat.rs,并继续保留规则结算主链。
  3. tests 已外置到 tests.rs,避免继续堆在主文件内。
  4. 本轮进一步把 compat 内部再拆成:
  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 作为 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,专门承接 battle 兼容桥里的纯规则与展示编译 helper。
  2. 已迁入 battle.rs 的内容包括:
    • 战斗数值写回 helper
    • 技能 / 物品的 battle action plan 生成
    • 战斗技能冷却读写
    • battle 选项与推荐物品编译
    • battle 胜利经验奖励计算
  3. compat.rs 当前继续只保留 resolve_battle_action(...) 这种动作编排入口,不再堆放大段 battle 纯 helper。
  4. 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,把锻造配方、重铸成本、材料消耗、运行时物品生成、拆解产物和重铸产物构造统一收口。
  2. compat.rs 当前对 forge 只保留:
    • resolve_forge_craft_action(...)
    • resolve_forge_dismantle_action(...)
    • resolve_forge_reforge_action(...) 这些动作编排入口。
  3. 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
  2. 已迁入的内容包括:
    • 赠礼好感收益与赠礼结果文本
    • 交易价格、折扣档位、货币文本、数量后缀
    • 队伍招募与满员换队 helper
  3. 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
  2. equipment_actions.rs
  3. forge_actions.rs
  4. npc_actions.rs
  5. 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 对这些动作只保留 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因为它依赖 AppStateplatform-llm
  5. 首批迁移不把 test route boundary 放进新 crateroute boundary 仍属于 api-server

这一步完成后,api-serverruntime_story/compat.rs 应该只负责:

  1. 从 HTTP 请求恢复 / 持久化 snapshot
  2. 调用 module-runtime-story-compat 产出确定性动作结果或状态响应
  3. 需要时调用本地 AI 增强
  4. 将最终响应包回 Json<Value>

这就是从“api-server 内部模块”到“独立 crate”的首个可验证切片。

截至当前工作区,第三阶段首批独立 crate 已落地:

  1. 已新增 module-runtime-story-compat
  2. 已接入 server-rs/Cargo.toml workspace。
  3. 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.rsJSON 快照读写、runtime stat、story history、progression、encounter 清理
    • game_state.rsencounter / inventory / equipment 的基础 helper
    • forge.rs:锻造配方、重铸成本、材料消耗、拆解产物、重铸产物、货币文本
    • forge_actions.rsforge_craft / forge_dismantle / forge_reforge 三条动作结算
    • npc_support.rs:赠礼好感收益、交易价格、数量文案、满员换队招募 helper
    • battle.rsbattle_* / inventory_use 的纯动作结算、patch 生成与胜负写回
  6. 当前 api-server 的 compat.rs 已经不再内嵌上述纯逻辑,只保留:
    • Axum handler
    • snapshot 读写
    • clientVersion 校验
    • functionId 分发
    • HTTP error 映射
    • 动作后 AI 文本增强
  7. 当前 api-server 的 forge.rs 已收缩成极薄 bridge只为 NPC trade bootstrap 复用新 crate 暴露的运行时物品构造 helper锻造规则主体不再保留本地副本。
  8. 当前 api-server 的 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 当前已同时承接:
    • resolve_battle_action(...)
    • restore_player_resource(...)
    • build_battle_runtime_story_options(...)
    • 技能冷却读取、推荐物品挑选、战斗技能 option compiler 等 battle 展示辅助
  2. api-server 的 compat.rs 已直接从 module-runtime-story-compat 导入 battle 展示编译与资源恢复 helper。
  3. api-server 本地的 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,统一承接:
    • 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 已对外 re-export 这些 option helperapi-server 直接复用。
  3. api-server 的 presentation.rs 已删除本地重复实现,只保留 NPC option 组合、view model 组装、quest currentStory 等尚未完全独立的部分。

这一步的意义不是单纯减少行数,而是先把 RuntimeStoryOptionView 的最小稳定编译面收敛到独立 crate。后续若继续外提 view modelfallback option compiler,将不需要再重复搬运这些 option 基础件。

同日继续推进后,presentation 中的纯 view-model builder 也已经抽到独立 crate

  1. 已新增 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 已删除本地 view-model 组装实现,继续只负责状态响应 orchestration、dialogue currentStory、fallback option compiler 与 quest 辅助。
  3. api-server 的 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 组合逻辑