This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -17,6 +17,10 @@
- [CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md](./CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md):自定义世界创作工具当前问题、体验断层和优化优先级审计。
- [AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md](./AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md)Agent 聊天、草稿生成、作品库存储与进入世界之间的断点、多 pipeline、冗余与未实装项审计。
- [CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md](./CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md):角色资产默认描述文本、正式图像/动作 prompt、共享模板与保留接口的分层与冗余审计。
- [RPG_RUNTIME_DIRECT_DRAFT_PROFILE_AUDIT_2026-04-25.md](./RPG_RUNTIME_DIRECT_DRAFT_PROFILE_AUDIT_2026-04-25.md)RPG 运行时进入世界时改为直读 Agent session 草稿 profile 的链路检查。
- [RPG_WORLD_DRAFT_EDIT_AUTOSAVE_OVERRIDE_AUDIT_2026-04-28.md](./RPG_WORLD_DRAFT_EDIT_AUTOSAVE_OVERRIDE_AUDIT_2026-04-28.md)RPG 世界草稿结果页编辑后被旧设定覆盖的前端本地态、session 真相源与自动保存链路审计。
- [engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md](./engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md)RPG 前端脚本中仍应迁到 `server-rs` / SpacetimeDB 的开局、快照、story engine、战斗、NPC/背包规则与创作残留后门审计。
- [engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md](./engineering/RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md)RPG 前端脚本后端迁移完成度复核,标明已完成项、已收口的结果页保存 normalize以及仍需收尾的 `camp_travel_home_scene` 前端专用旅行分支。
- [engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md):对 `2026-04-19` 工程清理审计的当前仓库复核,区分已完成项、仍存边界问题和新的热点迁移。
- [engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md):未引用垃圾、旧入口残留、前后端双份真相与后端迁移项的专项审计。

View File

@@ -0,0 +1,233 @@
# RPG 世界草稿编辑后被旧设定覆盖问题审计 2026-04-28
## 1. 问题摘要
当前 RPG 世界草稿结果页存在一条明显的“本地编辑态与 Agent session 真相源分叉”问题链:
1. 用户在结果页编辑世界设定时,前端只更新本地 `generatedCustomWorldProfile`
2. Agent 草稿会话里的 `draftProfile / resultPreview.preview` 并不会随着这次编辑同步更新。
3. 自动保存触发时,系统又会优先重新拉取 session 最新快照,并以 session 返回的 profile 作为最终保存内容。
4. 如果 session 里仍是编辑前旧设定,那么前端刚刚改过的内容就会被旧快照覆盖回来。
所以,这个问题表面看起来像“自动保存覆盖了我刚保存的内容”,本质上是:
- 结果页编辑写到了前端内存态;
- 自动保存落库前又改为优先相信 session 真相;
- 但 session 真相本身没有承接这次编辑。
## 2. 当前主链证据
### 2.1 结果页编辑只改前端本地 profile
`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx:2637-2640`
- `RpgCreationResultView``onProfileChange` 只调用:
- `sessionController.setGeneratedCustomWorldProfile(normalizeAgentBackedProfile(profile))`
- 这里没有触发任何 `update_draft_card``sync_result_profile` 或其它后端写回动作。
这意味着结果页里的“保存修改”目前只是更新前端内存中的 `generatedCustomWorldProfile`
### 2.2 结果页展示数据优先来自 session 预览
`src/services/rpg-creation/rpgCreationPreviewAdapter.ts:27-33`
- `buildCustomWorldProfileFromAgentSession()` 当前优先读取:
- `session.resultPreview.preview`
- 若没有,再回退到:
- `session.draftProfile.legacyResultProfile`
也就是说Agent 结果页最终重新打开、重新同步或重新计算时,主数据源仍是 session 快照,而不是用户刚改过的前端本地对象。
### 2.3 自动保存前会先刷新 session并优先保存 session 返回的最新结果
`src/components/rpg-entry/useRpgCreationResultAutosave.ts:181-236`
- `syncAgentDraftResultProfile()` 在 session 与前端 profile 签名不一致时,不再把前端 profile 回写 session。
- 它只会执行:
- `syncAgentSessionSnapshot(activeAgentSessionId)`
- 然后把拉回来的 session profile 重新塞回:
- `setGeneratedCustomWorldProfile(latestProfile)`
`src/components/rpg-entry/useRpgCreationResultAutosave.ts:326-340`
- 自动保存延迟触发后,如果当前是 Agent 草稿结果页:
1. 先调用 `syncAgentDraftResultProfile(profileToSave)`
2. 再把 `syncedResult.profile ?? profileToSave` 作为最终要入库的 profile
也就是:
- 自动保存不是直接保存用户刚改的前端 profile
- 它会先“向 session 对齐”;
- 对齐后优先保存 session 返回的新 profile。
如果 session 里还是旧设定,那么保存和界面都会一起回滚到旧设定。
## 3. 当前测试口径也在强化这条行为
### 3.1 单测明确要求“不触发 sync_result_profile只保存 session 最新草稿”
`src/components/rpg-entry/useRpgEntryAgentDraftRestore.test.tsx:195-276`
这条测试验证的是:
1. 前端当前持有的是 `oldProfile`
2. `syncAgentSessionSnapshot()` 返回的是 `latestSession`
3. 自动保存最终必须保存 `latestSession.draftProfile`
4. 并且明确断言不应触发 `sync_result_profile`
这说明现有实现不是偶然漏掉了 session 写回,而是被当前测试和实现共同锁定为:
- “自动保存只刷新 session不回写前端结果页编辑态”
### 3.2 交互测试也要求“结果页自动保存优先保存 session 最新快照”
`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx:2867-3006`
这条测试进一步验证:
1. 结果页最终保存的对象应来自 `syncedSession.resultPreview.preview`
2. 不应触发 `sync_result_profile`
所以现在的覆盖行为不是单点 bug而是当前结果页保存架构的直接产物。
## 4. 系统层面的根因拆解
### 4.1 双真相源并存
当前至少同时存在两份“看起来都像真相”的数据:
1. 前端结果页本地 `generatedCustomWorldProfile`
2. Agent session 内的 `draftProfile / resultPreview.preview`
结果页编辑写前者,自动保存与恢复又优先读后者,所以只要两边没有同事务同步,就一定会出现覆盖风险。
### 4.2 编辑动作没有接入正式后端写回动作
仓库契约和 Rust 模块实际上已经存在两条正式动作:
- `update_draft_card`
- `sync_result_profile`
证据:
- `packages/shared/src/contracts/rpgAgentActions.ts`
- `server-rs/crates/spacetime-module/src/custom_world/mod.rs`
其中:
- `update_draft_card` 适合基于卡片/section 的结构化修改写回 `draftProfile`
- `sync_result_profile` 适合把结果页完整 profile 回写 session 并重建 preview
但当前结果页 `onProfileChange` 没有接入这两条正式链路,导致“编辑成功”只停留在本地内存态。
### 4.3 自动保存策略与编辑链路不一致
自动保存的当前设计目标是正确的:
- 不再轻易把旧前端快照反向污染 session
- 优先保存 session 最新快照,避免把陈旧 preview 写进作品库
但前提应该是:
- 结果页编辑本身已经先进入 session 真相源
现在前提不成立,于是自动保存的“防旧快照污染”策略,反过来变成了“用 session 旧值覆盖用户刚编辑的新值”。
### 4.4 结果页定位混乱
当前结果页同时承担了两种角色:
1. 预览/发布页
2. 可深度编辑的草稿编辑器
但现有数据链路更偏向第 1 种:
- 数据来自 session preview
- 自动保存优先对齐 session preview
而 UI 行为却提供了第 2 种能力:
- 用户可以直接改世界、角色、场景等内容
两种定位没有统一,最终表现就是“能编辑,但编辑不能稳定进入主真相源”。
## 5. 用户视角下的实际故障表现
从用户体感看,当前问题会表现为以下几种:
1. 刚改完字段,短暂显示新内容,随后自动回弹为旧内容。
2. 页面提示“已自动保存”,但重新进入草稿页后仍然是旧设定。
3. 某些字段改了能停留一会儿,触发自动保存或 session 刷新后又被覆盖。
4. 用户会误以为“保存按钮坏了”或“自动保存有缓存问题”,但真实原因是保存目标和展示真相源不一致。
## 6. 影响范围
这个问题不只影响“世界简介文本”,而是整个结果页编辑体系:
1. 世界基础信息编辑
2. 角色编辑
3. 场景编辑
4. 封面/派生内容依赖当前 profile 的场景
5. 结果页返回、恢复、自动打开、发布前预览一致性
只要编辑发生在 `generatedCustomWorldProfile`,但没有进入 session 真相源,就都有同类风险。
## 7. 建议修复方向
### 7.1 第一优先级:统一结果页编辑的唯一真相源
推荐二选一,不要继续混用:
1. 结果页只做预览,不允许直接编辑复杂设定
2. 结果页继续允许编辑,但每次保存必须先落到 session 真相源,再更新本地显示
按当前产品形态,更合理的是第 2 条。
### 7.2 如果保留结果页编辑,建议采用的正式链路
1. 世界/角色/场景编辑保存时,优先调用 Rust/SpacetimeDB 的正式动作写回 session。
2. 能用 `update_draft_card` 的地方尽量走结构化 section 更新。
3. 对无法被 card section 覆盖的完整 profile 级编辑,再评估是否保留 `sync_result_profile`,或新增更细粒度 reducer。
4. session 写回完成后,再重新读取 session 并刷新结果页。
5. 自动保存只负责“把已经写进 session 的最新真相同步到作品库”,不要再承担“猜测该保存前端还是 session”的职责。
### 7.3 自动保存策略需要降级为“作品库存档层”不要兼任“session 真相协调层”
当前自动保存同时承担:
1. 防止旧前端快照污染 session
2. 选择最终保存哪份 profile
3. 刷新结果页显示
职责太重,也导致覆盖问题难以定位。
建议改为:
1. 编辑保存阶段:负责写 session
2. 自动保存阶段:只负责把 session 已确认真相落作品库
3. 结果页渲染阶段:只读 session 最新快照
### 7.4 需要补的测试口径
当前测试主要在保护“不要再触发 `sync_result_profile` 污染 session”。
后续修复后,至少要补以下测试:
1. 用户编辑世界基础字段后session `draftProfile / resultPreview` 会同步更新。
2. 自动保存不会把 session 旧值覆盖到刚编辑的新值上。
3. 刷新页面或重新进入结果页后,看到的是编辑后的新设定。
4. 进入世界、发布世界、继续扩展时,消费的是同一份最新 session 真相。
## 8. 结论
这次问题的核心结论是:
1. 你的判断“像是自动保存问题”是对的,但更准确地说,是“自动保存对齐 session 时,覆盖了未写回 session 的本地编辑态”。
2. 当前结果页编辑没有接上正式的 session 写回链路,这是第一根断点。
3. 当前自动保存被设计成优先信任 session 最新快照,这是第二根断点。
4. 两个断点叠加后,就形成了“编辑后又自动变回原设定”的现象。
如果后续要真正修掉这个问题,重点不该是单独调 debounce 时间或加一个“防抖保存中”提示,而是:
- 把结果页编辑动作重新接回 Rust/SpacetimeDB 的 session 真相源;
- 让自动保存只负责作品库存档,不再替代编辑写回链路做裁决。

