12 KiB
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 统一写回:
storyHistorystoryEngineMemorychapterStatecurrentScenePreset.mutationStateText / currentPressureLevel / description
前端只能展示这些后端字段,不能继续在 hook 中运行 chapterDirector / threadSignalRouter / worldMutationRouter 等正式状态机。
1. 后端落点
1.1 module-runtime-story-compat
新增 story_engine.rs,作为无 HTTP、无 AppState 的纯 JSON projector。
职责:
- 确保
storyEngineMemory最小结构存在。 - 按上一帧与下一帧快照生成 story signals。
- 基于信号推进 active thread、recent signal。
- 基于当前场景和任务生成
ChapterState。 - 基于章节和信号生成
WorldMutation。 - 把 mutation 投影到当前场景展示字段。
- 追加最小 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
保留:
- 展示层 loading/error。
- encounter 入场动画。
- 调用后端生成 story 或 fallback story。
移除:
applyStoryEngineEchoes- 本地章节任务补发。
- 本地 thread signal、companion reaction、chapter、journey beat、world mutation、QA、release gate 等编排。
2.2 storyContextBuilder.ts
保留 prompt context 适配职责,但只能读取后端已存在的字段:
state.chapterStatestate.storyEngineMemory.currentChapterstate.storyEngineMemory.currentJourneyBeatstate.storyEngineMemory.worldMutationsstate.currentScenePreset
禁止继续导入并运行 story engine director。
3. 验收标准
src/hooks/rpg-runtime-story/progressionActions.ts不再导入services/storyEngine/*。src/hooks/rpg-runtime-story/storyContextBuilder.ts不再导入services/storyEngine/*。resolve_runtime_story_action返回的 snapshot 中包含后端写入的storyEngineMemory.currentChapter。- 场景动作后
currentScenePreset.mutationStateText由后端 projector 写入。 cargo test -p module-runtime-story-compat story_engine --manifest-path server-rs/Cargo.toml通过。cargo test -p api-server runtime_story --manifest-path server-rs/Cargo.toml通过。- 前端相关 vitest 与编码检查通过。
4. 本轮落地记录
4.1 后端已落地
server-rs/crates/module-runtime-story-compat/src/story_engine.rs新增确定性 projector。server-rs/crates/api-server/src/runtime_story/compat.rs在 action resolve 写入storyHistory后调用 projector,再保存 snapshot。server-rs/crates/api-server/src/runtime_story/compat/tests.rs新增 route 边界测试,覆盖响应 snapshot 中的:chapterState.idstoryEngineMemory.currentChapter.idquests[].chapterIdcurrentScenePreset.mutationStateTextstoryEngineMemory.worldMutations
4.2 前端已收口
src/hooks/rpg-runtime-story/progressionActions.ts不再执行本地 story engine echo、chapter、journey beat、world mutation 编排。src/hooks/rpg-runtime-story/storyContextBuilder.ts不再导入services/storyEngine/*,只读取后端快照中已有的章节、旅程、mutation、chronicle、companion reaction 等字段。- prompt context 中
visibilitySlice / sceneNarrativeDirective / goalStack / activeScenarioPack / activeCampaignPack暂不在前端重建,等待后端后续模块正式写入后直接透传。
4.3 验证结果
已通过:
cargo test -p module-runtime-story-compat story_engine --manifest-path server-rs\Cargo.tomlcargo test -p api-server runtime_story --manifest-path server-rs\Cargo.tomlcargo test -p api-server runtime_story_route_boundary_projects_story_engine_state --manifest-path server-rs\Cargo.tomlnpm 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.tsnpx eslint src/hooks/rpg-runtime-story/storyContextBuilder.ts src/hooks/rpg-runtime-story/progressionActions.ts --max-warnings 0cargo fmt --manifest-path server-rs\Cargo.toml --all --check
已发现的非本轮阻塞:
npm run typecheck当前被既有 NPC 交易、背包/锻造 UI、测试 fixture、src/services/ai.ts缺 import 等错误拦截。npm run test -- src/hooks/rpg-runtime-story当前有 1 个storyChoiceRuntime.test.ts战斗死亡/复活断言失败,属于审计后续4.5post-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 状态机:
visibilityEngine.ts增加normalizeStoryEngineMemoryState(),以createEmptyStoryEngineMemoryState()为基底补齐数组字段,同时保留后端快照已有字段。syncNpcNarrativeState()与appendStoryEngineCarrierMemory()在读写叙事记忆前统一归一化,避免半量快照在 NPC 聊天、物品回声等路径里崩溃。buildEncounterVisibilitySlice()与buildQuestVisibilitySlice()直接消费外部 memory 时也先归一化,保证 visibility 层独立调用时口径一致。- 新增
echoMemory.test.ts回归用例,覆盖只有currentChapter、缺少activeThreadIds的后端投影快照。
验证:
npm run test -- src/services/storyEngine/echoMemory.test.ts src/services/storyEngine/visibilityEngine.test.tsnpm 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 请求编排:
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。
POST /api/runtime/story/initial与POST /api/runtime/story/continue支持新主链 payload:sessionIdclientVersionchoicelastFunctionIdobserveSignsRequestedrecentActionResultrequestOptions
- 后端收到
sessionId后只从服务端 runtime snapshot 读取worldType / playerCharacter / sceneHostileNpcs / storyHistory / prompt context;旧worldType / character / history / context字段仅保留兼容,不作为正式主链来源。 runtime_chat_plain.rs与runtime_chat.rs同步支持sessionId,角色私聊、NPC 对话、NPC 单轮聊天、招募对话的 prompt context 也由后端快照投影;前端只继续提交对话草稿、目标角色、玩家发言和必要 UI 临时项。src/hooks/rpg-runtime-story/storyContextBuilder.ts缩减为 session 元信息适配器,不再推导conversationSituation / conversationPressure / NPC disclosure / partyRelationshipNotes / scene pressure等正式上下文。src/services/aiService.ts在存在runtimeSessionId时,story initial/continue 只提交 session 轻量 payload;聊天接口只附带sessionId与对话输入,不再上传完整StoryGenerationContext。src/hooks/rpg-runtime-story/sessionActions.ts领取任务奖励时不再运行前端chapterDirector / echoMemory,只保留旧 UI 层奖励展示所需的本地字段;章节和storyEngineMemory.currentChapter等正式叙事字段等待后端 action snapshot 刷新。src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.tsNPC 聊天闭合后不再调用前端 scene act runtime 推进storyEngineMemory.currentSceneActState,也不再把deferredRuntimeState.storyEngineMemory写回正式GameState。src/hooks/rpg-runtime-story/choiceActions.ts兼容旧deferredRuntimeState时只允许采用场景字段,不再从 story moment 写入storyEngineMemory。
新增验证:
cargo test -p module-runtime-story-compat prompt_context --manifest-path server-rs\Cargo.tomlcargo test -p shared-contracts runtime_story_ai_request --manifest-path server-rs\Cargo.tomlcargo test -p api-server runtime_story_initial_uses_server_snapshot_prompt_context_when_session_id_present --manifest-path server-rs\Cargo.tomlcargo check -p api-server --manifest-path server-rs\Cargo.toml --message-format shortnpm run test -- src/services/ai.test.ts src/hooks/rpg-runtime-story/storyRequestCoordinator.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.test.tsxnpm 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.tsnpx 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(...) 提前拦截并本地拼装正式状态。
落地规则:
- 前端点击
camp_travel_home_scene后统一进入runServerRuntimeChoiceAction(...),只提交sessionId / clientVersion / functionId / optionText / runtimePayload。 server-rs/crates/api-server/src/runtime_story/compat.rs负责解析离营目标场景:- 优先使用 action payload 中的
targetSceneId。 - 内置世界按
playerCharacter.id + worldType映射到角色主场景。 - 自定义世界优先找玩家角色在
landmarks[].sceneNpcIds中绑定的地点,否则使用当前营地的forwardSceneId / connectedSceneIds或第一个 landmark。
- 优先使用 action payload 中的
- 后端 resolver 写入完整离营状态:
currentScenePresetcurrentEncounter / sceneHostileNpcs / npcInteractionActive / inBattleruntimeStats.scenesTraveledplayerX / playerFacing / animationState / playerActionMode / scrollWorldlastObserveSigns* / currentBattle* / spar* / activeCombatEffects
- 后端在目标场景上生成确定性的 encounter preview;内置场景至少带一个可交互 NPC,自定义场景复用
build_custom_scene_preset(...)中的 NPC 投影。 - 后端保存
storyHistory与currentStory,随后继续走project_story_engine_after_action(...)和持久化快照。 - 前端保留
camp_travel_home_scene作为 function id 与展示用 helper,但不再保留正式状态构造函数。
验证新增:
- 后端 route 测试覆盖
camp_travel_home_scene点击后 hydrated snapshot 已进入角色主场景、生成 encounter preview、递增scenesTraveled并持久化。 - 前端
choiceActions.test.ts覆盖camp_travel_home_scene即使命中旧 helper 判定,也只调用runServerRuntimeChoiceAction(...)。