# RPG 运行时 Story Engine 后端迁移落地方案(2026-04-28) ## 0. 本轮目标 本轮只收口 `RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md` 中 `4.4 P0 story engine / chapter / world mutation` 这条。 目标不是新增一套前端规则,而是让运行时动作完成后由 `server-rs` 统一写回: 1. `storyHistory` 2. `storyEngineMemory` 3. `chapterState` 4. `currentScenePreset.mutationStateText / currentPressureLevel / description` 前端只能展示这些后端字段,不能继续在 hook 中运行 `chapterDirector / threadSignalRouter / worldMutationRouter` 等正式状态机。 ## 1. 后端落点 ### 1.1 `module-runtime-story-compat` 新增 `story_engine.rs`,作为无 HTTP、无 `AppState` 的纯 JSON projector。 职责: 1. 确保 `storyEngineMemory` 最小结构存在。 2. 按上一帧与下一帧快照生成 story signals。 3. 基于信号推进 active thread、recent signal。 4. 基于当前场景和任务生成 `ChapterState`。 5. 基于章节和信号生成 `WorldMutation`。 6. 把 mutation 投影到当前场景展示字段。 7. 追加最小 chronicle、journey beat、continue digest。 ### 1.2 `api-server runtime_story compat` `resolve_runtime_story_action` 在动作确定性结算和 `storyHistory` 写入后,统一调用 projector,再持久化快照。 这样即使前端只提交 `functionId/payload`,正式叙事记忆也由后端结果生成。 ## 2. 前端收口 ### 2.1 `progressionActions.ts` 保留: 1. 展示层 loading/error。 2. encounter 入场动画。 3. 调用后端生成 story 或 fallback story。 移除: 1. `applyStoryEngineEchoes` 2. 本地章节任务补发。 3. 本地 thread signal、companion reaction、chapter、journey beat、world mutation、QA、release gate 等编排。 ### 2.2 `storyContextBuilder.ts` 保留 prompt context 适配职责,但只能读取后端已存在的字段: 1. `state.chapterState` 2. `state.storyEngineMemory.currentChapter` 3. `state.storyEngineMemory.currentJourneyBeat` 4. `state.storyEngineMemory.worldMutations` 5. `state.currentScenePreset` 禁止继续导入并运行 story engine director。 ## 3. 验收标准 1. `src/hooks/rpg-runtime-story/progressionActions.ts` 不再导入 `services/storyEngine/*`。 2. `src/hooks/rpg-runtime-story/storyContextBuilder.ts` 不再导入 `services/storyEngine/*`。 3. `resolve_runtime_story_action` 返回的 snapshot 中包含后端写入的 `storyEngineMemory.currentChapter`。 4. 场景动作后 `currentScenePreset.mutationStateText` 由后端 projector 写入。 5. `cargo test -p module-runtime-story-compat story_engine --manifest-path server-rs/Cargo.toml` 通过。 6. `cargo test -p api-server runtime_story --manifest-path server-rs/Cargo.toml` 通过。 7. 前端相关 vitest 与编码检查通过。 ## 4. 本轮落地记录 ### 4.1 后端已落地 1. `server-rs/crates/module-runtime-story-compat/src/story_engine.rs` 新增确定性 projector。 2. `server-rs/crates/api-server/src/runtime_story/compat.rs` 在 action resolve 写入 `storyHistory` 后调用 projector,再保存 snapshot。 3. `server-rs/crates/api-server/src/runtime_story/compat/tests.rs` 新增 route 边界测试,覆盖响应 snapshot 中的: - `chapterState.id` - `storyEngineMemory.currentChapter.id` - `quests[].chapterId` - `currentScenePreset.mutationStateText` - `storyEngineMemory.worldMutations` ### 4.2 前端已收口 1. `src/hooks/rpg-runtime-story/progressionActions.ts` 不再执行本地 story engine echo、chapter、journey beat、world mutation 编排。 2. `src/hooks/rpg-runtime-story/storyContextBuilder.ts` 不再导入 `services/storyEngine/*`,只读取后端快照中已有的章节、旅程、mutation、chronicle、companion reaction 等字段。 3. prompt context 中 `visibilitySlice / sceneNarrativeDirective / goalStack / activeScenarioPack / activeCampaignPack` 暂不在前端重建,等待后端后续模块正式写入后直接透传。 ### 4.3 验证结果 已通过: 1. `cargo test -p module-runtime-story-compat story_engine --manifest-path server-rs\Cargo.toml` 2. `cargo test -p api-server runtime_story --manifest-path server-rs\Cargo.toml` 3. `cargo test -p api-server runtime_story_route_boundary_projects_story_engine_state --manifest-path server-rs\Cargo.toml` 4. `npm run test -- src/hooks/rpg-runtime-story/storyRequestCoordinator.test.ts src/hooks/rpg-runtime-story/storyRequestRuntime.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.test.tsx src/hooks/rpg-runtime-story/choiceActions.test.ts src/hooks/rpg-runtime-story/storyInteractionCoordinator.test.ts` 5. `npx eslint src/hooks/rpg-runtime-story/storyContextBuilder.ts src/hooks/rpg-runtime-story/progressionActions.ts --max-warnings 0` 6. `cargo fmt --manifest-path server-rs\Cargo.toml --all --check` 已发现的非本轮阻塞: 1. `npm run typecheck` 当前被既有 NPC 交易、背包/锻造 UI、测试 fixture、`src/services/ai.ts` 缺 import 等错误拦截。 2. `npm run test -- src/hooks/rpg-runtime-story` 当前有 1 个 `storyChoiceRuntime.test.ts` 战斗死亡/复活断言失败,属于审计后续 `4.5` post-battle 迁移范围。 ### 4.4 NPC 聊天半量快照容错补丁 用户复测角色聊天时,点击 NPC 聊天选项后触发: `Cannot read properties of undefined (reading 'length')` 复查调用链确认,后端 story engine projector 已经成为 `storyEngineMemory` 的主写入方,但部分快照或旧存档可能只携带 `currentChapter / worldMutations` 等增量字段,没有补齐 `activeThreadIds / recentCarrierIds / discoveredFactIds` 等数组字段。前端在 `syncNpcNarrativeState()` 中把半量对象当完整 `StoryEngineMemoryState` 消费,直接读取 `activeThreadIds.length`,导致 NPC 选项点击后的好感与叙事记忆同步中断。 本轮只做消费边界容错,不恢复前端 story engine 状态机: 1. `visibilityEngine.ts` 增加 `normalizeStoryEngineMemoryState()`,以 `createEmptyStoryEngineMemoryState()` 为基底补齐数组字段,同时保留后端快照已有字段。 2. `syncNpcNarrativeState()` 与 `appendStoryEngineCarrierMemory()` 在读写叙事记忆前统一归一化,避免半量快照在 NPC 聊天、物品回声等路径里崩溃。 3. `buildEncounterVisibilitySlice()` 与 `buildQuestVisibilitySlice()` 直接消费外部 memory 时也先归一化,保证 visibility 层独立调用时口径一致。 4. 新增 `echoMemory.test.ts` 回归用例,覆盖只有 `currentChapter`、缺少 `activeThreadIds` 的后端投影快照。 验证: 1. `npm run test -- src/services/storyEngine/echoMemory.test.ts src/services/storyEngine/visibilityEngine.test.ts` 2. `npm run check:encoding -- src/services/storyEngine/echoMemory.ts src/services/storyEngine/visibilityEngine.ts src/services/storyEngine/echoMemory.test.ts docs/technical/RPG_RUNTIME_STORY_ENGINE_BACKEND_MIGRATION_2026-04-28.md` ### 4.5 story prompt context 后端 projector 收口 本轮继续收口 `RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md` 中仍未完成的 `story engine / prompt context / AI story 请求编排`: 1. `server-rs/crates/module-runtime-story-compat/src/prompt_context.rs` 新增 `build_runtime_story_prompt_context(...)`,基于后端持久化 `gameState` 投影: - 场景描述、mutation、压力等级。 - encounter / NPC 好感、披露阶段、可谈话题、首次接触姿态。 - conversationSituation / conversationPressure / talkPriority。 - chapter、journey beat、worldMutations、chronicle、party relationship notes。 2. `POST /api/runtime/story/initial` 与 `POST /api/runtime/story/continue` 支持新主链 payload: - `sessionId` - `clientVersion` - `choice` - `lastFunctionId` - `observeSignsRequested` - `recentActionResult` - `requestOptions` 3. 后端收到 `sessionId` 后只从服务端 runtime snapshot 读取 `worldType / playerCharacter / sceneHostileNpcs / storyHistory / prompt context`;旧 `worldType / character / history / context` 字段仅保留兼容,不作为正式主链来源。 4. `runtime_chat_plain.rs` 与 `runtime_chat.rs` 同步支持 `sessionId`,角色私聊、NPC 对话、NPC 单轮聊天、招募对话的 prompt context 也由后端快照投影;前端只继续提交对话草稿、目标角色、玩家发言和必要 UI 临时项。 5. `src/hooks/rpg-runtime-story/storyContextBuilder.ts` 缩减为 session 元信息适配器,不再推导 `conversationSituation / conversationPressure / NPC disclosure / partyRelationshipNotes / scene pressure` 等正式上下文。 6. `src/services/aiService.ts` 在存在 `runtimeSessionId` 时,story initial/continue 只提交 session 轻量 payload;聊天接口只附带 `sessionId` 与对话输入,不再上传完整 `StoryGenerationContext`。 7. `src/hooks/rpg-runtime-story/sessionActions.ts` 领取任务奖励时不再运行前端 `chapterDirector / echoMemory`,只保留旧 UI 层奖励展示所需的本地字段;章节和 `storyEngineMemory.currentChapter` 等正式叙事字段等待后端 action snapshot 刷新。 8. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` NPC 聊天闭合后不再调用前端 scene act runtime 推进 `storyEngineMemory.currentSceneActState`,也不再把 `deferredRuntimeState.storyEngineMemory` 写回正式 `GameState`。 9. `src/hooks/rpg-runtime-story/choiceActions.ts` 兼容旧 `deferredRuntimeState` 时只允许采用场景字段,不再从 story moment 写入 `storyEngineMemory`。 新增验证: 1. `cargo test -p module-runtime-story-compat prompt_context --manifest-path server-rs\Cargo.toml` 2. `cargo test -p shared-contracts runtime_story_ai_request --manifest-path server-rs\Cargo.toml` 3. `cargo test -p api-server runtime_story_initial_uses_server_snapshot_prompt_context_when_session_id_present --manifest-path server-rs\Cargo.toml` 4. `cargo check -p api-server --manifest-path server-rs\Cargo.toml --message-format short` 5. `npm run test -- src/services/ai.test.ts src/hooks/rpg-runtime-story/storyRequestCoordinator.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.test.tsx` 6. `npm run test -- src/hooks/rpg-runtime-story/sessionActions.test.ts src/hooks/rpg-runtime-story/choiceActions.test.ts src/hooks/rpg-runtime-story/npcEncounterActions.test.ts` 7. `npx eslint src/hooks/rpg-runtime-story/sessionActions.ts src/hooks/rpg-runtime-story/sessionActions.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts src/hooks/rpg-runtime-story/choiceActions.ts src/hooks/rpg-runtime-story/choiceActions.test.ts --max-warnings 0` ### 4.6 `camp_travel_home_scene` 后端收尾 本轮继续收口完成度核验中最后一个残留点:`camp_travel_home_scene` 不能再被前端 `runCampTravelHomeChoice(...)` 提前拦截并本地拼装正式状态。 落地规则: 1. 前端点击 `camp_travel_home_scene` 后统一进入 `runServerRuntimeChoiceAction(...)`,只提交 `sessionId / clientVersion / functionId / optionText / runtimePayload`。 2. `server-rs/crates/api-server/src/runtime_story/compat.rs` 负责解析离营目标场景: - 优先使用 action payload 中的 `targetSceneId`。 - 内置世界按 `playerCharacter.id + worldType` 映射到角色主场景。 - 自定义世界优先找玩家角色在 `landmarks[].sceneNpcIds` 中绑定的地点,否则使用当前营地的 `forwardSceneId / connectedSceneIds` 或第一个 landmark。 3. 后端 resolver 写入完整离营状态: - `currentScenePreset` - `currentEncounter / sceneHostileNpcs / npcInteractionActive / inBattle` - `runtimeStats.scenesTraveled` - `playerX / playerFacing / animationState / playerActionMode / scrollWorld` - `lastObserveSigns* / currentBattle* / spar* / activeCombatEffects` 4. 后端在目标场景上生成确定性的 encounter preview;内置场景至少带一个可交互 NPC,自定义场景复用 `build_custom_scene_preset(...)` 中的 NPC 投影。 5. 后端保存 `storyHistory` 与 `currentStory`,随后继续走 `project_story_engine_after_action(...)` 和持久化快照。 6. 前端保留 `camp_travel_home_scene` 作为 function id 与展示用 helper,但不再保留正式状态构造函数。 验证新增: 1. 后端 route 测试覆盖 `camp_travel_home_scene` 点击后 hydrated snapshot 已进入角色主场景、生成 encounter preview、递增 `scenesTraveled` 并持久化。 2. 前端 `choiceActions.test.ts` 覆盖 `camp_travel_home_scene` 即使命中旧 helper 判定,也只调用 `runServerRuntimeChoiceAction(...)`。