View File

@@ -10,21 +10,25 @@
这一版是第六批落地记录,聚焦删除无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本并补齐后端运行时 function catalog 契约覆盖。
3. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md)
这一版是第五批落地记录,聚焦旧命名 re-export、空路由骨架、旧发布服务、前端 prompt 镜像与无入口编辑器壳层的物理删除。
4. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
4. [RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md](./RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md)
这一版专项扫描 `src/` 下 RPG 开头脚本明确运行时开局、快照、story engine、战斗后处理、NPC/背包规则和创作链残留后门中应迁到 `server-rs` 的逻辑。
5. [RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md](./RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_COMPLETION_CHECK_2026-04-28.md)
这一版复核 RPG 前端脚本后端迁移完成度确认开局、快照、存档、NPC、背包/锻造、结果页保存前 normalize 与角色资产 prompt 主链已收口,同时标出 `camp_travel_home_scene` 前端专用旅行分支仍未完全迁完。
6. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 `server-rs` 的运行时、鉴权、生成编排与本地真相残留。
5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。
6. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
8. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。
7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md)
9. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md)
这一版是第二批落地记录,聚焦旧主流程壳层、旧 bootstrap 和旧 inventory / forge / equipment flow Hook 的正式出清。
8. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
10. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
这一版是第一批落地记录聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。
9. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
11. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
这一版是面向当前仓库状态的优化点盘点,适合直接拿来排优先级和拆执行批次。
10. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
12. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
这一版是对 `2026-04-19` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。
11. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
13. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。
## 融合结论

View File

