Files
Genarrative/docs/technical/RPG_RUNTIME_STORY_ENGINE_BACKEND_MIGRATION_2026-04-28.md
2026-04-28 20:25:37 +08:00

12 KiB
Raw Permalink Blame History

RPG 运行时 Story Engine 后端迁移落地方案2026-04-28

0. 本轮目标

本轮只收口 RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md4.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/initialPOST /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.rsruntime_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 在存在 runtimeSessionIdstory 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. 后端保存 storyHistorycurrentStory,随后继续走 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(...)