@@ -0,0 +1,650 @@
# RPG 前端脚本后端迁移审计2026-04-28
## 0. 审计目标
这份文档只回答一个问题:
**当前 `src/` 下 RPG 开头或 RPG 目录内的前端脚本中,哪些逻辑已经越过“前端只做表现”的边界,应该继续迁移到 `server-rs` / SpacetimeDB 后端。**
本轮只做审计与迁移拆分,不改业务代码。
## 1. 审计范围
本轮扫描范围:
1. `src/RpgRuntimeApp.tsx`
2. `src/components/rpg-*/*.ts`
3. `src/components/rpg-*/*.tsx`
4. `src/hooks/rpg-*/*.ts`
5. `src/services/rpg-*/*.ts`
6. `src/services/rpgRuntimeChatTypes.ts`
不把测试文件作为迁移对象,但会参考测试暴露的当前行为。
本轮依据:
1. `docs/reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md`
2. `docs/audits/engineering/FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md`
3. `docs/technical/RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md`
4. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
5. `spacetimedb-concepts` / `spacetimedb-rust` / `spacetimedb-typescript` skill 约束
## 2. 判断标准
### 2.1 应迁后端
只要逻辑满足下面任一项,就不应继续以浏览器前端作为正式真相:
1. 生成或解释 `GameState`、任务、背包、装备、NPC 状态、战斗状态、剧情记忆。
2. 决定开局初始状态、场景落点、遭遇、初始物品、初始装备。
3. 决定 action 是否合法、价格、数量、奖励、掉落、复活、场景推进。
4. 组装正式 AI prompt / story context / fallback generation。
5. 持久化完整运行时快照,或让前端上传整份 `GameState` 作为后端写入依据。
6. 对服务端返回的快照做业务补丁,补齐战斗阵型、场景跳转、任务推进等正式状态。
7. 解释 Agent session / draft / result preview 哪份才是创作真相。
### 2.2 可留前端
下面这些可以继续留在前端:
1. 面板开关、tab、modal、loading、error、按钮禁用展示。
2. 动画播放、镜头、过场、打字机效果、临时视觉态。
3. API client、请求封装、SSE 文本消费。
4. 纯展示 view model 适配,但不能改变正式业务含义。
5. 用户正在编辑的表单草稿,但保存、校验、合并、发布门禁以后端为准。
## 3. 结论先行
当前 RPG 前端脚本中仍有 9 类逻辑应该迁移到后端:
1. `P0` 运行时开局 `GameState` 装配。
2. `P0` runtime story 网关中的“客户端带快照解析”和快照补丁。
3. `P0` 前端自动保存整份运行时快照。
4. `P0` 前端 story engine / chapter / world mutation / prompt context 编排。
5. `P0` 战斗胜负后处理、死亡复活、战斗后章节推进。
6. `P1` NPC 交易、送礼、价格、数量与库存校验。
7. `P1` 背包、装备、锻造可用性与配方视图。
8. `P1` RPG 创作 profile 生成的非浏览器 legacy AI 回退。
9. `P1` RPG 创作结果页自动保存、session/result preview 真相优先级与 legacy preview fallback。
一句话判断:
**前端已经大面积开始调用 server-rs但仍保留了“后端兼容不完整时由前端补真相”的模式这部分需要继续收口否则 SpacetimeDB 表和 reducer 永远无法成为唯一运行时真相源。**
## 4. 高优先级迁移项
## 4.1 `P0` 运行时开局 `GameState` 装配仍在前端
### 代码证据
主要文件:
1. `src/hooks/rpg-session/useRpgSessionBootstrap.ts`
关键逻辑:
1. `PLAYER_BASE_MAX_HP = 180`
2. `mergeStarterInventoryItems(...)`
3. `normalizeExplicitStarterCategory(...)`
4. `inferExplicitStarterSlot(...)`
5. `buildExplicitCustomWorldRoleStarterState(...)`
6. `createInitialGameState()`
7. `handleCustomWorldSelect(...)`
8. `handleCharacterSelect(...)`
对应代码表现:
1.`58` 行到第 `151` 行:前端把角色初始物品编译成 `InventoryItem`,推断装备槽,并直接生成 `runtimeMetadata`
2.`185` 行到第 `235` 行:前端创建完整初始 `GameState`
3.`517` 行到第 `563`前端选择世界时重置运行时上下文、progression、story memory、战斗态。
4.`572` 行到第 `710` 行:前端选角时决定开局场景、开局 NPC、初始 NPC state、初始装备、血蓝、货币、背包、任务、队伍、战斗字段。
### 为什么应迁
这些不是表现层逻辑,而是“新开局正式状态”的创建权。只要开局状态仍由浏览器本地装配,后端就只能被动接收一份客户端快照,无法成为 runtime session 的唯一真相。
### 后端落点
建议收口到:
1. `server-rs/crates/api-server/src/runtime_story.rs`
2. `server-rs/crates/api-server/src/story_sessions.rs`
3. `server-rs/crates/module-runtime/src/lib.rs`
4. `server-rs/crates/module-story/src/lib.rs`
5. `server-rs/crates/module-inventory/src/lib.rs`
6. `server-rs/crates/module-progression/src/lib.rs`
7. `server-rs/crates/module-runtime-story-compat/src/game_state.rs`
SpacetimeDB 方向:
1. 新增或扩展 `begin_rpg_runtime_session` reducer。
2. 由 reducer 基于 `ctx.sender()`、作品 profile id、角色 id 创建 runtime session。
3. 在表中写入初始场景、角色状态、背包、装备、NPC 状态、任务、progression。
4. 前端只调用 reducer / API 并订阅或读取后端返回的 session view model。
注意:外部 AI / 网络调用不能放进 reducer如果开局需要 LLM应由 `api-server` / `platform-llm` 完成生成,再把确定结果写入 SpacetimeDB。
## 4.2 `P0` runtime story 网关仍要求客户端携带快照解析
### 代码证据
主要文件:
1. `src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts`
关键逻辑:
1.`112` 行到第 `123` 行:`buildRuntimeSnapshotRequest(...)` 把本地 `gameState``currentStory` 一并提交给后端。
2.`125` 行到第 `213` 行:`bridgeServerSceneTravelSnapshot(...)` 在前端补齐服务端旅行快照。
3.`216` 行到第 `317` 行:`bridgeServerNpcBattleSnapshot(...)` 在前端补齐 NPC 战斗阵型。
4.`323` 行到第 `341` 行:拉取 option catalog 时仍基于客户端快照。
5.`387` 行到第 `430` 行:正式动作结算后继续由前端对服务端快照做桥接修补。
### 为什么应迁
这类桥接逻辑意味着服务端返回结果不是完整业务真相,前端还在做二次裁决:
1. 推断下一场景。
2. 补齐场景 preset。
3. 补齐遭遇预览。
4. 补齐任务推进。
5. 补齐 NPC 战斗阵型。
6. 改写 `GameState` 中的战斗、场景、任务字段。
这些都应该由后端 runtime action resolver 在同一个事务边界内完成。
### 后端落点
建议收口到:
1. `server-rs/crates/api-server/src/runtime_story/compat/game_state.rs`
2. `server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs`
3. `server-rs/crates/api-server/src/runtime_story/compat/equipment_actions.rs`
4. `server-rs/crates/api-server/src/runtime_story/compat/quest_actions.rs`
5. `server-rs/crates/module-runtime-story-compat/src/options.rs`
6. `server-rs/crates/module-runtime-story-compat/src/battle.rs`
7. `server-rs/crates/module-runtime-story-compat/src/view_model.rs`
迁移后前端应该只提交:
1. `sessionId`
2. `functionId`
3. `runtimePayload`
4. 当前 UI 所需的弱表现参数
后端应该基于已存 session state 解析动作,不再要求前端上传完整 `GameState`
### 本轮落地口径2026-04-28
本轮先收口 `runtime story` 网关这条 P0 主链:
1. 前端 `rpgRuntimeStoryGateway.ts` 不再构造或上传 `snapshot.gameState / currentStory`
2. 前端 `rpgRuntimeStoryClient.ts` 的状态读取统一走 `GET /api/runtime/story/state/:sessionId`,动作结算统一走 `POST /api/runtime/story/actions/resolve` 且请求体只包含 `sessionId / clientVersion / action`
3. `idle_travel_next_scene` 的下一场景、场景 preset、遭遇清理、战斗清理与旅行计数由 `server-rs/crates/api-server/src/runtime_story/compat.rs` 在持久化快照上完成。
4. `npc_fight / npc_spar` 的战斗阵型、`sceneHostileNpcs`、战前 encounter 归档与战斗字段由 `server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs` 在后端完成。
5. 前端只消费后端返回的 hydrated snapshot 和 presentation不再保留 `bridgeServer*Snapshot` 业务补丁。
### 本轮落地验收2026-04-28
1. `rpgRuntimeStoryGateway.ts` 已删除 `buildRuntimeSnapshotRequest``bridgeServerSceneTravelSnapshot``bridgeServerNpcBattleSnapshot` 以及 NPC 战斗/旅行快照补丁依赖option catalog 和 action resolve 都只提交 `sessionId / clientVersion / action / payload`
2. `rpgRuntimeStoryClient.ts` 已移除前端 `RuntimeStorySnapshotRequest` 入参;状态读取固定为 `GET /api/runtime/story/state/:sessionId`,动作结算请求体不再带 `snapshot` 字段。
3. `server-rs` 已覆盖 `idle_travel_next_scene``npc_fight``npc_spar` 的后端快照补齐测试,并将 route 级测试改为从服务端持久化 session 读取,不再通过旧 save 端点上传整份前端快照铺垫。
4. 已执行验收:`cargo check -p api-server --message-format short``cargo test -p api-server runtime_story_ -- --nocapture``npm run test -- src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts`
5. 搜索确认:`src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts``src/services/rpg-runtime/rpgRuntimeStoryClient.ts` 不再包含 `/state/resolve``buildRuntimeSnapshotRequest``bridgeServer*``params.snapshot` 或动作请求上传 `snapshot.gameState/currentStory` 的路径。
## 4.3 `P0` 自动保存仍由前端上传整份运行时快照
### 代码证据
主要文件:
1. `src/hooks/rpg-session/useRpgSessionPersistence.ts`
2. `src/services/rpg-runtime/rpgSnapshotClient.ts`
关键逻辑:
1. `useRpgSessionPersistence.ts``12` 行到第 `23` 行:前端判断哪些运行态可以入正式存档。
2.`90` 行到第 `150` 行:前端把 `gameState / bottomTab / currentStory` 作为 payload 写入远端。
3.`216` 行到第 `234` 行:前端监听本地 `gameState` 变化并自动保存。
4.`236` 行到第 `263` 行:手动存档也上传整份本地快照。
5. `rpgSnapshotClient.ts``31` 行到第 `48` 行:`putRpgSaveSnapshot(...)` 直接发送 `SavedGameSnapshotInput`
### 为什么应迁
运行时存档不是普通表单保存它是玩家进度、任务、背包、战斗、NPC、故事状态的正式真相。当前模式仍是“前端本地状态变化 -> 上传整份快照 -> 后端持久化”,后端缺少对状态版本、动作来源、事务一致性的最终解释权。
### 后端落点
建议改成:
1. 后端 action resolver 每次动作结算时事务性写入 session state。
2. 自动保存只变成后端已有 session state 的归档或 checkpoint。
3. 前端最多提交 `sessionId``saveSlot``bottomTab` 等非业务 UI 状态。
4. `runtime_save.rs` 负责快照归档,不再信任浏览器传入的整份 `GameState`
涉及表结构时同步检查并更新 `migration.rs`
## 4.4 `P0` story engine / chapter / world mutation 仍在前端编排
### 代码证据
主要文件:
1. `src/hooks/rpg-runtime-story/progressionActions.ts`
2. `src/hooks/rpg-runtime-story/storyContextBuilder.ts`
3. `src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.ts`
关键逻辑:
1. `progressionActions.ts``193` 行到第 `293` 行:前端生成章节任务并写入 `quests`
2.`295` 行到第 `395` 行:前端收集 story signals、推进 thread、同伴反应、章节、旅程 beat、阵营/世界 mutation、setpiece。
3.`644` 行到第 `782` 行:前端提交生成后状态、追加 story history、执行 AI 后再做 recovery。
4. `storyContextBuilder.ts``1` 行到第 `51` 行:前端引入大量 story engine director / graph / memory / prompt context 组件。
5. `useRpgRuntimeStoryController.ts``80` 行到第 `103` 行:前端把 `generateInitialStory / generateNextStep` 注入本地 story request runtime。
### 为什么应迁
这些逻辑不是 UI而是叙事引擎状态机
1. 章节任务发放。
2. 线索和 thread 更新。
3. 同伴关系变化。
4. 营地事件与 setpiece 触发。
5. 世界 mutation。
6. 生成上下文组织。
7. fallback / recovery。
这些都应作为后端 story session 的 reducer / domain service而不是 React hook 的本地副作用。
### 后端落点
建议收口到:
1. `server-rs/crates/api-server/src/runtime_story/compat/ai.rs`
2. `server-rs/crates/api-server/src/prompt/rpg/runtime_chat.rs`
3. `server-rs/crates/module-story/src/lib.rs`
4. `server-rs/crates/module-progression/src/lib.rs`
5. `server-rs/crates/module-runtime-story-compat/src/core.rs`
6. `server-rs/crates/module-runtime-story-compat/src/view_model.rs`
前端保留:
1. `currentStory` 展示。
2. loading / error。
3. SSE 增量文本。
4. 视觉过场。
## 4.5 `P0` 战斗后处理、死亡复活、章节推进仍在前端补真相
### 代码证据
主要文件:
1. `src/hooks/rpg-runtime-story/storyChoiceRuntime.ts`
2. `src/hooks/rpg-runtime-story/postBattleFlow.ts`
关键逻辑:
1. `storyChoiceRuntime.ts``1` 行到第 `6` 行仍引用掉落、背包合并等本地规则。
2.`304` 行到第 `390` 行:服务端动作返回后,前端仍判断死亡、等待复活、构造复活状态、构造战斗胜利后状态与 story。
3. `postBattleFlow.ts``86` 行到第 `105` 行:前端构造战斗胜利后的 `GameState`
4.`107` 行到第 `158` 行:前端推进 scene act 并构造战斗后 story / deferred options。
5.`161` 行到第 `205` 行:前端决定复活回第一场景、恢复血蓝、清战斗态、重建首幕 encounter preview。
### 为什么应迁
死亡、胜利、切磋完成、复活、掉落、战斗后章节推进都属于正式玩法结果。前端可以播死亡动画和过场,但不能决定:
1. 玩家是否死亡。
2. 玩家在哪里复活。
3. 复活后血蓝。
4. 战斗是否结束。
5. 任务或章节是否推进。
6. deferred options 里有哪些正式可选动作。
### 后端落点
建议收口到:
1. `server-rs/crates/module-combat/src/lib.rs`
2. `server-rs/crates/module-runtime-story-compat/src/battle.rs`
3. `server-rs/crates/module-runtime-story-compat/src/view_model.rs`
4. `server-rs/crates/api-server/src/story_battles.rs`
5. `server-rs/crates/api-server/src/runtime_story/compat/presentation.rs`
前端保留:
1. 根据后端返回的 `presentation.battle` 播动画。
2. 根据后端返回的 `nextStory` 展示文本和选项。
3. 根据后端返回的 `GameState` 或 view model 渲染血条、角色、敌人。
## 5. 中优先级迁移项
## 5.1 `P1` NPC 交易、送礼的价格和库存校验仍在前端
### 代码证据
主要文件:
1. `src/hooks/rpg-runtime-story/npcInteraction.ts`
关键逻辑:
1.`186` 行到第 `218` 行:前端读取 NPC / 玩家物品,计算交易单价、最大数量、数量 clamp。
2.`629` 行到第 `683` 行:前端按本地价格和库存判断买入 / 卖出是否允许,再提交后端。
3.`685` 行到第 `702` 行:前端检查礼物是否存在,并构造送礼 action。
### 当前判断
这条链已经比早期更好:正式交易和送礼会调用 `resolveRpgRuntimeChoice(...)` 走后端。但前端仍在承担“能不能买、能不能卖、价格是多少、数量是否合法”的第一层裁决。
### 迁移建议
后端应该:
1. 计算价格。
2. 校验库存和货币。
3. 校验 NPC 亲和度影响。
4. 原子更新玩家库存、NPC 库存、货币和 NPC state。
5. 返回可展示的交易 view model 与错误原因。
前端可以保留:
1. 当前选择的 itemId / quantity。
2. 后端返回价格的展示。
3. modal 开关和数量输入。
### 本次落地设计2026-04-28
为避免“前端只是少算了一处价格,但仍在提交前裁决库存”的半迁移,本次按下面边界收口:
1. `server-rs` 在 runtime story compat 状态桥中编译 `gameState.runtimeNpcInteraction` 派生 view包含当前 NPC、玩家货币、购买列表、出售列表、赠礼列表、服务端单价、服务端库存上限、赠礼好感增益与不可选原因。
2. `resolve_npc_trade_action(...)``resolve_npc_gift_action(...)` 仍是唯一正式结算入口,提交后重新校验 mode、itemId、quantity、NPC 库存、玩家背包数量与玩家货币;前端展示的 view 只作为 UI 提示,不作为可信输入。
3. 前端 `npcInteraction.ts` 不再读取本地 NPC / 玩家物品计算单价、总价、最大数量,也不再因为本地库存或货币不足提前阻断提交;确认时只提交 `{ mode, itemId, quantity }`,后端返回错误即展示错误。
4. 前端 `NpcModals.tsx` 只渲染 `runtimeNpcInteraction` 中的价格、库存和赠礼增益;按钮禁用仅保留“未选择条目 / 服务端 view 标记不可选 / 当前操作正在提交”的表现态,不再用浏览器本地价格和货币重新裁决。
5. 验收测试必须覆盖后端购买成功、NPC 库存不足拒绝、玩家货币不足拒绝、出售成功、玩家背包数量不足拒绝、赠礼成功、礼物不存在拒绝;前端测试覆盖确认交易/赠礼在本地库存或货币不满足时仍提交后端,由后端错误接管。
## 5.2 `P1` 背包、装备、锻造可用性与配方视图仍在前端
### 代码证据
主要文件:
1. `src/hooks/rpg-runtime-story/inventoryActions.ts`
关键逻辑:
1.`44` 行到第 `52` 行:前端基于 `playerInventory / playerCurrency / worldType` 计算 forge recipe views。
2.`97` 行到第 `180` 行:前端先查找物品 / 装备 / 配方,再提交后端 reducer 风格 action。
### 当前判断
正式动作已经走后端,这是正确方向。但 `forge recipe view` 和本地物品存在性判断仍会让前端表现出“它知道业务规则”的形态。
### 迁移建议
后端应该提供:
1. 当前玩家背包 view。
2. 当前装备 view。
3. 当前可锻造配方 view。
4. 每个动作的 disabled / reason。
前端只渲染后端 view并把用户选择提交给后端。
## 5.3 `P1` RPG 创作 profile 生成仍保留非浏览器 legacy AI 回退
### 迁移回填2026-04-28
已完成本项迁移:
1. `src/services/rpg-creation/rpgCreationGenerationClient.ts` 删除 `LegacyAiModule``loadLegacyAiModule``typeof window === 'undefined'` 分支。
2. `src/services/rpg-creation/rpgCreationGenerationClient.node.test.ts` 锁定 node 环境也只 mock `requestJson`,不再触发前端 legacy AI 模块。
3. `src/services/rpg-creation/index.ts` 删除 `generateLegacyCustomWorldProfile` 旧命名导出。
4. `server-rs/crates/api-server/src/app.rs` 挂载 `POST /api/runtime/custom-world/profile`
5. `server-rs/crates/api-server/src/custom_world.rs` 复用 `generate_custom_world_foundation_draft(...)` 生成 profile并补齐前端结果页所需稳定字段。
6. 迁移说明见 `docs/technical/RPG_CREATION_PROFILE_GENERATION_BACKEND_MIGRATION_2026-04-28.md`
### 迁移前历史证据
主要文件:
1. `src/services/rpg-creation/rpgCreationGenerationClient.ts`
关键逻辑:
1.`9` 行到第 `19` 行:前端 client 仍定义 `LegacyAiModule = typeof import('../ai')` 并动态加载旧 AI 模块。
2.`32` 行到第 `35` 行:非浏览器环境直接调用 `aiClient.generateCustomWorldProfile(...)`
3.`43` 行到第 `51` 行:浏览器环境才请求 `/api/runtime/custom-world/profile`
### 为什么应迁
这与 `RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md` 的目标冲突。即使该分支主要服务测试或 SSR它仍保留了“前端可持有 RPG 生成逻辑和 prompt”的技术后门。
### 迁移建议
1. 删除 `import('../ai')` 回退。
2. 非浏览器环境也应通过 `server-rs` client / test mock 调用后端 contract。
3. 若测试需要离线能力,应 mock `requestJson`,不要恢复前端生成链。
后端落点:
1. `server-rs/crates/api-server/src/custom_world.rs`
2. `server-rs/crates/api-server/src/custom_world_foundation_draft.rs`
3. `server-rs/crates/api-server/src/prompt/rpg/foundation_draft.rs`
4. `server-rs/crates/platform-llm/`
## 5.4 `P1` 创作结果页保存与 Agent session 真相优先级仍在前端编排
### 代码证据
主要文件:
1. `src/components/rpg-entry/useRpgCreationResultAutosave.ts`
2. `src/components/rpg-entry/useRpgEntryLibraryDetail.ts`
3. `src/services/rpg-creation/rpgCreationPreviewAdapter.ts`
关键逻辑:
1. `useRpgCreationResultAutosave.ts` 负责 profile signature、自动保存请求去重、`upsertRpgWorldProfile(...)`、Agent session refresh。
2. `useRpgEntryLibraryDetail.ts` 负责判断 draft work 应打开 Agent workspace、生成过程页还是结果页。
3. `rpgCreationPreviewAdapter.ts``resultPreview` 缺失时回退读取 `draftProfile.legacyResultProfile`
### 当前判断
这部分大多是创作 UI 编排,不能简单全迁。但下面三件事属于后端真相:
1. 保存前 profile normalize。
2. Agent session / result preview / legacyResultProfile 的优先级。
3. 发布门禁与草稿是否可进入结果页。
### 迁移建议
后端应该提供一个稳定的 `creation_result_view``work_detail_view`
1. 已标准化 profile。
2. 当前 session / work 状态。
3. 是否可发布。
4. 应打开的前端 stage。
5. 缺失或失败时的恢复指令。
前端只根据 view model 切页面,不再自行解释 session 阶段。
## 5.5 `P1` 角色资产工坊默认 prompt 与缓存合并规则仍在前端
### 本轮落地状态2026-04-28
已完成默认 prompt 与缓存合并规则迁移:
1. 新增 `server-rs/crates/api-server/src/prompt/rpg/role_asset_studio.rs`,作为角色资产工坊默认 prompt、legacy prompt 过滤、逐动作 prompt 缓存合并的后端主源。
2. 新增 `POST /api/runtime/custom-world/asset-studio/role/{character_id}/workflow`,由前端提交当前正在编辑的角色快照,后端返回合并后的 workflow view。
3. 新增 `PUT /api/runtime/custom-world/asset-studio/role/{character_id}/workflow`,复用 OSS JSON 缓存保存,并补齐 `animationPromptTextByKey` 持久化。
4. `RpgCreationRoleAssetStudioModalImpl.tsx` 不再调用 `buildDefaultRolePromptBundle`,也不再包含 legacy prompt 判断和缓存合并函数。
5. 删除 `src/prompts/customWorldRolePromptDefaults.ts` 与旧兼容 re-export避免 `src/` 继续持有角色资产默认 prompt 主源。
### 代码证据
主要文件:
1. `src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx`
2. `src/components/rpg-creation-asset-studio/useRoleVisualCandidateWorkflow.ts`
3. `src/components/rpg-creation-asset-studio/useRoleAnimationWorkflow.ts`
关键逻辑:
1. `RpgCreationRoleAssetStudioModalImpl.tsx``54` 行到第 `85` 行:前端合并默认动画 prompt、缓存 prompt、legacy prompt。
2.`564` 行到第 `591` 行:前端调用 `buildDefaultRolePromptBundle(baseRole)` 生成默认视觉 / 动作 prompt。
3.`807` 行到第 `830` 行:前端保存工作流缓存,包括 prompt、草稿、asset id、animation map。
4. `useRoleVisualCandidateWorkflow.ts``useRoleAnimationWorkflow.ts` 把 prompt 文本直接作为生成 payload。
### 当前判断
用户正在编辑的 prompt 草稿可以留在前端表单里,但默认 prompt 生成、legacy prompt 判断、缓存合并、生成参数默认值不应继续散落在 UI modal 内。
### 迁移建议
后端应该提供:
1. `GET /api/runtime/custom-world/asset-studio/role/:id/workflow`
2. `PUT /api/runtime/custom-world/asset-studio/role/:id/workflow`
3. `POST /api/runtime/custom-world/assets/role-visual-candidates`
4. `POST /api/runtime/custom-world/assets/role-animation`
并在 `server-rs/crates/api-server/src/prompt/rpg/` 或资产 prompt 模块中统一默认 prompt 生成。
前端只保留:
1. prompt 文本框。
2. 参考图上传 UI。
3. 候选图 / 动画预览。
4. 用户点击生成、应用、保存的交互入口。
## 6. 可保留在前端的 RPG 脚本
以下脚本当前主要承担表现层或 request client 职责,可以暂时保留:
1. `src/components/rpg-runtime-shell/*`
2. `src/components/rpg-runtime-panels/RpgAdventurePanel.tsx`
3. `src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx`
4. `src/components/rpg-entry/RpgEntryHomeView.tsx`
5. `src/components/rpg-entry/RpgEntryWorldDetailView.tsx`
6. `src/services/rpg-runtime/rpgRuntimeRequest.ts`
7. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts`
8. `src/services/rpg-runtime/rpgSnapshotClient.ts`
9. `src/services/rpg-creation/rpgCreationAgentClient.ts`
10. `src/services/rpg-creation/rpgCreationAssetClient.ts`
11. `src/services/rpg-creation/rpgCreationLibraryClient.ts`
12. `src/services/rpg-creation/rpgCreationRuntimeClient.ts`
但要注意:
1. client 文件可以保留请求封装,但不应继续加入 fallback 生成逻辑。
2. UI 文件可以根据后端 view model 展示 disabled / reason但不能本地重算业务可用性。
3. snapshot client 可以保留读取 / 删除 / 归档入口,但不应让前端上传整份 `GameState` 作为正式真相。
## 7. 推荐迁移顺序
### 第一阶段:收运行时真相
优先迁移:
1. `useRpgSessionBootstrap.ts`
2. `rpgRuntimeStoryGateway.ts`
3. `useRpgSessionPersistence.ts`
目标:
1. 后端创建 session。
2. 后端保存 session state。
3. runtime action 不再依赖客户端完整快照。
4. 前端不再补战斗 / 旅行快照。
### 第二阶段:收 story engine 与战斗后处理
优先迁移:
1. `progressionActions.ts`
2. `storyContextBuilder.ts`
3. `storyChoiceRuntime.ts`
4. `postBattleFlow.ts`
目标:
1. 后端统一处理 story history、thread、chapter、companion reaction、world mutation。
2. 后端统一处理死亡、复活、胜利、切磋完成、战斗后选项。
3. 前端只播放 presentation。
### 第三阶段:收 NPC / 背包 / 锻造可用性
优先迁移:
1. `npcInteraction.ts`
2. `inventoryActions.ts`
目标:
1. 后端提供交易 / 礼物 / 背包 / 锻造 view model。
2. 前端不再重算价格、数量、配方、动作合法性。
### 第四阶段:收创作链残留后门
优先迁移:
1. `rpgCreationGenerationClient.ts`
2. `useRpgCreationResultAutosave.ts`
3. `useRpgEntryLibraryDetail.ts`
4. `rpgCreationPreviewAdapter.ts`
5. `RpgCreationRoleAssetStudioModalImpl.tsx`
目标:
1. 移除 `import('../ai')`
2. 后端输出稳定 result/work view。
3. 后端统一角色资产工坊默认 prompt 和缓存合并规则。
## 8. 后端实现注意事项
### 4.3 落地记录:自动保存 checkpoint 化2026-04-28
本轮已完成 `自动保存仍由前端上传整份运行时快照` 的迁移:
1. `src/hooks/rpg-session/useRpgSessionPersistence.ts` 自动保存与手动保存不再构造 `gameState / currentStory` 上传体,只提交 `sessionId / bottomTab` checkpoint 元数据。
2. `src/services/rpg-runtime/rpgSnapshotClient.ts``PUT /api/runtime/save/snapshot` 请求体改为 `RuntimeSaveCheckpointInput`,前端保存链路只请求后端刷新 checkpoint。
3. `server-rs/crates/api-server/src/runtime_save.rs` 改为读取已存在的服务端 `runtime_snapshot`,校验 `runtimeSessionId` 一致后刷新 `savedAt / bottomTab / runtimeStats.playTimeMs / lastPlayTickAt`,不再信任浏览器传入完整运行态。
4. 旧式 `gameState / currentStory` 上传体会被 `PutRuntimeSaveCheckpointRequest``deny_unknown_fields` 拒绝无服务端快照、session 不一致、preview/test 快照都会返回冲突。
5. 本轮不涉及 SpacetimeDB 表结构变更,因此 `server-rs/crates/spacetime-module/src/migration.rs` 无需调整。
对应测试:
1. `src/services/rpg-runtime/rpgSnapshotClient.test.ts` 覆盖保存请求体不含 `gameState / currentStory`
2. `src/hooks/runtimeAuthGuards.test.tsx` 覆盖自动保存只提交 checkpoint。
3. `server-rs/crates/api-server/src/runtime_save.rs` 覆盖旧式整快照上传拒绝、缺少服务端快照冲突、session 不一致冲突、成功 checkpoint 沿用服务端快照真相。
按 SpacetimeDB 约束,后续落地时要遵守:
1. reducer 不返回数据,前端通过订阅 / 查询读取 view model。
2. reducer 使用 `ctx.sender()` 做鉴权,不信任前端传入身份。
3. reducer 必须确定性,不能访问网络、文件、外部随机。
4. LLM、OSS、图片生成等外部 I/O 放在 `api-server` / `platform-*` crate 中,再把确定结果写回 SpacetimeDB。
5. 前端调用 reducer 使用生成绑定和对象参数,不编辑生成代码。
6. 涉及表结构修改时同步更新 `migration.rs`
7. 修改后端代码后统一执行 `npm run api-server:maincloud`,并跑对应自动测试。
## 9. 最小验收标准
后续迁移完成后,前端 RPG 脚本应满足:
1. 搜索 `src/hooks/rpg-*` 不再出现创建完整 `GameState` 的逻辑。
2. runtime action 请求不再携带完整 `gameState`
3. 前端不再出现 `bridgeServer*Snapshot` 这类业务补丁函数。
4. 前端不再 `putSnapshot({ gameState, currentStory })`
5. 前端不再动态 `import('../ai')`
6. NPC 交易、送礼、背包、锻造的价格、合法性、结果都以后端 view model 为准。
7. 战斗死亡、复活、胜利后状态都以后端返回为准。
8. 创作结果页打开哪个 stage 由后端 work/session view 指示。
## 10. 一句话结论
当前 RPG 前端脚本最需要迁移的不是 UI 组件,而是仍残留在 hooks / gateway / client 里的:
**开局造状态、运行时带快照解析、前端补服务端快照、自动保存整份 GameState、story engine 编排、战斗后处理、NPC/背包/锻造规则裁决,以及 RPG 创作生成和结果预览的 legacy 后门。**

View File

@@ -0,0 +1,169 @@
# RPG 前端脚本后端迁移完成度核验2026-04-28
## 1. 核验结论
本次按 `RPG_FRONTEND_SCRIPT_BACKEND_MIGRATION_AUDIT_2026-04-28.md` 中列出的应迁后端项逐项检查当前代码。
结论:**应迁移项尚未全部迁移完成。**
当前状态:
1. `已完成`9 项。
2. `部分完成`1 项。
3. `未发现完全未启动`0 项。
本轮重新核查的变化:
1. 上次残留的 `RPG 创作结果页` 保存前 profile normalize 已完成后端化。
2. 新发现 `camp_travel_home_scene` 已登记为服务端 runtime function id但正式点击仍会被前端专用旅行分支提前拦截并本地拼装场景迁移状态。
## 2. 核验口径
### 2.1 判定为已完成
满足以下条件才记为已完成:
1. 前端不再构造正式业务状态。
2. 前端不再上传完整 `GameState` 作为后端写入依据。
3. 前端不再用本地规则裁决价格、库存、掉落、复活、章节推进、结果页真相优先级。
4. 前端只保留请求封装、UI 展示、表单草稿、loading/error、动画表现和按钮禁用展示。
### 2.2 判定为部分完成
满足以下任一情况记为部分完成:
1. 主链已经迁到 `server-rs`,但旧前端分支仍可被正式入口调用。
2. 后端已经提供 view/action但前端仍保留影响业务真相的 normalize、fallback 或 AI context 编排。
3. 后端只覆盖部分状态机,前端仍负责另一部分正式状态推进。
## 3. 逐项结果
| 原审计项 | 当前状态 | 核验结论 |
| --- | --- | --- |
| `P0` 运行时开局 `GameState` 装配 | 已完成 | 正式开局状态由 `server-rs/crates/api-server/src/runtime_story/compat/bootstrap.rs` 创建并持久化;前端 `useRpgSessionBootstrap.ts` 只保留选择页占位态和 `beginRpgRuntimeStorySession(...)` 调用。 |
| `P0` runtime story 网关客户端快照解析/补丁 | 已完成 | `rpgRuntimeStoryGateway.ts` 不再有 `buildRuntimeSnapshotRequest` / `bridgeServer*Snapshot``rpgRuntimeStoryClient.ts` 读取 `/state/:sessionId`,动作提交 `/actions/resolve`,不再上传完整 `snapshot.gameState/currentStory`。 |
| `P0` 自动保存整份运行时快照 | 已完成 | `useRpgSessionPersistence.ts` / `rpgSnapshotClient.ts` 保存链路只提交 `sessionId/bottomTab` checkpoint`runtime_save.rs` 从服务端已有快照刷新 checkpoint并测试拒绝旧式完整快照上传。 |
| `P0` story engine / chapter / world mutation / prompt context 编排 | 部分完成 | 后端已有 `project_story_engine_after_action(...)``build_runtime_story_prompt_context(...)``/story/initial``/story/continue``sessionId` 时只从服务端 snapshot 投影 world / character / history / prompt context`camp_travel_home_scene` 已是服务端 function id前端仍先走本地 `runCampTravelHomeChoice(...)` 拼装场景迁移、encounter preview 和 runtimeStats。 |
| `P0` 战斗胜负后处理、死亡复活、战斗后章节推进 | 已完成 | `battle_* / inventory_use` 正式点击统一走 `runServerRuntimeChoiceAction(...)` 与后端 `/actions/resolve``storyChoiceContinuation.ts` 对战斗 / 逃脱 / 物品动作加硬保护,不再裁决掉落、任务推进、死亡复活或战后 story`postBattleFlow.ts` 正式状态构造函数已删除。 |
| `P1` NPC 交易/送礼价格数量库存校验 | 已完成 | 前端 `npcInteraction.ts` 已改为消费 `runtimeNpcInteraction` view 并只提交 `{ mode, itemId, quantity }`;后端 `npc_actions.rs` / `npc_support.rs` 负责价格、库存、货币、赠礼好感和原子更新。 |
| `P1` 背包/装备/锻造可用性与配方视图 | 已完成 | 前端 `inventoryActions.ts` 读取 `loadRpgRuntimeInventoryView(...)`,根据后端 action/view 提交;后端 `view_model.rs` / `forge.rs` 生成背包、装备槽、配方、`canCraft/enabled/reason`。 |
| `P1` RPG 创作 profile 生成 legacy AI 回退 | 已完成 | `rpgCreationGenerationClient.ts` 只调用 `/api/runtime/custom-world/profile`,不再动态 `import('../ai')` 或导出 `generateLegacyCustomWorldProfile`。 |
| `P1` 创作结果页保存与 Agent session/result preview 真相优先级 | 已完成 | 后端已提供 `GET /api/runtime/custom-world/agent/sessions/:sessionId/result-view`,统一 `targetStage/profileSource/canAutosaveLibrary/canSyncResultProfile`,前端不再直接读取 `legacyResultProfile`;保存前 canonicalize 已迁到 `server-rs``normalizeRpgEntryAgentBackedProfile(...)` 现在只透传兼容旧导入。 |
| `P1` 角色资产工坊默认 prompt 与缓存合并规则 | 已完成 | 默认 prompt、legacy prompt 过滤、逐动作缓存合并已在 `server-rs/crates/api-server/src/prompt/rpg/role_asset_studio.rs`;前端 modal 只调用 workflow API、保存用户草稿和发起生成/发布。 |
## 4. 仍未完成的具体收尾点
### 4.1 story engine / prompt context 主链已完成,但 `camp_travel_home_scene` 仍残留前端正式分支
当前后端已经处理动作结算后的确定性 story projector
1. `server-rs/crates/module-runtime-story-compat/src/story_engine.rs`
2. `server-rs/crates/api-server/src/runtime_story/compat.rs`
本轮已补齐 prompt context 后端 projector
1. `server-rs/crates/module-runtime-story-compat/src/prompt_context.rs`
2. `server-rs/crates/api-server/src/runtime_story/compat.rs`
3. `server-rs/crates/api-server/src/runtime_chat.rs`
4. `server-rs/crates/api-server/src/runtime_chat_plain.rs`
完成状态:
1. 前端不再决定 `conversationSituation``conversationPressure`、NPC 对话上下文、party relationship notes、scene pressure 文本等正式 prompt context。
2. 前端 story initial / continue 在有 `runtimeSessionId` 时只提交 `sessionId / clientVersion / choice / lastFunctionId / requestOptions` 等轻量字段。
3. 后端从已持久化 runtime snapshot 投影 `worldType / playerCharacter / sceneHostileNpcs / storyHistory / context`,旧 payload 字段只保留兼容。
4. 角色私聊、NPC 对话、NPC 单轮聊天、NPC 招募对话已支持 `sessionId`,有 session 时上下文同样以后端 snapshot 为准。
5. 前端奖励领取、NPC 聊天闭合与旧 `deferredRuntimeState` 兼容分支不再写入 `storyEngineMemory`章节、scene act、thread、world mutation 等正式叙事记忆只以后端快照为准。
本轮验收:
1. `cargo test -p module-runtime-story-compat prompt_context --manifest-path server-rs\Cargo.toml` 覆盖后端 prompt context projector 对场景、NPC 披露阶段、对话压力和关系态度的投影。
2. `cargo test -p shared-contracts runtime_story_ai_request --manifest-path server-rs\Cargo.toml` 覆盖 story AI 请求可只携带 `sessionId` 的共享契约。
3. `cargo test -p api-server runtime_story_initial_uses_server_snapshot_prompt_context_when_session_id_present --manifest-path server-rs\Cargo.toml` 覆盖 `/story/initial` 在 session 模式下以后端快照覆盖浏览器传入的 world / character / context。
4. `npm run test -- src/services/ai.test.ts src/hooks/rpg-runtime-story/storyRequestCoordinator.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.test.tsx` 覆盖前端 story / chat 请求在 session 模式下只发送轻量 payload。
5. `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` 覆盖前端旧 UI 分支不再回写后端拥有的 `storyEngineMemory`
重新核查新增残留:
1. `packages/shared/src/contracts/rpgRuntimeStoryAction.ts` 已把 `camp_travel_home_scene` 纳入 `TASK5_RUNTIME_FUNCTION_IDS` / `SERVER_RUNTIME_FUNCTION_IDS`
2. `server-rs/crates/api-server/src/runtime_story/compat.rs` 已有 `camp_travel_home_scene` resolver 分支,但当前只清理 encounter并未承接前端旧分支里的目标场景、encounter preview 与完整离营故事提交。
3. `src/hooks/rpg-runtime-story/choiceActions.ts` 仍在 `isRpgRuntimeServerFunctionId(...)` 之前判断 `isCampTravelHomeOption(...)`,并调用 `runCampTravelHomeChoice(...)`
4. `src/hooks/rpg-runtime-story/storyChoiceRuntime.ts``runCampTravelHomeChoice(...)` 会在浏览器中决定目标场景、清理战斗/遭遇、递增 `scenesTraveled`、构造 encounter preview并通过 `commitGeneratedStateWithEncounterEntry(...)` 写入后续故事。
影响:
1. 这条链不是纯动画表现,而是正式场景迁移、运行时统计、遭遇预览和后续故事提交。
2. 它已经具备服务端 function id 身份,却没有统一走 `/api/runtime/story/actions/resolve`,因此仍不满足“前端只提交 action后端返回 hydrated snapshot”的边界。
### 4.2 本地战斗 continuation 已收口到后端
当前后端已经有战斗终局收口:
1. `server-rs/crates/module-runtime-story-compat/src/post_battle.rs`
2. `server-rs/crates/module-runtime-story-compat/src/battle.rs`
3. `server-rs/crates/api-server/src/runtime_story/compat.rs`
本轮已完成前端旧正式分支收口:
1. `src/hooks/rpg-runtime-story/choiceActions.ts` 不再保留 `shouldResolveCombatChoiceLocally(...)`,所有服务端 function id 都统一进入 `runServerRuntimeChoiceAction(...)`
2. `src/hooks/rpg-runtime-story/storyChoiceContinuation.ts``battle_* / inventory_use` 以及被分类为 `battle / escape` 的动作加硬保护,误入时只报错回退,不会写掉落、任务、复活或战后 story。
3. `src/hooks/rpg-runtime-story/storyChoiceRuntime.ts` 删除本地敌对 NPC 掉落 reward helper不再调用 `rollHostileNpcLoot(...)` / `addInventoryItems(...)` 构造正式战斗奖励。
4. `src/hooks/rpg-runtime-story/postBattleFlow.ts` 与对应测试删除,前端不再保留 `buildPostBattleVictoryState(...)``buildPostBattleVictoryStory(...)``buildRevivedFirstSceneState(...)``buildDeathStory(...)` 作为正式状态构造入口。
已消除风险:
1. `battle_* / inventory_use` 不再因 `inBattle``currentBattleNpcId`、可见 story option 等状态回落到本地结算。
2. 本地 continuation 不再调用敌对 NPC 掉落、背包合并、敌对 NPC 任务推进。
3. 前端不再构造死亡复活状态、胜利后 story、deferred options 和章节推进。
本轮验收:
1. `choiceActions.test.ts` 覆盖 `battle_use_skill``battle_attack_basic` stale option、`inventory_use` 均只调用后端 resolver不触发 `buildResolvedChoiceState(...)` / `playResolvedChoice(...)`
2. `storyChoiceRuntime.test.ts` 保留服务端 battle presentation 验收,确认胜利 / 失败最终采用服务端 hydrated snapshot。
3. 搜索确认 `src/hooks/rpg-runtime-story` 不再包含 `shouldResolveCombatChoiceLocally``buildPostBattleVictory*``buildRevivedFirstSceneState``buildDeathStory``buildHostileNpcBattleReward`
### 4.3 创作结果页保存前 normalize 已完成后端化
后端已经负责 Agent result-view
1. `server-rs/crates/api-server/src/custom_world.rs`
2. `packages/shared/src/contracts/rpgCreationResultView.ts`
3. `src/services/rpg-creation/rpgCreationAgentClient.ts`
重新核查结果:
1. `src/components/rpg-entry/rpgEntryShared.ts`
2. `src/components/rpg-entry/useRpgCreationResultAutosave.ts`
3. `server-rs/crates/api-server/src/custom_world.rs`
当前完成状态:
1. `normalizeRpgEntryAgentBackedProfile(...)` 现在直接返回原始 `profile`,注释明确保存前 canonicalize 已迁到 `server-rs`
2. `stringifyRpgEntryAgentBackedProfile(...)` 现在只做 `JSON.stringify(profile)`,不再触发前端 normalize。
3. `put_custom_world_library_profile(...)` 写入作品库前调用 `canonicalize_custom_world_library_profile_payload(payload.profile)`
4. `serialize_sync_result_profile_action_payload(...)` 会在 Agent `sync_result_profile` action payload 中对 `profile` 执行 `canonicalize_custom_world_profile_before_save(...)`
5. 后端测试 `sync_result_profile_payload_is_canonicalized_on_server``custom_world_library_profile_payload_is_canonicalized_on_server` 已覆盖保存前 canonicalize。
本项不再计为未迁移残留。
## 5. 已完成项的保留边界
以下前端残留可以保留,不视为未迁移:
1. `useRpgSessionBootstrap.ts``createSelectionGameState()`:只服务选择页占位,正式开局不使用它造运行时真相。
2. `NpcModals.tsx` 的数量 stepper、价格展示和按钮禁用只消费后端 view不重新裁决价格或库存。
3. `inventoryActions.ts``submitInventoryAction(...)`:只读取后端 action 的 `enabled/reason`,不本地计算规则。
4. 角色资产工坊 modal 中的 prompt 输入框与缓存保存:这是用户正在编辑的 UI 草稿,默认 prompt 和合并规则已由后端 workflow 输出。
5. `playServerBattlePresentation(...)`:只播放临时动画态,最终 `GameState/currentStory` 仍以服务端 snapshot 为准。
## 6. 下一步建议
推荐按风险顺序继续:
1.`camp_travel_home_scene` 点击链统一改为 `runServerRuntimeChoiceAction(...)` / `/api/runtime/story/actions/resolve`
2. 扩展 `server-rs/crates/api-server/src/runtime_story/compat.rs` 中的 `camp_travel_home_scene` resolver让目标场景、encounter preview、`scenesTraveled`、故事提交和快照持久化全部由后端完成。
3. 补齐前端测试,锁定 `camp_travel_home_scene` 不再调用 `runCampTravelHomeChoice(...)`;补齐后端 route 级测试,覆盖离营后 hydrated snapshot 字段。
## 7. 一句话结论
**当前迁移已经完成了开局、快照、存档、story engine / prompt context 主链、NPC、背包/锻造、战斗后处理、profile 生成、创作结果页 normalize 和角色资产 prompt 主链;但 `camp_travel_home_scene` 仍由前端专用分支拼装正式场景迁移状态,所以不能判定“应迁移项已全部迁移完成”。**