8 Commits

Author SHA1 Message Date
cf8da3f50f Merge branch 'codex/dev' into codex/backend-rewrite-spacetimedb
# Conflicts:
#	docs/technical/README.md
#	server-node/src/modules/assets/qwenSpriteRoutes.ts
#	src/components/CustomWorldResultView.test.tsx
#	src/components/CustomWorldResultView.tsx
#	src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx
#	src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx
#	src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx
#	src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx
#	src/components/rpg-entry/RpgEntryCharacterSelectView.tsx
#	src/components/rpg-entry/RpgEntryHomeView.tsx
#	src/services/apiClient.ts
#	src/tools/QwenSpriteSheetTool.tsx
2026-04-21 20:16:01 +08:00
48957311bc 1 2026-04-21 19:18:26 +08:00
4372ab5be1 1 2026-04-21 18:27:46 +08:00
04bff9617d Update creation flow refactor docs and auth test fixtures 2026-04-21 11:19:25 +08:00
13bc79306f 1 2026-04-21 10:30:12 +08:00
ae28dab032 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-04-21 09:44:25 +08:00
3614e1f5a2 创作数据流程收束 2026-04-21 09:44:17 +08:00
effe0355bd 1 2026-04-21 00:48:17 +08:00
477 changed files with 38047 additions and 26570 deletions

View File

@@ -92,7 +92,7 @@ docs/
│ ├─ AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md
│ ├─ BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md
│ └─ RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md
├─ reference/[QwenSpriteSheetTool.tsx](src/tools/QwenSpriteSheetTool.tsx)
├─ reference/
│ ├─ README.md
│ └─ FUNCTION_SCRIPT_CATALOG_2026-04-04.md
└─ technical/

View File

@@ -10,7 +10,7 @@
- NPC 交易、送礼、求助、招募
- 宝藏交互
- 同伴跟随与战斗
- 预设编辑器 / NPC 视觉编辑器 / 行为编辑
- 游戏主流程内嵌的角色资产工坊、自定义世界实体编辑与角色形象编辑
- 自动存档与继续游戏
## 运行
@@ -99,11 +99,11 @@ npm run check:content
- [src/hooks/useCombatFlow.ts](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts)
- [src/hooks/useStoryGeneration.ts](/E:/Repos/Genarrative/src/hooks/useStoryGeneration.ts)
编辑器
主流程内嵌编辑能力
- [src/components/PresetEditor.tsx](/E:/Repos/Genarrative/src/components/PresetEditor.tsx)
- [src/components/NpcVisualEditor.tsx](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx)
- [src/components/StateFunctionEditor.tsx](/E:/Repos/Genarrative/src/components/StateFunctionEditor.tsx)
- [src/components/CustomWorldEntityEditorModal.tsx](/E:/Repos/Genarrative/src/components/CustomWorldEntityEditorModal.tsx)
- [src/components/CustomWorldNpcVisualEditor.tsx](/E:/Repos/Genarrative/src/components/CustomWorldNpcVisualEditor.tsx)
- [src/components/CustomWorldRoleAssetStudioModal.tsx](/E:/Repos/Genarrative/src/components/CustomWorldRoleAssetStudioModal.tsx)
核心数据:

View File

@@ -251,8 +251,8 @@ custom-world-library
| `anchorContent / creatorIntent / anchorPack / lockState` 被直接塞进 legacy profile | 创作态元数据 | 会跟随自动保存一起写进作品库 profile但 runtime 并不以这些字段为正式运行时输入 | 当前更像创作态元数据泄漏进运行时 profile |
| `qualityFindings` | session / contract 字段 | contract、session store、测试里存在但没形成生成、渲染、发布阻断闭环 | 当前主链悬空 |
| `checkpoints` | session 字段 | session store 会记录,但主工作区和结果页没有真实展示入口 | 当前主链悬空 |
| `suggestedActions` | session 字段 | session 会生成,但工作区没有 `QuickActions` 面板 | 当前主链悬空 |
| `pendingClarifications` | session 字段 | session 有数据,但澄清面板未接入主工作区 | 当前主链悬空 |
| `suggestedActions` | session 字段 | session 会生成,但当前正式工作区没有对应消费面;旧 `QuickActions` 面板已在 `2026-04-21` 判定退出当前版本主链并物理删除 | 当前主链悬空 |
| `pendingClarifications` | session 字段 | session 有数据,但当前正式工作区没有对应消费面;旧澄清面板已在 `2026-04-21` 判定退出当前版本主链并物理删除 | 当前主链悬空 |
| `operations` 历史 | session 字段 | 主工作区只展示当前 `activeOperation` 横幅,不展示完整历史 | 当前主链弱消费 |
| `roleAssetSummaryLabel / cover* / counts` 等 works 字段 | works 聚合字段 | 后端能返回,但主平台 create tab 没走 `works` 入口 | 当前主链弱消费 |
@@ -266,11 +266,11 @@ custom-world-library
| --- | --- | --- |
| 结果页直接生成 playable/story/landmark | `CustomWorldResultView.tsx` 仍可直接调用 AI 生成 | 与 Agent 对象精修链重复,且不会同步回 session |
| 结果页直接编辑 `CustomWorldProfile` | `CustomWorldEntityEditorModal` 仍挂在结果页 | 把结果页继续维持成旧编辑器,而不是 Agent 流程的收口层 |
| 旧 `custom-world/sessions` 世界生成 | `aiService.generateCustomWorldProfile()` 仍完整可用 | 与 Agent 八锚点世界创建重复 |
| 旧 `custom-world/sessions` 世界生成 | `2026-04-20` 审计时仍完整可用;`2026-04-21` 已完成物理删除 | 与 Agent 八锚点世界创建重复;当前遗留问题已转为文档口径清理 |
| 作品库 `publish/unpublish` 与 Agent `publish_world` | 两套“发布”概念并行 | 一套作用于 library profile一套想作用于 Agent session但后者还未打通 |
| 结果页自动保存 | `generatedCustomWorldProfile` 变化时自动 `upsertCustomWorldProfile()` | 让“草稿保存”“作品库存档”“正式发布”语义混在一起 |
## 7.2 冗余或未接线组件
## 7.2 冗余或已退出当前版本主链的组件
`src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx` 当前只真正接了:
@@ -280,7 +280,7 @@ custom-world-library
4. `CustomWorldAgentThread`
5. `CustomWorldAgentComposer`
但同目录下已经存在且主工作区未接线组件包括
`2026-04-21` 清理前,同目录下还存在一组未接线组件:
1. `CustomWorldAgentLockBar.tsx`
2. `CustomWorldAgentDraftDrawer.tsx`
@@ -291,6 +291,25 @@ custom-world-library
7. `CustomWorldAgentClarificationPanel.tsx`
8. `CustomWorldGenerateEntityModal.tsx`
其中:
1. `CustomWorldAgentDraftDrawer.tsx` 已在批次 A 清理中删除
2. `CustomWorldAgentLockBar.tsx`
3. `CustomWorldAgentDraftDetailPanel.tsx`
4. `CustomWorldAgentQuickActions.tsx`
5. `CustomWorldAgentSummaryPanel.tsx`
6. `CustomWorldAgentIntentSummaryPanel.tsx`
7. `CustomWorldAgentClarificationPanel.tsx`
8. `CustomWorldGenerateEntityModal.tsx`
已在批次 D 清理中判定为退出当前版本主链,并完成物理删除。
因此这里的审计结论需要更新为:
1. 它们不再属于“待接线组件”
2. 它们属于已确认退场的旧副面板链
3. 当前版本如果还要补 `suggestedActions / pendingClarifications / draftCards` 的消费面,应基于新的主链设计重新定义,而不是默认把旧面板接回来
另外,`src/components/custom-world-home/CustomWorldCreationHub.tsx` 也已存在,但平台 `create` tab 还没有把它接成主入口。
---
@@ -345,16 +364,16 @@ Agent session
2. 只有 `publish_world` 成功后,才产出正式 `CustomWorldProfile` 并允许主入口进入世界。
3. `qualityFindings / blocker` 必须在 foundation draft 完成、资产写回后、publish 前持续重跑。
## P1决定旧 world session 流程的命运
## P1继续做旧 world session 链的文档收口
当前最容易继续制造重复复杂度的是旧 `custom-world/sessions` 链。
`2026-04-21` 更新:
建议二选一:
`custom-world/sessions` 链已经完成物理删除。
1. 明确保留为“快速世界生成兼容模式”,但从主入口降级。
2. 明确进入淘汰路径,逐步下线 `generateCustomWorldProfile()` 这条旧链。
因此这里不再是“保留还是淘汰”的开放问题,而是:
不建议继续让它和 Agent 八锚点链同时作为主入口长期并存。
1. 继续清理由这条旧链残留在审计、PRD、知识图谱中的过时口径
2. 把当前正式主链与仍保留的兼容层边界写清楚
## P1把 works 创作中心接回主平台
@@ -365,16 +384,23 @@ Agent session
3. 已发布 profile 通过“进入世界”或“查看详情”进入。
4. `myEntries` 退回为作品库子集,而不是 create tab 的唯一数据源。
## P1补齐 Agent workspace 的最小闭环
## P1为悬空 session 字段重新定义最小闭环
建议优先接上
`2026-04-21` 更新
1. `CustomWorldAgentQuickActions`
2. `CustomWorldAgentDraftDrawer`
3. `CustomWorldAgentDraftDetailPanel`
4. `CustomWorldAgentClarificationPanel`
原文这里建议把旧 `QuickActions / DraftDrawer / DraftDetailPanel / ClarificationPanel` 接回主工作区。
如果这几个面板不接上,`suggestedActions / pendingClarifications / draftCards` 这些 session 字段会长期处于悬空状态。
但这些旧副面板已经在当前版本收口判断中被明确认定为:
1. 不属于现行主链
2. 不再作为当前版本默认待落地项
3. 已完成物理删除
因此当前更准确的建议应该是:
1. 如果 `suggestedActions / pendingClarifications / draftCards` 仍要进入正式主流程,需要先重新定义符合当前极简工作区的消费方式
2. 不应再以“把旧副面板接回来”作为默认方案
3. 在没有新主链设计前,这些字段继续标记为“主链悬空”
## P2等主链收口后再清桥接字段

View File

@@ -0,0 +1,141 @@
# 工程死分支清理执行记录 A2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
这份记录对应:
- `docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md`
- 其中的 `P0 + 批次 A`
本批次只做一件事:
**先清理高置信度、低耦合、无正式入口的小型孤岛与残留壳子。**
这批对象有一个共同特征:
1. 当前没有正式运行时引用
2. 没有当前主链计划要接回
3. 删除后有明确替代路径,或者本身只是历史占位
因此这批次不碰运行时真相链、不碰鉴权链、不碰任务物品主链,只先做低风险去噪。
---
## 1. 本批次已处理对象
## 1.1 已删除文件
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 | 验证口径 |
| --- | --- | --- | --- | --- |
| `src/services/customWorldPresentation.stub.ts` | 无引用占位 stub | 文件本身就是占位实现,且正式逻辑已由 `customWorldPresentation.ts` 承接 | `src/services/customWorldPresentation.ts` | 符号级检索确认正式调用方都指向正式实现 |
| `src/services/typewriter.ts` | 无引用 helper 残留 | 独立 helper 已失效,正式链路已在 `storyPresentation.ts` / `storyRenderingHelpers.ts` 等处内联或迁移 | `src/hooks/story/storyPresentation.ts``src/hooks/story/storyRenderingHelpers.ts` | `getTypewriterDelay` 调用点未指向该文件 |
| `src/prompts/customWorldOrchestratorPrompts.ts` | 前端孤岛 prompt 壳 | 当前无正式 import正式主编排 prompt 已收口到后端 prompt 目录,前端 `ai.ts` 也保留自己的现行实现 | `server-node/src/prompts/customWorldOrchestratorPrompts.ts``src/services/ai.ts` | 全仓检索仅剩文档引用,无代码消费 |
| `src/prompts/storyOrchestratorPrompts.ts` | 前端孤岛 prompt 壳 | 当前无正式 import剧情语言修复 prompt 已由后端 prompt 目录承接,前端当前执行路径不依赖该文件 | `server-node/src/prompts/storyOrchestratorPrompts.ts``src/services/ai.ts` | 全仓检索仅剩文档引用,无代码消费 |
| `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx` | 无入口 UI 壳层 | 最近两轮工程审计都确认无运行时引用,当前平台主流程未接这条入口 | 当前平台正式入口链 | 文件级检索确认无组件 import |
| `src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx` | 无入口 UI 壳层 | Agent 创作主流程已切到当前工作区链路,这个旧 modal 没有接线价值 | 当前 Agent 工作区主链 | 文件级检索确认无组件 import |
| `src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx` | 无入口 UI 壳层 | 只有孤立 UI 实现,没有正式调用链,也不在当前结果页 / 工作区主链中 | 当前 Agent 工作区与结果页正式链 | 文件级检索确认无组件 import |
---
## 2. 本批次为什么先删这 7 个
这批文件适合先处理,不是因为它们最大,而是因为它们最清晰:
1. **没有正式入口。**
本轮检索没有发现主工程 import。
2. **删除后不会形成职责空洞。**
要么已有正式替代路径,要么本身只是历史占位。
3. **不会误伤当前重点链路。**
这批不涉及运行时快照、鉴权、任务、物品、AI 正式编排主链。
4. **可以最快降低目录噪音。**
先把真假并存的壳子删掉,后面做批次 B/C/D 时判断成本会更低。
---
## 3. 本批次暂不处理对象
以下对象虽然已进入首轮台账,但本批次暂不删除:
1. `src/components/GameShell.tsx`
2. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
3. `src/hooks/story/storyBootstrap.ts`
4. `src/hooks/useEquipmentFlow.ts`
5. `src/hooks/useForgeFlow.ts`
6. `src/hooks/useInventoryFlow.ts`
7. `src/data/buildTagSimilarity.generated.ts`
暂缓原因分别是:
1. 仍属于旧主流程 / 旧 flow 级别对象,删除前要先核对更多历史依赖和替代路径
2. 部分对象仍有测试引用或更大的上下文耦合
3. `buildTagSimilarity.generated.ts` 虽无正式业务 import但属于生成产物处理前还要确认脚本链与文档链
这批对象更适合进入:
1. `批次 B旧 flow / 旧 shell / 旧 hook`
2. 或独立的数据产物复核批次
---
## 4. 本批次同步更新的文档
本批次除了删文件,还同步做了文档回填:
1. 新增本执行记录,说明本批删了什么、为什么删、哪些对象暂缓
2. 更新 `docs/audits/engineering/README.md`,把这份执行记录加入当前审计入口
这样做的目的,是避免再次出现:
1. 代码删了
2. 但审计入口还是旧状态
3. 后续开发又从旧清单里重复判断一遍
---
## 5. 验证方式
本批次验证采用两层口径:
## 5.1 删除前验证
1. 文件级检索确认无正式 import
2. 符号级检索确认关键导出没有被主链消费
3. 结合 `2026-04-20` 工程审计交叉确认这些对象已被标记为高置信度孤岛
## 5.2 删除后验证
建议至少执行:
1. `npm run check:encoding`
2. `npm run build`
说明:
- 当前仓库已知 `typecheck``lint` 仍处于红线阶段,因此本批不把它们作为“由本批引入的新失败”判断口径
- 本批主要验证目标是:删除小残留后,不产生新的导入断裂和构建断裂
---
## 6. 本批次结果判断
本批次完成后,工程至少获得了 3 个直接收益:
1. `src/prompts/``src/services/``src/components/custom-world-*` 中少了一批无入口孤岛
2. 当前目录里“看起来像正式入口,其实已经废弃”的误导性对象减少
3. 后续可以把精力集中到真正高价值的批次 B/C/D而不是继续被小残留分散判断成本
---
## 7. 下一批建议
建议严格按计划继续往下推进:
1. 批次 B`GameShell``storyBootstrap``useEquipmentFlow``useForgeFlow``useInventoryFlow`
2. 批次 C`runtimeStoryCoordinator``runtimeStoryService``apiClient`
3. 批次 D`npcEncounterActions``questDirector``runtimeItemAiDirector``ai.ts`
一句话总结本批次:
**先把最确定的死分支和占位壳子清掉,让主工程少一些假入口、假主源、假能力,再进入更重的主链收口。**

View File

@@ -0,0 +1,145 @@
# 工程死分支清理执行记录 B2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
这份记录对应清洗计划中的:
- `批次 B旧 flow / 旧 shell / 旧 hook`
本批次聚焦的不是小型 stub而是
**已经退出正式主流程、但仍占着高辨识度命名和旧职责心智的壳层与流程 Hook。**
这类文件如果继续留在仓库里,问题比小 helper 更大,因为它们会持续制造误判:
1. 新人会以为它们还是正式入口
2. 后续开发会误判“应该往这里接逻辑”
3. review 时会多出一层“旧主链是不是还活着”的判断成本
---
## 1. 本批次已处理对象
## 1.1 已删除文件
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 | 验证口径 |
| --- | --- | --- | --- | --- |
| `src/components/GameShell.tsx` | 旧主流程壳层残留 | 当前正式壳层已由 `src/components/game-shell/GameShellRuntime.tsx` 承接,旧文件无正式 import | `src/components/game-shell/GameShellRuntime.tsx``src/hooks/useGameShellRuntime.ts` | 全仓检索未发现对旧 `GameShell` 组件的正式消费 |
| `src/hooks/story/storyBootstrap.ts` | 旧启动流程 Hook 残留 | 当前主剧情启动链已不再调用该 Hook继续保留只会误导人以为它还是故事初始化入口 | 当前 story runtime / coordinator 链 | 全仓检索未发现 `useStoryBootstrap` 消费方 |
| `src/hooks/useEquipmentFlow.ts` | 旧装备流程 Hook 残留 | 当前正式背包与装备链未消费该 Hook属于旧流程实现残留 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
| `src/hooks/useForgeFlow.ts` | 旧锻造流程 Hook 残留 | 当前正式锻造入口未通过该 Hook 进入主链,保留会制造旧流程错觉 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
| `src/hooks/useInventoryFlow.ts` | 旧背包使用流程 Hook 残留 | 当前主流程未消费该 Hook属于旧状态推进实现残留 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
---
## 2. 为什么这批要紧跟批次 A 处理
批次 A 清掉的是“小型假入口”。
批次 B 清掉的是“高辨识度旧主链”。
这批必须紧跟着做,原因是:
1. 它们虽然比 stub 更大,但引用关系同样清楚
2. 它们的误导性比小残留更强
3. 不先处理这批,后面做批次 C/D 时,很容易继续有人拿旧 flow Hook 当候选接线点
一句话讲:
**批次 A 是去噪,批次 B 是拔掉旧路牌。**
---
## 3. 本批次删除后的结构变化
本批次完成后,仓库里的流程心智会更清楚:
1. 游戏壳层正式入口继续收敛到 `src/components/game-shell/**`
2.`GameShell.tsx` 不再和 `GameShellRuntime.tsx` 并存
3. 旧的装备 / 锻造 / 背包单独 flow Hook 不再伪装成还在生效的正式实现
4.`storyBootstrap` 不再和当前 story runtime 链并存
这会直接减少两类误判:
1. “是不是还有旧主流程没迁完”
2. “我是不是应该把新逻辑继续补进这些旧 Hook”
---
## 4. 本批次暂不处理对象
虽然批次 B 已经处理了旧 shell / old flow / old bootstrap但以下对象仍暂缓
1. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
2. `src/data/buildTagSimilarity.generated.ts`
3. 批次 C 的运行时真相链:
- `src/hooks/story/runtimeStoryCoordinator.ts`
- `src/services/runtimeStoryService.ts`
- `src/services/apiClient.ts`
4. 批次 D 的混合执行层:
- `src/hooks/story/npcEncounterActions.ts`
- `src/services/questDirector.ts`
- `src/services/runtimeItemAiDirector.ts`
- `src/services/ai.ts`
暂缓原因很明确:
1. 这些对象要么仍在当前正式链上
2. 要么涉及运行时真相与鉴权边界
3. 不能按“无引用旧壳”同一口径直接删除
---
## 5. 本批次验证方式
## 5.1 删除前验证
1. 全仓检索 `GameShell` 旧组件消费方,确认当前正式壳层已切到 `game-shell/` 目录
2. 全仓检索 `useStoryBootstrap`
3. 全仓检索旧装备 / 锻造 / 背包 flow Hook 导出的 handler 名称
4. 交叉确认当前正式主链入口已存在替代实现
## 5.2 删除后验证
建议至少执行:
1. `npm run check:encoding`
2. `npm run build`
如果这两项通过,说明:
1. 删除没有引入新的导入断裂
2. 主工程构建链仍然成立
---
## 6. 本批次结果判断
本批次完成后,工程获得的直接收益是:
1. 旧主流程壳层不再和现行壳层并存
2. 旧流程 Hook 不再占据 `src/hooks/` 的主路径注意力
3. 当前正式入口和历史残留的边界更清楚
4. 后续开发更不容易把新逻辑接回旧流程壳子
---
## 7. 下一批建议
建议下一步进入真正有结构价值的收口:
1. `批次 C运行时真相收口`
- `runtimeStoryCoordinator`
- `runtimeStoryService`
- `apiClient`
2. `批次 D任务 / 物品 / AI 混合执行层收口`
- `npcEncounterActions`
- `questDirector`
- `runtimeItemAiDirector`
- `ai.ts`
一句话总结本批次:
**这一步不是在“删几个没用 Hook”而是在把已经退场的旧主流程壳层和旧 flow 路牌从主工程里真正拔掉,让现行架构不再和历史壳子并排站着。**

View File

@@ -0,0 +1,202 @@
# 工程死分支清理执行记录 C2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
这份记录对应清洗计划中的:
- `批次 C运行时真相收口`
但这次不是“一口气把运行时真相链全删干净”,而是先做其中最明确、风险最低、最不该继续拖的那一段:
1. **收掉前端本地自动登录用户名 / 密码真相**
2. **把登录恢复改成优先依赖服务端 session / refresh**
同时,这一批也明确记录了一件事:
**运行时快照前置写入链当前还不能直接砍。**
原因不是“不想动”,而是服务端当前 `runtime story` 动作入口仍然以远端快照作为执行基线。
在后端 contract 没先改好之前,前端不能假装自己已经退出这条链。
---
## 1. 本批次已处理对象
## 1.1 已收口的鉴权链
| 文件 | 处理动作 | 本批结论 |
| --- | --- | --- |
| `src/services/apiClient.ts` | 删除本地自动登录用户名 / 密码存取逻辑 | 前端不再保存 auto auth 账号密码 |
| `src/services/authService.ts` | 去掉对本地游客凭证的读写依赖 | 自动游客登录改为仅本次生成凭证,不再长期落本地 |
| `src/components/auth/AuthGate.tsx` | 去掉“必须先有本地 access token 才尝试恢复”的前置假设 | 登录恢复改为优先尝试服务端 `getCurrentAuthUser()` / refresh session |
| `src/services/authService.test.ts` | 改写游客自动登录相关断言 | 验证改为“生成临时凭证并完成登录”,而不是“落本地账号密码” |
| `src/components/auth/AuthGate.test.tsx` | 改写登录恢复 mock | 验证改为“先尝试服务端会话恢复,再决定是否走游客兜底” |
---
## 2. 本批次为什么先做这段
这批优先级高,是因为它同时满足 4 条:
1. **风险明确。**
浏览器保存自动登录用户名 / 密码,本身就不符合“前端只做表现、后端负责鉴权真相”的方向。
2. **替代路径已经存在。**
后端已经有 refresh session cookie 与 `getCurrentAuthUser()`,不是没有可替代能力。
3. **改动边界清楚。**
这一段主要落在前端鉴权恢复逻辑和测试,不会直接波及运行时战斗、任务、物品、剧情主链。
4. **收益直接。**
一旦收掉,前端就少了一份最不该长期保留的高风险真相。
一句话讲:
**这一步先把“浏览器记住游客账号密码再重登”这条假真相链拔掉。**
---
## 3. 本批次明确没做的事
## 3.1 没有直接删除 `runtimeStoryCoordinator.ts` 里的前置 `putSaveSnapshot(...)`
这不是漏做,而是明确暂缓。
当前复核结果是:
1. `server-node/src/modules/story/storyActionService.ts`
2. `server-node/src/routes/runtimeRoutes.ts`
3. `server-node/src/repositories/runtimeRepository.ts`
这条后端链当前仍然通过远端快照读取运行时状态,再执行:
1. `getRuntimeStoryState`
2. `resolveRuntimeStoryAction`
也就是说,当前真实情况不是“前端多写了一份完全没用的镜像”,而是:
**前端在提交动作前先把当前状态写回远端快照,后端再基于这份快照执行业务动作。**
在这个 contract 没先升级为“前端只发 action后端自己持有完整 session 真相”之前,前端不能直接把这一步砍掉。
否则会出现:
1. 动作请求仍在走
2. 但服务端读取到的执行基线不完整
3. 最后不是收口真相,而是把主链打断
## 3.2 没有删除 `runtimeStoryService.ts` / `runtimeStoryCoordinator.ts` 的快照再水合逻辑
这一步本轮也做了复核,结论是:
1. 我曾尝试把 `runtimeStoryCoordinator.ts` 中对服务端返回快照的重复再水合去掉
2. 但对应的 `runtimeStoryCoordinator` 测试立即暴露出:当前后端返回的快照在部分战斗场景下还不是完整水合态
3. 说明前端当前这层再水合仍然有现实职责,不是纯多余代码
所以这一步本批明确结论是:
**暂不删除,等后端快照 contract 先补完整后再做。**
---
## 4. 本批次验证结果
本批次已完成的定向验证:
1. `npx vitest run src/services/authService.test.ts`
2. `npx vitest run src/components/auth/AuthGate.test.tsx`
3. `npx vitest run src/hooks/story/runtimeStoryCoordinator.test.ts`
4. `npm run check:encoding`
结果:
1. `authService` 测试通过
2. `AuthGate` 测试通过
3. `runtimeStoryCoordinator` 测试通过
4. 编码检查通过
另外执行了:
1. `npm run build`
结果:
构建产物生成成功,但 `build-gate` 仍因主包 chunk warning 拦截失败。
当前失败点仍是已知的主包体积问题:
- `AuthenticatedApp-*.js` 超过当前 warning 门槛
这属于仓库当前既有工程问题,不是本批次引入的新断裂。
## 4.1 2026-04-21 补充修正:会话探测 401 自触发循环
在这批收口完成后,前端又暴露出一条更细的鉴权恢复回路问题:
1. `AuthGate` 启动时会调用 `getCurrentAuthUser()` 探测现有会话
2. `/api/auth/me` 返回 `401` 时,`apiClient.ts` 会默认广播一次 `AUTH_STATE_EVENT`
3. `AuthGate` 自己又监听这个事件并重新 `hydrate()`
4. 最终形成 `hydrate -> /auth/me 401 -> emit -> hydrate` 的自触发循环
这条链的问题不在“是否允许 401”而在
**会话探测请求把“未登录态探测”错误地当成了“全局登录态变更”。**
因此这里补了一条更细粒度的约束:
1. `apiClient.ts` 新增 `notifyAuthStateChange` 选项,默认仍保持原有广播行为
2. `getCurrentAuthUser()` 作为会话探测请求,显式关闭这类 401 广播
3. 真实登录、登出、刷新成功后,仍保留全局鉴权变更通知
这样修完后:
1. `AuthGate` 仍会优先尝试服务端会话恢复
2. 无会话时会正常落回未登录分支
3. 不会因为探测型 401 把自己重新唤醒并刷爆控制台
---
## 5. 本批次完成后的实际收益
这一步完成后,工程在鉴权边界上有了两个明确改善:
1. **前端不再保存自动登录用户名 / 密码。**
浏览器只保留 access token本地高风险游客凭证真相已经收掉。
2. **登录恢复逻辑更接近服务端为真相源。**
`AuthGate` 不再假设“没有本地 token 就一定还没登录”,而是优先尝试服务端会话恢复。
这意味着前端鉴权链已经从:
```text
本地用户名/密码 -> 再次 entry -> 拿 token
```
进一步收到了:
```text
refresh session / 当前会话 -> 恢复用户
兜底时才创建一次游客凭证
```
---
## 6. 本批次后续建议
要继续完成批次 C下一步不该直接在前端硬删而应该先补后端 contract
1.`runtime story` 动作链逐步摆脱“前端先写远端快照”的依赖
2. 让服务端自己持有更完整的运行时 session 真相
3. 等后端返回快照已经稳定水合后,再删前端的重复再水合
换句话说,批次 C 的后半段应该拆成:
1. **C-1鉴权真相收口**
本批已完成
2. **C-2运行时快照 contract 后端化**
需要先改后端
3. **C-3前端镜像写入与重复水合退场**
依赖 C-2
---
## 7. 一句话总结
**批次 C 这一轮已经先把“浏览器长期保存游客账号密码”这条最不该存在的鉴权假真相链收掉了;而运行时快照前置写入这条链经过复核确认仍受后端 contract 约束,不能在服务端未先补齐前硬砍。**

View File

@@ -0,0 +1,56 @@
# 工程死分支清理执行记录 D2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
本批次继续清理上一轮复核后剩余的低风险数据产物与测试占位:
1. 未接入业务的生成产物
2. 只为测试替换真实实现的空 stub
3. 支撑这些残留的配置与脚本
---
## 1. 已删除对象
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 |
| --- | --- | --- | --- |
| `src/data/buildTagSimilarity.generated.ts` | 未接入业务的生成产物 | 运行时代码不 importBuild 相似度当前由 `buildTags.ts` 中的属性亲和度逻辑计算 | `src/data/buildTags.ts` |
| `scripts/generate-build-tag-similarity.py` | 已无输出目标的生成脚本 | 只负责生成已删除的矩阵文件,继续保留会误导后续开发恢复旧主源 | `src/data/buildTags.ts` 的手工审表逻辑 |
| `src/data/customWorldCharacterLoadout.stub.ts` | 测试专用空 stub | 只通过 `vitest.config.ts` alias 替换真实实现;真实实现已经稳定存在 | `src/data/customWorldCharacterLoadout.ts` |
---
## 2. 同步更新
本批次同步移除了:
1. `vitest.config.ts` 中指向 `customWorldCharacterLoadout.stub.ts` 的 alias
2. `BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` 中把旧 generated 矩阵描述为当前文件的表述
3. 清理计划里对 `buildTagSimilarity.generated.ts` 的未处理状态说明
---
## 3. 验证口径
删除前已确认:
1. `buildTagSimilarity.generated.ts` 无运行时代码引用
2. `customWorldCharacterLoadout.stub.ts` 只被 `vitest.config.ts` alias 引用
3. 真实 `customWorldCharacterLoadout.ts` 仍被 `characterPresets.ts``npcInteractions.ts` 使用,不能删除
删除后建议验证:
1. `npm run check:encoding`
2. 与自定义世界开局物品相关的测试
---
## 4. 当前结论
本批次完成后,剩余清理对象已经不再适合按“无引用直接删”推进。后续如果继续清,需要先改 contract 或主链职责:
1. 运行时快照真相链
2. 任务 / 物品 / AI 混合执行层
3. 大型主流程组件继续拆分,而不是直接删除

View File

@@ -0,0 +1,117 @@
# 工程死分支清理执行记录 E2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
本批次承接批次 D继续清掉已经退出 RPG 游戏创作主流程、RPG 运行时玩法主流程、平台基本功能主流程的历史壳层。
本批次不处理仍需后端 contract 先收口的对象,例如:
1. `src/services/questDirector.ts`
2. `src/services/runtimeItemAiDirector.ts`
3. `src/hooks/rpg-runtime-story/runtimeStoryCoordinator.ts`
4. `src/services/apiClient.ts`
这些对象仍属于“前端越界逻辑继续后端化”的后续批次,不按无引用文件直接删除。
---
## 1. 删除判定口径
本批只删除满足下面条件之一的对象:
1. 无运行时入口、无脚本入口、无当前路由挂载。
2. 已有现行正式实现,旧文件只剩 re-export / facade / 兼容命名。
3. 只被测试验证旧壳自身,且该测试不再服务当前主流程门禁。
4. 文档已明确该对象处于“后续只允许收缩、不再接新逻辑”的兼容残留状态。
---
## 2. 本批次已处理对象
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 |
| --- | --- | --- | --- |
| `server-node/src/routes/rpgCreationAgentRoutes.ts` | 旧命名 re-export | 当前后端正式路由直接使用 `customWorldAgent.ts` | `server-node/src/routes/customWorldAgent.ts` |
| `server-node/src/routes/rpgWorldGalleryRoutes.ts` | 空路由骨架 | 世界广场实际列表和详情已经进入世界库路由 | `server-node/src/routes/rpg-entry/rpgWorldLibraryRoutes.ts` |
| `server-node/src/services/RpgAgentOrchestrator.ts` | 旧命名 re-export | 当前正式上下文直接使用 `CustomWorldAgentOrchestrator` | `server-node/src/services/customWorldAgentOrchestrator.ts` |
| `server-node/src/services/RpgAgentSessionStore.ts` | 旧命名 re-export | 当前正式上下文直接使用 `CustomWorldAgentSessionStore` | `server-node/src/services/customWorldAgentSessionStore.ts` |
| `server-node/src/services/customWorldWorkSummaryService.ts` | 旧兼容入口 | 测试和路由已改为直接使用 RPG 命名服务 | `server-node/src/services/RpgWorldWorkSummaryService.ts` |
| `server-node/src/services/customWorldAgentPublishGateService.ts` | 旧发布门禁实现 | 当前 action executor 与作品库发布链已统一走 PublishingService | `server-node/src/services/customWorldAgentPublishingService.ts` |
| `server-node/src/services/customWorldAgentPublishService.ts` | 旧发布实现 | 当前发布链不再编译旧 legacy result profile | `server-node/src/services/customWorldAgentPublishingService.ts` |
| `server-node/src/modules/custom-world/runtime-profile/runtimeProfileCompiler.ts` | 旧 facade | runtime profile 已拆到目录模块并由 `index.ts` / `runtimeProfile.ts` 承接 | `server-node/src/modules/custom-world/runtime-profile/index.ts` |
| `server-node/src/bridges/legacyBuildRuntimeBridge.ts` | 无引用旧桥 | 后端 runtime build / equipment 已直接在正式模块内使用 | `server-node/src/modules/runtime/**` |
| `server-node/src/bridges/legacyRuntimeItemResolutionBridge.ts` | 旧桥 | runtime item 解析服务一并删除,正式运行时使用 `runtimeItemModule.ts` | `server-node/src/modules/runtime-item/runtimeItemModule.ts` |
| `server-node/src/modules/runtime-item/runtimeItemResolutionService.ts` | 无正式入口 wrapper | 只被 barrel 和自身测试引用,未挂入 Express 运行时主链 | `server-node/src/modules/runtime-item/runtimeItemModule.ts` |
| `server-node/src/modules/**/index.ts` | 无引用 barrel | 这些 barrel 没有被当前后端入口消费,反而制造“公共模块入口仍存在”的错觉 | 直接 import 具体正式模块 |
| `server-node/src/routes/rpg-*/index.ts` | 无引用 barrel | 当前 Express app 直接 import 具体 route 文件 | `server-node/src/app.ts` 中的具体路由 |
| `server-node/src/repositories/rpg-*/index.ts` | 无引用 barrel | 当前上下文直接 import 具体 repository | `server-node/src/server.ts` 中的具体仓储 |
| `src/components/DeveloperTeamModal.tsx` | 无入口 UI | 平台主流程没有打开该弹窗的入口 | 无替代 UI删除历史壳 |
| `src/components/LazySkillEffectPreview.tsx` | 无入口 lazy 壳 | 正式技能预览直接使用 `SkillEffectPreview` | `src/components/SkillEffectPreview.tsx` |
| `src/components/npcVisualEditorModel.ts` | 旧 NPC 形象写回模型 | 当前 RPG 创作编辑器使用 `CustomWorldNpcVisualEditor` 与结果页新入口 | `src/components/CustomWorldNpcVisualEditor.tsx``src/components/rpg-creation-editor/**` |
| `src/components/npcVisualEditorPersistence.ts` | 旧 NPC 形象写回持久层 | 只被旧持久化测试引用,正式编辑入口已迁移 | `src/components/rpg-creation-editor/**` |
| `src/components/rpg-creation-*/index.ts` | 无引用 barrel | 当前入口直接 import 具体 facade 文件barrel 没有主流程消费 | 直接 import `RpgCreation*` 具体文件 |
| `src/components/rpg-creation-editor/CustomWorldSceneChapterEditorSection.tsx` | 旧 facade | 当前编辑器 section 直接在 `RpgCreationEntityEditorShared.tsx` 中分发 | `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx` |
| `src/data/editorValidation.ts` | 旧预设编辑器校验 | 当前主流程和内容门禁不再调用 | `scripts/validate-overrides.ts`、后端 editor API |
| `src/editor/shared/EditorNotice.tsx` | 无入口共享 UI | 只被同批删除的 FormFields 使用 | 无替代 UI删除历史编辑器壳 |
| `src/editor/shared/FormFields.tsx` | 无入口共享 UI | 旧编辑器共享表单未接主流程 | 当前 RPG 编辑器组件内聚在 `rpg-creation-editor/**` |
| `src/editor/shared/SectionCard.tsx` | 无入口共享 UI | 旧编辑器卡片未接主流程 | 当前 RPG 编辑器组件内聚在 `rpg-creation-editor/**` |
| `src/hooks/rpg-runtime-story/npcEncounterActions.ts` | 旧 wrapper | 正式实现已在 `useRpgRuntimeNpcInteraction.ts`,测试已改到正式文件 | `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` |
| `src/hooks/rpg-runtime-story/openingAdventure.ts` | 旧前端开局特殊流程 | 开局营地对白已由后端 `RpgRuntimeStoryActionDomain` 和当前 story context 承接 | `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts` |
| `src/hooks/rpg-runtime-story/storyCampCompanion.ts` | 旧前端营地同伴 helper | 只剩旧开局流程和自身测试引用,正式开局上下文已迁到当前 runtime story 链 | 后端 runtime story action domain 与 `storyContextBuilder.ts` |
| `src/hooks/rpg-runtime-story/storyRenderingHelpers.ts` | 无入口旧渲染 helper | 当前正式 story presentation 不再 import | `src/hooks/rpg-runtime-story/storyPresentation.ts` |
| `src/prompts/questPrompts.ts` | 前端 prompt 残留 | Quest prompt 真相已迁到后端 | `server-node/src/prompts/questPrompts.ts` |
| `src/prompts/runtimeItemPrompts.ts` | 前端 prompt 残留 | Runtime item prompt 真相已迁到后端 | `server-node/src/prompts/runtimeItemPrompts.ts` |
| `src/services/questPrompt.ts` | 前端 prompt re-export | 只指向同批删除的前端 prompt | `server-node/src/prompts/questPrompts.ts` |
| `src/services/runtimeItemAiPrompt.ts` | 前端 prompt re-export | 只指向同批删除的前端 prompt | `server-node/src/prompts/runtimeItemPrompts.ts` |
| `src/services/storyEngine/contentDependencyGraph.ts` | 实验性孤岛 | 只被自身测试引用,没有主流程消费 | 后续如需要重新设计到后端 story graph 服务 |
---
## 3. 同步调整
1. `customWorldAgentPhase2/3/4` 测试改为直接实例化 `RpgWorldWorkSummaryService`
2. `customWorldWorkSummaryService.integration.test.ts` 改为直接覆盖 `RpgWorldWorkSummaryService`
3. `npcEncounterActions.test.ts` 改为直接覆盖 `useRpgRuntimeNpcInteraction.ts`,不再通过旧 wrapper。
4. `story_opening_camp_dialogue` 的 function catalog 执行路径改为后端 runtime action domain不再指向已删除旧前端文件。
5. NPC function catalog 中 `npc_chat / npc_help / npc_leave / npc_fight / npc_spar / npc_preview_talk` 的 executor 路牌改到现行 `useRpgRuntimeNpcInteraction.ts`
---
## 4. 本批次暂缓对象
以下对象仍然保留,原因是它们不是“无引用死代码”,而是需要下一轮按 contract 或主链职责迁移:
1. `src/services/questDirector.ts`
2. `src/services/runtimeItemAiDirector.ts`
3. `src/services/ai.ts`
4. `src/data/sceneObservation.ts`
5. `server-node/ecosystem.config.cjs`
6. `server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts`
其中 `ecosystem.config.cjs` 被部署脚本直接使用;`sceneObservation.ts` 被内容 smoke 脚本验证;`syncCustomWorldSavedProfileAssets.ts` 是一次性运维脚本,后续要单独按运维脚本治理口径确认是否归档。
---
## 5. 验证口径
本批删除后建议验证:
1. `npm run check:encoding`
2. `npx tsx --test server-node/src/services/customWorldWorkSummaryService.integration.test.ts`
3. `npx vitest run src/hooks/rpg-runtime-story/npcEncounterActions.test.ts`
4. `npm run server-node:build`
5. `npm run build`
如果 `npm run build` 仍被既有 chunk warning 拦截,需要单独记录为既有门禁问题,不归因到本批删除。
---
## 6. 当前结论
本批次进一步删除了“旧命名入口、旧 facade、旧 prompt 前端镜像、无入口编辑器壳层”这批容易误导后续开发的文件。
后续清理不应继续按“静态无引用”直接推进,而应进入两类工作:
1. 运行时 / 任务 / 物品 / AI 的后端 contract 收口。
2. RPG 创作编辑器与运行时热点文件的职责拆分。

View File

@@ -0,0 +1,91 @@
# 工程死分支清理执行记录 F2026-04-21
更新时间:`2026-04-21`
## 0. 本批次目标
本批次承接批次 E 的验证结果,继续处理删除后暴露出的最后一组高置信残留:
1. 已经没有任何代码入口引用的前端任务生成 director。
2. 只被内容 smoke 牵住、但不再是正式运行时入口的旧观察文案 helper。
3. 带有固定用户、固定 session、固定 profile 的一次性历史同步脚本。
4. 清理后暴露出的 function catalog 契约覆盖缺口。
本批次仍然不按文件名直接删除 `legacy` 命名对象。经核对,`server-node/src/bridges/legacyInventoryRuntimeBridge.ts``legacyNpcTask6Bridge.ts``legacyQuestProgressBridge.ts``legacyQuestRuntimeBridge.ts``legacyRuntimeItemBridge.ts``legacyTreasureRuntimeBridge.ts` 仍被后端战斗、背包、任务、宝藏主链直接引用,不能按历史命名硬删。
---
## 1. 删除判定口径
本批删除对象必须同时满足:
1. 修正 `.js -> .ts` 后端源码解析、前端懒加载入口解析后,仍不可从正式入口到达。
2. 全仓库代码引用扫描没有正式入口引用。
3. 如只被 smoke 或测试牵住,先把 smoke / 测试改到当前正式主链,再删除旧对象。
4. 删除后通过对应门禁验证,没有新增悬空 import。
---
## 2. 本批次已处理对象
| 文件 | 判定 | 删除 / 调整原因 | 替代路径 / 当前真相源 |
| --- | --- | --- | --- |
| `src/services/questDirector.ts` | 无代码入口残留 | 正式 quest 生成已由后端 `/api/runtime/quests/generate``questService.ts` 承接,前端当前没有任何 import | `server-node/src/services/questService.ts``server-node/src/modules/quest/runtimeQuestModule.ts` |
| `src/data/sceneObservation.ts` | 旧观察文案 helper | 只被 `scripts/smoke-content.ts` 引用,正式观察动作已走 `idle_observe_signs` function 与运行时 story continuation | `src/data/functionCatalog/state/idleObserveSigns.ts``src/hooks/rpg-runtime-story/storyChoiceContinuation.ts` |
| `server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts` | 一次性硬编码运维脚本 | 脚本内固定用户、session、profile只服务历史补丁没有 CLI 参数和当前运维入口 | 无替代;如未来需要,按参数化运维脚本重新设计 |
---
## 3. 同步调整
1. `scripts/smoke-content.ts` 不再 import 旧 `sceneObservation.ts`,改为通过 `resolveFunctionOption('idle_observe_signs', ...)` 验证当前正式 function 目录。
2. `packages/shared/src/contracts/rpgRuntimeContracts.test.ts` 不再验证已移除的旧 `story` façade改为直接验证当前拆分契约。
3. `src/data/functionCatalog/` 补齐仍在后端运行时契约中的 function 文档:
- `battle_attack_basic`
- `battle_use_skill`
- `npc_chat_quest_offer_view`
- `npc_chat_quest_offer_replace`
- `npc_chat_quest_offer_abandon`
4. `battle_attack_basic``battle_use_skill` 只作为后端契约文档登记,不进入 `STATE_FUNCTION_DEFINITIONS`,避免前端本地候选池生成缺少 `runtimePayload.skillId` 的假技能 option。
---
## 4. 本批次暂缓对象
以下对象经本批复核后继续保留:
1. `server-node/src/services/customWorldAgentRepositoryTestHelpers.ts`
2. `server-node/src/services/customWorldAgentTestHelpers.ts`
3. `server-node/src/testFixtures/runtimeCharacter.ts`
4. `server-node/src/testHttp.ts`
这些文件不属于正式运行时入口但当前被后端测试、smoke 与路由边界门禁使用。它们不是 RPG 创作 / 运行时玩法主流程代码,但仍是平台基本质量门禁的一部分,不能在“删除冗余业务代码”批次里直接硬删。
另保留:
1. `src/services/runtimeItemAiDirector.ts`
2. `src/services/ai.ts`
3. `src/services/apiClient.ts`
这些文件仍被当前主链或前端 SDK 入口引用,后续如继续压缩,必须先完成对应 contract / SDK 拆分,不按无引用规则删除。
---
## 5. 验证结果
本批已通过:
1. `npx vitest run src/data/functionCatalog/functionCatalog.test.ts packages/shared/src/contracts/rpgRuntimeContracts.test.ts`
2. `npx tsx scripts/smoke-content.ts`
3. `npm run check:encoding`
并额外确认:
1. 全仓库代码中不再引用 `sceneObservation``questDirector``syncCustomWorldSavedProfileAssets`
2. `buildStateFunctionDefinitions()` 中不会出现 `battle_attack_basic` / `battle_use_skill`,这两个 function 只由后端运行时 option 池下发。
---
## 6. 当前结论
本批次后,静态入口扫描中剩余的高置信“不可达源码”已经收敛为测试辅助、测试夹具和 smoke helper。继续删除前需要先重构测试基础设施或迁移剩余前端 SDK而不应再按文件名或历史命名直接硬删。

View File

@@ -0,0 +1,553 @@
# 前端应迁后端逻辑审计2026-04-21
更新时间:`2026-04-21`
## 0. 审计目标
这份文档只回答一个问题:
**当前前端代码里哪些逻辑已经明显越过“前端只做表现Express 后端负责逻辑、数据与存储”的边界,应该继续迁到后端。**
本轮不改业务代码,只做:
1. 基于当前仓库状态给出高置信度候选点
2. 标明代码证据
3. 给出迁移优先级
4. 说明迁移后前端应该保留什么、移走什么
---
## 1. 结论先行
结合当前代码与已有边界文档,前端里仍有 7 类逻辑应该继续后移:
1. **运行时快照前置写入与本地镜像解释**
2. **鉴权 token 的浏览器本地真相**
3. **平台浏览历史的本地真相与迁移状态**
4. **NPC 待接委托“换单”仍由前端直接触发正式生成**
5. **quest/runtime item 的双环境混合编排**
6. **浏览器侧大型 AI orchestration 与 prompt/repair/fallback 主链**
7. **NPC 招募对白之后的正式结算链路**
一句话判断:
**当前前端已经不是最早那种“大量主算”的状态,但仍然保留了运行时镜像、生成编排和部分正式真相。后端边界还需要再收一轮,前端才算真正退回表现层。**
---
## 2. 审计依据
### 2.1 文档依据
1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
3. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md`
4. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
5. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
### 2.2 当前代码依据
1. `src/hooks/story/runtimeStoryCoordinator.ts`
2. `src/services/apiClient.ts`
3. `src/services/platformBrowseHistory.ts`
4. `src/components/game-shell/PreGameSelectionFlow.tsx`
5. `src/hooks/story/npcEncounterActions.ts`
6. `src/services/questDirector.ts`
7. `src/services/runtimeItemAiDirector.ts`
8. `src/services/ai.ts`
---
## 3. 当前高置信度应后移逻辑
## 3.0 本轮已完成后移
以下链路已在本轮或上一轮连续落地中完成后移,不再属于“仍残留在前端”的正式主链:
1. access token 浏览器本地真相
2. browse history 本地真相
3. runtime story 前置 `PUT /runtime/save/snapshot`
4. NPC 待接委托 `replace / abandon / accept`
5. custom world profile 正式浏览器入口
6. `questDirector` / `runtimeItemAiDirector` 浏览器正式编排
7. NPC 招募正式结算
其中 NPC 招募已从“前端本地改 companions / roster / npcStates / storyHistory”收回到后端 runtime action。
## 3.1 运行时快照前置写入仍在前端
### 代码证据
`src/hooks/story/runtimeStoryCoordinator.ts` 当前仍存在以下链路:
1. `syncRuntimeSnapshot(...)`
2. `syncRuntimeSnapshot(...)` 内部直接调用 `putSaveSnapshot(...)`
3. `loadServerRuntimeOptionCatalog(...)` 在请求 `getRuntimeStoryState(...)` 之前先写本地快照
4. `resolveServerRuntimeChoice(...)` 在请求 `resolveRuntimeStoryAction(...)` 之前先写本地快照
对应位置:
1. `src/hooks/story/runtimeStoryCoordinator.ts:21`
2. `src/hooks/story/runtimeStoryCoordinator.ts:25`
3. `src/hooks/story/runtimeStoryCoordinator.ts:36`
4. `src/hooks/story/runtimeStoryCoordinator.ts:99`
### 当前问题
这意味着运行时正式动作发起前,前端仍会先落一份自己的快照真相,再去请求后端。
这条链的问题不是“有没有缓存”,而是:
1. 前端仍在承担正式提交前的状态镜像
2. 快照解释权没有完全收回到后端
3. 运行时主链仍处于“本地镜像 + 服务端会话”并存状态
### 迁移建议
后端继续承接:
1. 运行时快照写入
2. 快照版本解释
3. 动作提交前的状态一致性校验
前端只保留:
1. 当前展示用的 view model
2. 可选的只读恢复缓存
3. 纯表现态的 loading / transition / animation state
### 优先级
`P0`
---
## 3.2 鉴权 token 仍由前端 localStorage 持有真相
### 代码证据
`src/services/apiClient.ts` 当前仍直接访问 `window.localStorage` 保存 access token
1. `getStoredAccessToken()`
2. `setStoredAccessToken(...)`
3. `clearStoredAccessToken(...)`
4. `withAuthorizationHeaders(...)` 直接从本地 token 组装请求头
对应位置:
1. `src/services/apiClient.ts:333`
2. `src/services/apiClient.ts:341`
3. `src/services/apiClient.ts:362`
4. `src/services/apiClient.ts:382`
### 当前问题
第三批清理已经收掉了“自动登录用户名/密码”本地真相,但 access token 仍然由浏览器长期持有。
这在当前项目边界下仍有两个问题:
1. 正式鉴权真相仍没有完全收回后端 session 边界
2. 前端 SDK 仍然负担 token 生命周期的关键部分
### 迁移建议
后端继续承接:
1. session / refresh / cookie 真相
2. 鉴权状态续期
3. token 更新与失效策略
前端只保留:
1. 当前是否已登录的展示态
2. 统一的请求封装
3. 401 后的 UI 响应
### 优先级
`P0`
---
## 3.3 平台浏览历史仍是“前端本地历史 + 后端回填”的双真相
### 代码证据
`src/services/platformBrowseHistory.ts` 当前仍维护一整套本地历史真相:
1. `readPlatformBrowseHistory(...)`
2. `writePlatformBrowseHistory(...)`
3. `hasPendingPlatformBrowseHistoryMigration(...)`
4. `markPlatformBrowseHistoryMigrated(...)`
对应位置:
1. `src/services/platformBrowseHistory.ts:77`
2. `src/services/platformBrowseHistory.ts:103`
3. `src/services/platformBrowseHistory.ts:151`
4. `src/services/platformBrowseHistory.ts:164`
`src/components/game-shell/PreGameSelectionFlow.tsx` 当前仍显式做:
1.`writePlatformBrowseHistory(...)`
2. 再调用 `upsertProfileBrowseHistory(...)`
3. 同步成功后 `markPlatformBrowseHistoryMigrated(...)`
4. 启动阶段读取 `readPlatformBrowseHistory(...)`
5. 根据 `hasPendingPlatformBrowseHistoryMigration(...)` 决定是否补同步
对应位置:
1. `src/components/game-shell/PreGameSelectionFlow.tsx:383`
2. `src/components/game-shell/PreGameSelectionFlow.tsx:392`
3. `src/components/game-shell/PreGameSelectionFlow.tsx:394`
4. `src/components/game-shell/PreGameSelectionFlow.tsx:433`
5. `src/components/game-shell/PreGameSelectionFlow.tsx:466`
### 当前问题
这条链已经不是单纯缓存,而是:
1. 本地历史存储
2. 本地同步标记
3. 后端历史持久化
三套状态同时存在。
### 迁移建议
后端继续承接:
1. 浏览历史唯一持久化真相
2. 历史去重、排序、截断
3. 迁移完成标记
前端只保留:
1. 展示缓存
2. 弱网下的临时 optimistic UI
3. 刷新后重新拉取远端结果
### 优先级
`P1`
---
## 3.4 NPC 待接委托“换单”仍由前端直接发起正式生成
### 代码证据
`src/hooks/story/npcEncounterActions.ts` 当前仍保留:
1. `replacePendingNpcQuestOffer = async () => { ... }`
2. 内部直接调用 `generateQuestForNpcEncounter(...)`
对应位置:
1. `src/hooks/story/npcEncounterActions.ts:1561`
2. `src/hooks/story/npcEncounterActions.ts:1595`
### 当前问题
聊天后是否挂出待接委托已经后移,但“换一份委托”这条分支仍然是:
1. 前端组装上下文
2. 前端决定调用生成
3. 前端直接把结果写回当前 story UI
这仍属于正式运行时任务编排没有收干净。
### 迁移建议
后端继续承接:
1. NPC 待接委托换单决策
2. 是否允许换单
3. 换单后的任务草案生成
4. 对应聊天态快照回填
前端只保留:
1. 点击“换一份委托”
2. loading / error 展示
3. 消费后端返回的新 pending quest offer
### 优先级
`P0`
---
## 3.5 questDirector 仍是前端 SDK 与生成编排混合体
### 代码证据
`src/services/questDirector.ts` 当前同时承担:
1. `generateQuestForNpcEncounter(...)`
2. 浏览器路径 `requestJson('/api/runtime/quests/generate')`
3. 非浏览器路径 `requestChatMessageContent(...)`
4. 本地 `compileQuestIntentToQuest(...)` fallback
对应位置:
1. `src/services/questDirector.ts:213`
2. `src/services/questDirector.ts:242`
3. `src/services/questDirector.ts:267`
4. `src/services/questDirector.ts:256`
5. `src/services/questDirector.ts:281`
6. `src/services/questDirector.ts:293`
### 当前问题
这类文件虽然浏览器正式路径已经优先走后端,但职责仍混在一起:
1. 前端 SDK
2. Quest prompt 编排
3. Quest intent 解析
4. deterministic fallback compile
这会导致边界长期模糊,也让前端仍像“半个服务端”。
### 迁移建议
后端继续承接:
1. quest intent 生成
2. prompt 组装
3. JSON 解析
4. fallback compile
前端只保留:
1. `requestGenerateQuest(...)` 这类轻量 SDK
2. 请求参数组装
3. 结果消费
### 优先级
`P1`
---
## 3.6 runtimeItemAiDirector 仍是前端 SDK 与意图生成混合体
### 代码证据
`src/services/runtimeItemAiDirector.ts` 当前同时承担:
1. `generateRuntimeItemAiIntents(...)`
2. 浏览器路径 `requestJson('/api/runtime/items/runtime-intent')`
3. 非浏览器路径 `requestChatMessageContent(...)`
4. 本地 `buildRuntimeItemAiIntent(...)` fallback
对应位置:
1. `src/services/runtimeItemAiDirector.ts:84`
2. `src/services/runtimeItemAiDirector.ts:94`
3. `src/services/runtimeItemAiDirector.ts:118`
### 当前问题
它和 `questDirector` 是同类问题:
1. 正式浏览器路径已经走后端
2. 但前端文件仍然承担完整生成逻辑认知
3. 文件职责仍然是双环境混合
### 迁移建议
后端继续承接:
1. runtime item intent prompt
2. 模型调用
3. 结果解析与 fallback
前端只保留:
1. 轻量请求 SDK
2. 结果到 UI 的映射
### 优先级
`P1`
---
## 3.7 `src/services/ai.ts` 仍是浏览器侧正式 AI orchestration 热点
### 代码证据
当前 `src/services/ai.ts` 仍直接承担以下正式链路:
1. `requestChatMessageContent(...)`
2. `requestPlainTextCompletionFromClient(...)`
3. `streamPlainTextCompletionFromClient(...)`
4. `generateCustomWorldProfile(...)`
5. `generateInitialStory(...)`
6. `generateNextStep(...)`
7. `streamNpcChatDialogue(...)`
8. `streamNpcRecruitDialogue(...)`
对应位置:
1. `src/services/ai.ts:1732`
2. `src/services/ai.ts:1868`
3. `src/services/ai.ts:2038`
4. `src/services/ai.ts:2339`
5. `src/services/ai.ts:2447`
6. `src/services/ai.ts:2487`
7. `src/services/ai.ts:2529`
8. `src/services/ai.ts:2570`
并且文件内仍保留:
1. JSON repair
2. prompt 组装
3. response normalize
4. fallback/offline 响应
5. 角色聊天建议与摘要生成
### 当前问题
这说明浏览器端并不只是“请求一个后端接口”,而是还在承担:
1. prompt source
2. 生成策略
3. 错误修复
4. fallback 编排
5. 多类业务场景的正式 AI 出口
这与“前端只做表现”存在明确冲突。
### 迁移建议
后端继续承接:
1. story / npc / recruit / custom-world 的 prompt 编排
2. JSON repair
3. fallback 策略
4. streaming orchestration
5. 模型调用与日志
前端只保留:
1. 轻量 AI SDK
2. SSE 文本流展示
3. UI fallback 呈现
### 优先级
`P0`
---
## 3.8 NPC 招募对白之后的正式结算链路已完成后移
### 本轮前状态
迁移前,`src/hooks/story/npcInteraction.ts` 中的 `buildRecruitmentOutcome / executeRecruitment / startRecruitmentSequence` 仍在前端本地正式结算:
1.`npcStates`
2.`companions`
3.`roster`
4.`currentEncounter / inBattle / sceneHostileNpcs`
5. 直接写 `storyHistory`
6. 再触发后续剧情推进
这与“前端只做表现,所有正式逻辑、数据都放到 Express 后端”直接冲突。
### 本轮后状态
本轮已完成:
1. `server-node/src/modules/story/runtimeSession.ts`
- 正式承接完整 `companions`
- 正式承接 `roster`
2. `server-node/src/modules/npc/npcInteractionService.ts`
- `npc_recruit` 已支持正常入队
- `npc_recruit` 已支持满员换队招募
3. `src/hooks/story/npcInteraction.ts`
- 前端只保留招募对白流式展示
- 正式招募结算改为调用后端 runtime action
### 当前判断
这一项已不再属于前端残留正式逻辑。
---
## 4. 可以暂时保留在前端的部分
下面这些内容即使和上述模块同文件出现,也不属于必须后移的对象:
1. 面板开关、loading、error、streaming 文本展示
2. 动画时间线、过场状态、临时 UI 回显
3. 表单草稿、筛选词、排序选项
4. 只影响表现、不影响正式真相的 view model 拼接
迁移时要注意:
**不是把所有前端代码都往后端搬,而是把“正式状态解释、规则裁决、生成编排、持久化真相”搬走。**
---
## 5. 推荐迁移顺序
## 5.1 第一阶段
先收最危险的正式真相:
1. `runtimeStoryCoordinator.ts`
2. `apiClient.ts`
3. `npcEncounterActions.ts` 里的 quest replace 分支
原因:
1. 这三处最直接影响运行时真相和动作主链
2. 不先收这些,前端仍然不是纯表现层
## 5.2 第二阶段
再拆双环境混合服务:
1. `questDirector.ts`
2. `runtimeItemAiDirector.ts`
3. `platformBrowseHistory.ts`
原因:
1. 这几处已经有后端承接基础
2. 迁移成本相对可控
## 5.3 第三阶段
最后继续压缩浏览器 AI orchestration
1. `src/services/ai.ts`
2. 相关 prompt builder / repair helper / offline fallback
原因:
1. 这部分体量大
2. 链路多
3. 更适合在前两阶段把 contract 稳住后集中拆
---
## 6. 建议产出物
如果后续按这份文档继续落地,建议每一批都至少同步产出:
1. 一份落地文档,说明迁移了哪条链
2. 一组 contract/route 变更说明
3. 一组前端 SDK 收缩说明
4. 一组防回退测试
---
## 7. 一句话结论
当前前端最需要继续后移的,不是零散小工具,而是:
**运行时快照前置写入、鉴权 token、本地浏览历史真相、NPC 委托换单、quest/runtime item 双环境混合编排,以及 `src/services/ai.ts` 里仍然留在浏览器的正式 AI orchestration。**

View File

@@ -4,21 +4,42 @@
## 当前推荐入口
1. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
1. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md)
这一版是第六批落地记录,聚焦删除无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本并补齐后端运行时 function catalog 契约覆盖。
2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md)
这一版是第五批落地记录,聚焦旧命名 re-export、空路由骨架、旧发布服务、前端 prompt 镜像与无入口编辑器壳层的物理删除。
3. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 Express 后端的运行时、鉴权、生成编排与本地真相残留。
4. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。
5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。
6. [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 的正式出清。
7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
这一版是第一批落地记录聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。
8. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
这一版是面向当前仓库状态的优化点盘点,适合直接拿来排优先级和拆执行批次。
2. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
9. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
这一版是对 `2026-04-19` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。
3. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
10. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。
4. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md)
11. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md)
这一版最适合作为当前工程基线,重点从“是否真正绿色”“门禁有没有覆盖真实风险”来判断仓库状态。
5. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md)
12. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md)
适合回看运行时主链路、Story/Combat 边界、分层过渡期问题。
6. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md)
13. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md)
适合看第一轮系统性工程扫描,了解最早的问题基线。
## 融合结论
- 最新专项审计已经把“前端哪些逻辑还该后移到后端”收敛到 6 类:运行时快照、本地 token、本地浏览历史、NPC 委托换单、quest/runtime item 混合编排、浏览器 AI orchestration。
- 工程大清洗已经开始进入实际执行阶段,首批高置信度小型孤岛和残留壳子已开始清理。
- 第二批已经开始清理旧主流程壳层与旧 flow Hook当前主工程的“现行入口”和“历史入口”边界正在变得更清楚。
- 第三批已经先完成鉴权真相收口的一段,前端不再保存自动登录用户名/密码;运行时快照链仍需先补后端 contract再继续往前删。
- 第四批已经继续收掉未接入业务的数据生成产物、测试专用 stub 与对应脚本/配置残留,主工程里的“假数据主源”进一步减少。
- 第五批已经继续收掉旧命名 re-export、空路由骨架、旧发布 service、前端 prompt 镜像与无入口编辑器壳层,主工程里的“假入口”和“假 prompt 主源”进一步减少。
- 第六批已经继续收掉无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本并修复 function catalog 对后端运行时契约的覆盖缺口。
- 当前仓库已经完成“旧 dev 插件链路删除、根目录噪音清理、`server-node -> src/**` 反向依赖切断”这批第一阶段任务。
- 当前如果想直接判断“今天先优化什么”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
- 当前的新重点已经进一步收敛到三类:未接线孤岛模块、前端残留的运行时/鉴权真相、热点向 prompt/runtime profile/平台入口壳层迁移。

View File

@@ -163,4 +163,13 @@
---
## 13. 2026-04-21 创作中心失效草稿恢复兜底
- `src/components/rpg-entry/useRpgEntryLibraryDetail.ts` 现在会识别 `custom-world agent session``404 NOT_FOUND` 读取失败,不再把这类错误直接冒泡成未捕获 Promise。
- 当用户在创作中心点击“继续创作”命中失效草稿时,前端会主动清空 `customWorldSessionId` 恢复参数,并刷新一次 works 列表,避免刷新页面后反复尝试恢复同一个坏会话。
- 当前交互已收口成平台内可见提示:用户会停留在创作中心,并看到“这份共创草稿已失效,已为你返回创作中心,请重新开始创作。”,而不是卡在空白工作区或只在控制台看到英文异常。
- 这次兜底只处理失效会话恢复,不改变正常草稿继续创作、结果页恢复和已发布作品进入世界的主链。
---
_文档目的交接给下一个 Agent 时,优先读本文件 + `UI_CODING_STANDARD.md`,再改 `uiAssets.ts` / `App.tsx` / `index.css`。_

View File

@@ -22,20 +22,20 @@
### 2.1 编辑器入口与页签整理
- 保留 `/preset-editor``/npc-editor``/function-editor`
- 新增 `/behavior-editor` 作为“选项行为”编辑页别名
- 当时曾保留 `/preset-editor``/npc-editor``/function-editor`
- 当时还新增 `/behavior-editor` 作为“选项行为”编辑页别名
- 将原先单独的 `NPC 视觉` 标签并回 `NPC` 编辑页
-`Function` 页签改名为 `选项行为`
结论:
- 路由层要尽量兼容旧入口,避免历史链接失效
- 独立编辑器入口如果没有继续接入主流程,应及时物理删除,不要长期保留兼容壳
- 页签命名要贴近创作者语言,而不是内部实现命名
### 2.2 NPC 视觉模块并入 NPC 编辑
完成内容:
- 将 [NpcVisualEditor](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx) 嵌入 [PresetEditor](/E:/Repos/Genarrative/src/components/PresetEditor.tsx) 的 NPC 编辑页
- 当时曾将 `NpcVisualEditor` 嵌入 `PresetEditor` 的 NPC 编辑页
- 让 NPC 文本字段与视觉字段围绕同一个当前选中 NPC 联动
- 保留视觉覆盖保存与全局布局保存能力

View File

@@ -86,6 +86,22 @@
2. 不为了“文档里写过”就把所有没接线面板都接进来
3. 不把当前工作区重新改造成一个更复杂的大后台
补充更新(`2026-04-21`
上一轮审计里提到的一组旧 Agent 副面板,已经在工程清理中被明确判定为退出当前版本主链并物理删除,包括:
1. `CustomWorldAgentLockBar.tsx`
2. `CustomWorldAgentQuickActions.tsx`
3. `CustomWorldAgentSummaryPanel.tsx`
4. `CustomWorldAgentIntentSummaryPanel.tsx`
5. `CustomWorldAgentClarificationPanel.tsx`
6. `CustomWorldAgentDraftDetailPanel.tsx`
7. `CustomWorldDraftCardDetailModal.tsx`
8. `CustomWorldDraftEditPanel.tsx`
9. `CustomWorldGenerateEntityModal.tsx`
所以这里的“不为了文档里写过就全接进来”,现在不只是态度提醒,而是已经执行过的现实边界。
### 3.3 不把结果页继续当旧编辑器扩写
这轮明确不再鼓励:
@@ -244,6 +260,15 @@
尤其是旧 `custom-world/sessions` 这条链,如果还要保留,也只能是兼容入口,不能再和 Agent 主链平起平坐。
补充更新(`2026-04-21`
这条旧 `custom-world/sessions` 世界生成链已经完成物理删除。
因此阶段三在当前仓库里的剩余任务,不再是“决定要不要保留这条旧链”,而是:
1. 继续收掉结果页 legacy profile 直改能力的误导性职责
2. 继续清理文档里把旧链当成现行选项的表述
---
## 4.5 第五件事:把文稿里那些“这轮不做”的未完成项从主叙事里移掉
@@ -323,6 +348,12 @@
2. 把“未来也许做”从“这轮要做”里拆开
3. 让所有当前规划只服务当前版本
就当前状态补一句最重要的执行口径:
1. 已经物理删除的旧链和旧副面板,不再作为“本轮待落地项”
2. 历史 PRD 可以保留实现设想,但必须和“当前版本执行规划”分开
3. 当前版本规划只保留仍对正式主链有现实约束的事项
这一阶段的目标是:
**让接下来所有开发都围绕同一套现实目标执行。**

View File

@@ -0,0 +1,579 @@
# 工程无用分支、历史代码与隐形多链路大清洗执行计划2026-04-21
更新时间:`2026-04-21`
## 0. 文档目标
这份文档只解决一件事:
**对当前工程发起一轮“不是继续加功能,而是系统性减负、删重、收口、归档”的大清洗。**
这轮重点不是做表面上的“代码变少”,而是把下面 3 类长期拉低可读性和可维护性的东西真正处理掉:
1. 无用历史代码
2. 隐形的多数据链路 / 多真相链路乱代码
3. 实现到一半但长期挂在主工程里的半成品代码
本文目标不是重复现有审计,而是把已有结论整理成:
1. 可执行的清洗范围
2. 明确的判定标准
3. 分阶段的推进顺序
4. 每阶段的交付物
5. 可以落地的验收与回滚口径
---
## 1. 先把“清什么”说清楚
这次文档里说的“无用分支”,优先指的是:
1. 代码逻辑分支
2. 数据链路分支
3. 兼容实现分支
4. 遗留入口分支
**不是先把 Git 分支清空。**
Git 分支治理可以后置做,但不能和首轮工程清洗混在一起,否则很容易把“代码归因”“入口归因”“历史责任归因”一起搅乱。
---
## 2. 三类清洗对象的定义
## 2.1 无用历史代码
满足以下任一特征,即进入“无用历史代码候选”:
1. 没有正式运行时入口,也没有当前规划要接回入口
2. 只被测试或历史兼容层引用,但主流程已经不再依赖
3. 与当前正式实现功能重复,但不是唯一真相源
4. 只剩 stub、占位、迁移残骸、旧 prompt 壳子、旧 helper 壳子
5. 生成产物仍留在主仓库,但已不再被正式流程消费
这类代码的处理目标是:
**删除、归档、降级标记三选一,不再长期以“也许以后要用”为理由挂在主路径里。**
## 2.2 隐形多数据链路乱代码
满足以下任一特征,即进入“隐形多链路问题候选”:
1. 同一份运行时状态同时由前端本地镜像和后端会话共同解释
2. 同一类任务、物品、剧情、鉴权逻辑在前后端或多模块里各维护一份
3. 同一份数据在“提交前本地写一份、提交后服务端再回填一份”
4. 同一功能表面只有一个按钮,背后却有两到三条实现路径
5. 正式链路和 fallback 链路长期并存,且没有退场时间
这类问题的处理目标是:
**把每条正式能力收敛成单一主链、单一真相源、单一编排出口。**
## 2.3 实现到一半的半成品代码
满足以下任一特征,即进入“半成品候选”:
1. UI、Hook、Service 已存在,但没有正式入口
2. 文档写了概念,代码只落了一半,后续也没有继续接完
3. 只有局部测试或局部 mock 在用,真实流程不用
4. 仍保留 TODO / stub / draft / launcher / modal但未纳入当前主线
5. 用户看不到、主流程不调用、团队也没有当前阶段交付计划
这类代码的处理目标是:
**要么纳入当前主线尽快补完,要么明确归档,不允许继续以“半活状态”污染主工程。**
---
## 3. 这轮清洗后的目标状态
本轮完成后,工程应至少达到下面 7 个状态:
1. 同一领域只保留一条正式主链,不再出现前后端双真相或多桥接链路并存
2. 无入口孤岛、旧兼容壳子、旧 prompt 壳子、旧 stub 文件有明确去留结果
3. “实现到一半”的模块不再伪装成正式能力挂在主工程中
4. 前端继续回到“表现层”,正式运行时逻辑、鉴权真相、任务物品编排继续向后端收口
5. 热点大文件不再同时背负历史残留、兼容残留和新逻辑堆叠
6. 文档与代码状态一致,不再让旧规划长期误导当前执行方向
7. `lint + typecheck + test + build + check:content` 重新成为可信基线
---
## 4. 执行原则
## 4.1 不做大爆炸整仓改写
本轮只允许“小批次、可回归、可解释”的连续清洗,不做一次性整仓推翻。
## 4.2 先建台账,再动删除
任何删除、归档、重定向动作前,必须先确认:
1. 当前入口关系
2. 当前依赖关系
3. 当前替代路径
4. 删除后的验收路径
没有台账,不做大规模删改。
## 4.3 先收真相源,再谈瘦身
如果同一领域仍有多条真相链路并存,优先收口真相源,而不是只删表面代码量。
## 4.4 文档和代码同步收口
只要本轮确认某条旧链降级、冻结、归档,相关文档必须同步更新,不能让旧文档继续把团队往废链路上拉。
## 4.5 每批清洗必须可回归
每一批完成后至少要求:
1. 入口可解释
2. 回归路径明确
3. 门禁可跑
4. 回滚点存在
---
## 5. 当前已知问题基础
本计划基于现有文档已经确认的结论推进,重点参考:
1. `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
2. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
3. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
4. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
5. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
按当前审计结果,首轮就应重点关注下面 3 组对象。
## 5.1 当前高置信度“无入口 / 孤岛 / 残留”候选
以下对象已经在最近审计中被点名,默认进入首轮复核台账:
1. `src/components/GameShell.tsx`
2. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
3. `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx`
4. `src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx`
5. `src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx`
6. `src/hooks/story/storyBootstrap.ts`
7. `src/hooks/useEquipmentFlow.ts`
8. `src/hooks/useForgeFlow.ts`
9. `src/hooks/useInventoryFlow.ts`
10. `src/services/customWorldPresentation.stub.ts`
11. `src/services/typewriter.ts`
12. `src/prompts/customWorldOrchestratorPrompts.ts`
13. `src/prompts/storyOrchestratorPrompts.ts`
14. `src/data/buildTagSimilarity.generated.ts`(已在后续清理批次中删除)
这些文件不能直接判死刑,但必须进入“保留 / 接回 / 归档 / 删除”四选一清单。
## 5.2 当前高置信度“隐形多链路 / 双真相”候选
以下对象应进入首轮主链收口清单:
1. `src/hooks/story/runtimeStoryCoordinator.ts`
2. `src/services/runtimeStoryService.ts`
3. `src/services/apiClient.ts`
4. `src/hooks/story/npcEncounterActions.ts`
5. `src/services/questDirector.ts`
6. `src/services/runtimeItemAiDirector.ts`
7. `src/services/ai.ts`
当前这批问题的共同特征是:
1. 前端仍保留本地镜像、自动登录凭证或双环境编排残留
2. NPC 任务换单、任务生成、运行时物品生成仍有前端发起和混合执行痕迹
3. 浏览器侧大型 AI orchestration 仍未完全退出主工程
## 5.3 当前“新热点继续吸纳历史复杂度”候选
以下对象不一定是垃圾代码,但很容易继续成为历史残留的新容器:
1. `src/components/CustomWorldEntityEditorModal.tsx`
2. `server-node/src/modules/assets/characterAssetRoutes.ts`
3. `src/prompts/storyPromptBuilders.ts`
4. `server-node/src/modules/custom-world/runtimeProfile.ts`
5. `src/components/game-shell/PreGameSelectionFlow.tsx`
6. `src/components/game-shell/PlatformHomeView.tsx`
这批文件必须在本轮中被视为“禁止继续裸堆新逻辑”的重点区域。
---
## 6. 清洗判定表
每个候选对象进入清理台账后,只允许落到下面 4 类结果之一:
| 结果类型 | 适用场景 | 处理动作 |
| --- | --- | --- |
| 删除 | 无入口、无当前规划、无兼容价值 | 直接删文件、删引用、补回归 |
| 归档 | 暂不继续,但保留历史价值 | 移出主路径、在文档中标明冻结状态 |
| 扶正 | 当前主线确实需要,只是入口丢失或命名混乱 | 接回正式入口、补测试、补文档 |
| 拆分收口 | 不是废代码,但混合了历史残留和正式逻辑 | 先拆职责,再删除残留分支 |
禁止出现第 5 种状态:
**“先留着,以后再说,但继续挂在主工程里。”**
---
## 7. 分阶段执行计划
## P0冻结新增污染先建立清洗台账
### 目标
先把“哪些东西要清、为什么清、怎么判定是否能清”讲清楚,停止继续往旧热点和疑似废链上加逻辑。
### 主要动作
1. 建立 3 份清单:
- 无入口孤岛清单
- 多真相链路清单
- 半成品能力清单
2. 为每个对象补 5 个字段:
- 当前入口
- 当前调用方
- 当前替代路径
- 建议结论
- 回归验证点
3. 约束新增开发:
- 不再向疑似废链补功能
- 不再向热点大文件直接叠逻辑
- 新需求优先接到当前正式主链
4. 明确本轮清洗后的唯一方向:
- 前端只做表现
- 后端持有正式运行时真相
- 旧兼容链不能继续膨胀
### 交付物
1. 清洗对象总台账
2. 首轮批次拆分表
3. 每批回归清单
### 完成标准
不是“开始删文件”才算开始。
只要台账、批次、判定口径和冻结规则明确,这一阶段就算完成。
---
## P1先清无入口孤岛和明显历史残留
### 目标
先把最容易污染阅读体验、又不需要大规模业务改造的对象清掉,快速降低仓库噪音。
### 优先清理对象
1. 无运行时入口组件
2. 只被测试引用的旧壳层
3. 已迁移后留下的 stub / prompt 壳 / helper 壳
4. 已不进入正式链路的 generated 文件
5. 旧 launcher / draft / modal 壳层
### 处理顺序建议
1. 先处理 `prompt / stub / helper / launcher` 级别的小残留
2. 再处理 `旧 hook / 旧 flow / 旧 shell` 级别的流程残留
3. 最后处理“可能有历史价值但暂不接回”的 UI 大块头
### 本阶段输出结果
每个对象必须给出明确结果:
1. 删除
2. 归档
3. 扶正接回
### 验收标准
1. 主工程中“没有正式入口的文件”显著减少
2. 新人看目录时,不再大量遇到真假难辨的旧入口
3. 相关引用、测试、文档同步更新
---
## P2收单一真相源清掉隐形多数据链路
### 目标
这阶段不以“删多少文件”为核心,而是以“同一件事最终只走一条正式链”作为核心。
### 第一优先级链路
1. 运行时快照链
2. 鉴权与自动登录链
3. NPC 任务生成 / 换单链
4. 运行时物品生成链
5. 浏览器端 AI orchestration 链
### 重点动作
1. 收掉前端“提交前先写本地真相,再等服务端回填”的链路
2. 收掉本地存储中的自动登录用户名 / 密码真相
3. 把 NPC 委托换单动作继续迁回后端运行时主链
4.`questDirector``runtimeItemAiDirector` 拆成:
- 前端 SDK 层
- 后端正式执行层
5. 继续压缩浏览器端 `src/services/ai.ts` 的正式职责
### 这阶段最重要的判断标准
不是“文件还在不在”,而是下面 4 条是否成立:
1. 玩家一次动作只提交一个正式 action而不是两边各写一遍状态
2. 前端不再持有正式运行时镜像真相
3. 前端不再长期持有自动登录账号密码
4. 同一类生成能力不再同时存在“浏览器正式版”和“后端正式版”
### 验收标准
1. 正式运行时状态解释权明确以后端为准
2. 鉴权边界不再依赖浏览器保存高风险凭证
3. NPC 任务、物品、剧情编排链路的职责边界清楚
---
## P3集中处理实现到一半的半成品能力
### 目标
把“看起来像功能、实际上不是当前正式能力”的对象清出主路径。
### 清理规则
半成品对象统一按下面规则处理:
1. 30 天内明确要接回主线的,进入补完批次
2. 当前阶段不做的,降级为归档或实验稿
3. 没有继续计划、也没有正式入口价值的,直接删除
### 本阶段重点对象
1. 只有 modal / launcher / draft 壳层,但没有正式调用链的 UI
2. 只有部分 hook / service 实现,但没有主链消费的流程模块
3. 只剩“概念占位”的 prompt、adapter、presentation、stub 文件
4. 文档里反复提到、代码里却长期不接线的能力块
### 必须同步做的事
1. 更新对应规划文档
2. 从当前主叙事中移除本轮明确不做的项
3. 给保留实验稿加清晰标签,避免被误读成正式能力
### 验收标准
1. 主工程里不再混着大量“像功能但不是正式功能”的对象
2. 文档不再持续推动团队回头补本轮已冻结能力
3. 目录层级和入口关系显著更清楚
---
## P4在减负后的基础上拆热点恢复可读性
### 目标
前 3 阶段做完后,再进入“真正让工程重新好读”的结构优化。
### 重点对象
1. `src/components/CustomWorldEntityEditorModal.tsx`
2. `server-node/src/modules/assets/characterAssetRoutes.ts`
3. `src/prompts/storyPromptBuilders.ts`
4. `server-node/src/modules/custom-world/runtimeProfile.ts`
5. `src/components/game-shell/PreGameSelectionFlow.tsx`
6. `src/components/game-shell/PlatformHomeView.tsx`
### 拆分原则
1. 先按职责拆,不按文件长度拆
2. 先把历史残留和兼容分支移走,再做正式模块化
3. 拆完之后必须更清晰地回答:
- 谁负责 UI
- 谁负责数据准备
- 谁负责正式规则
- 谁负责调用后端
### 验收标准
1. 热点文件不再同时吞 UI、规则、编排、兼容残留
2. 新功能不需要再跨四五层历史壳子一起改
3. 后续 review 能更快定位责任边界
---
## 8. 批次拆分建议
为了避免清理动作过大失控,建议按下面粒度推进:
## 批次 A小型孤岛与残留壳子
处理对象:
1. stub
2. prompt 壳
3. 无入口 helper
4. 无入口 launcher / modal
目标:
快速去噪,降低目录误导性。
## 批次 B旧 flow / 旧 shell / 旧 hook
处理对象:
1. `GameShell`
2. `storyBootstrap`
3. `useEquipmentFlow`
4. `useForgeFlow`
5. `useInventoryFlow`
目标:
清旧主流程壳层和旧流程残留。
## 批次 C运行时真相收口
处理对象:
1. `runtimeStoryCoordinator`
2. `runtimeStoryService`
3. `apiClient`
目标:
去掉本地镜像真相与本地鉴权真相。
## 批次 D任务 / 物品 / AI 混合执行层收口
处理对象:
1. `npcEncounterActions`
2. `questDirector`
3. `runtimeItemAiDirector`
4. `ai.ts`
目标:
消灭混合执行和双环境正式链。
## 批次 E热点大文件拆分
处理对象:
1. custom world
2. assets
3. game shell platform
4. prompt builder
5. runtime profile
目标:
在主链已收口后恢复可读性。
---
## 9. 每批必须产出的内容
每一批都必须带着下面 5 类产出结束:
1. 代码改动
2. 文档回填
3. 去留说明
4. 验收记录
5. 回滚点说明
如果一个批次只能产出“删了几个文件”,但说不清:
1. 删除后谁接手
2. 主链是否更清楚
3. 文档是否同步
那么这个批次不算完成。
---
## 10. 统一验收口径
本轮建议至少用下面 10 条作为统一验收口径:
1. `npm run lint`
2. `npm run test`
3. `npm run build`
4. `npm run check:content`
5. 目录中高置信度孤岛数量下降
6. 旧兼容链不再继续接收新逻辑
7. 前端不再保存自动登录用户名 / 密码
8. 运行时主状态不再由前端本地镜像优先解释
9. 当前正式能力的入口关系能在文档中说清楚
10. 新人阅读主目录和主流程文件时,不再频繁遇到真假并存入口
---
## 11. 风险与控制点
## 11.1 最大风险不是“删多了”,而是“边删边继续加废链”
如果没有冻结规则,这轮会一边清旧,一边又把新逻辑接回旧壳子里,最后只会重复劳动。
## 11.2 不能把“兼容”当永久借口
兼容链可以短期存在,但必须写清:
1. 为什么保留
2. 保留到什么时候
3. 谁负责后续移除
## 11.3 不能只删代码,不收文档
如果代码删了,旧文档不改,团队还是会持续把需求往旧链上接。
## 11.4 不能只盯文件大小,不盯真相链
有些文件很大但确实是正式主链。
有些文件很小,却是双真相和多链路的根源。
本轮必须优先盯后者。
---
## 12. 当前不建议优先做的事
1. 不建议在清洗期间继续横向扩功能
2. 不建议直接对热点文件做“纯格式化式拆分”
3. 不建议在未确认入口关系前整片删除可疑模块
4. 不建议让前端继续补正式运行时逻辑作为短期兜底
5. 不建议保留“也许以后有用”的主工程残留
原因很简单:
**当前最需要恢复的不是功能宽度,而是工程的干净边界、单一主链和可读体验。**
---
## 13. 推荐推进顺序
建议严格按下面顺序推进:
1. 先做 P0建台账、冻结污染
2. 再做 P1清无入口孤岛和小残留
3. 再做 P2收运行时、鉴权、任务物品的单一主链
4. 再做 P3处理半成品能力与文档冻结项
5. 最后做 P4拆热点、补结构可读性
不建议倒过来先拆热点。
因为如果历史残留和双真相还在,大文件拆完以后,复杂度只是换地方继续长。
---
## 14. 一句话结论
这轮工程大清洗的核心,不是“删旧代码看起来更清爽”,而是:
**用一轮有台账、有判定、有阶段、有验收的大清理,把无用历史代码、隐形多链路乱代码和半成品能力从主工程里真正清出去,让项目重新回到单一主链、单一真相源、目录可读、职责清楚的健康状态。**

View File

@@ -2,6 +2,7 @@
## 当前入口
- [ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md](./ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md):面向无用历史代码、隐形多数据链路和半成品实现的一轮工程大清洗执行计划,强调先建台账、再删重收口、最后恢复主工程可读性。
- [CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md](./CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md):当前阶段最值得优先做什么、为什么,以及它和审计/PRD 的对应关系。
- [CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md](./CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md):在不新增前端创作流程的前提下,围绕当前 Agent 创作动线做收口、删重、补通和文档收束的大白话执行规划。
- [EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md](./EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md):基于“前端只做表现、逻辑与数据全部后端化”的工程重构规划。

View File

@@ -2,6 +2,16 @@
更新时间:`2026-04-13`
## 0.1 当前状态说明2026-04-21
这份文档保留为第一阶段历史落地方案。
补充边界:
1. 文中出现的旧 Agent 副面板、旧世界生成链,不代表它们仍是当前版本待落地项
2. 已被工程清理判定退出主链并删除的对象,应以最新清理记录为准,不再按这里的历史计划继续接回
3. 当前版本执行优先级,应回到 `docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md`
## 0. 文档目的
这份文档用于把以下两份 PRD 收束成可直接开工的第一阶段实现方案:
@@ -1056,6 +1066,16 @@ onSubmit({
## 12. 落地文件清单
### 当前状态补充2026-04-21
下面这份文件清单是第一阶段编写当日的历史落地清单。
需要特别注意:
1. 其中列出的 `CustomWorldAgentLockBar.tsx``CustomWorldAgentQuickActions.tsx` 等旧副面板,已经在当前版本清理中退出主链并物理删除
2. 因此这里不能再被当作“当前版本仍要补齐的现行文件清单”
3. 当前仍然有效的执行边界,应以最新优化规划和清理记录为准
## 12.1 shared
必须新增:

View File

@@ -2,6 +2,15 @@
更新时间:`2026-04-13`
## 0.1 当前状态说明2026-04-21
这份文档保留为第二阶段历史落地方案。
补充边界:
1. 文中提到的 `CustomWorldAgentIntentSummaryPanel``CustomWorldAgentClarificationPanel``CustomWorldAgentLockBar``CustomWorldAgentQuickActions` 等旧副面板,已经在当前版本收口判断中退出主链并物理删除
2. 这些内容现在只能作为历史设计参考,不再作为当前版本默认待接线项
3. 当前如果要重新设计这些能力的消费方式,应基于现行主链重新定义
## 0. 文档目的
这份文档用于把以下两份文档进一步收束成第二阶段实现方案:
@@ -548,6 +557,21 @@ buildPendingClarifications(intent, readiness)
## 10. 前端实现方案
### 当前状态补充2026-04-21
这一节描述的是第二阶段编写当时的右侧副面板方案。
但当前版本已经明确:
1. `CustomWorldAgentIntentSummaryPanel`
2. `CustomWorldAgentClarificationPanel`
3. `CustomWorldAgentLockBar`
4. `CustomWorldAgentQuickActions`
这组旧副面板不再作为当前版本默认待接线方向,其中对应已落地但退出主链的文件也已物理删除。
因此本节以下内容应视为历史设计稿,不再直接代表当前版本执行方案。
## 10.1 修改 `CustomWorldAgentWorkspace.tsx`
第二阶段它不再只是空壳工作区,而要新增:
@@ -679,6 +703,12 @@ buildPendingClarifications(intent, readiness)
## 13. 落地文件清单
### 当前状态补充2026-04-21
本节列的是第二阶段历史文件清单。
其中涉及旧副面板的部分,现在只保留历史参考价值,不再等同于当前版本待落地项。
## 13.1 shared
必须修改:

View File

@@ -2,6 +2,15 @@
更新时间:`2026-04-14`
## 0.1 当前状态说明2026-04-21
这份文档保留为第三阶段历史落地方案。
补充边界:
1. 文中涉及的 `CustomWorldAgentQuickActions``CustomWorldAgentDraftDetailPanel``CustomWorldDraftCardDetailModal` 等旧副面板,已在当前版本清理中退出主链并物理删除
2. 因此这份文档里的对应实现章节,不能再直接视为当前版本待继续补完的目标
3. 当前版本只保留对正式主链仍有现实约束的能力项
## 0. 文档目的
这份文档用于把以下几份文档进一步收束成第三阶段实现方案:
@@ -721,6 +730,21 @@ object_refining
## 11. 前端实现方案
### 当前状态补充2026-04-21
这一节描述的是第三阶段编写当时的草稿抽屉与详情侧栏方案。
但当前版本已经明确:
1. `CustomWorldAgentDraftDrawer.tsx` 已在 earlier cleanup 中删除
2. `CustomWorldAgentDraftDetailPanel.tsx`
3. `CustomWorldDraftCardDetailModal.tsx`
4. `CustomWorldAgentQuickActions.tsx`
已在当前版本收口判断中退出主链并物理删除。
因此本节以下内容应视为历史设计稿,而不是当前版本默认待落地方案。
## 11.1 修改 `CustomWorldAgentQuickActions.tsx`
第三阶段必须让:
@@ -918,6 +942,12 @@ creatorIntentReadiness.isReady === false
## 14. 落地文件清单
### 当前状态补充2026-04-21
本节列的是第三阶段历史文件清单。
其中涉及旧抽屉、旧详情面板、旧快捷动作面板的部分,当前仅保留历史参考意义。
## 14.1 shared
必须修改:

View File

@@ -2,6 +2,15 @@
更新时间:`2026-04-14`
## 0.1 当前状态说明2026-04-21
这份文档保留为第四阶段历史落地方案。
补充边界:
1. 文中聚焦的草稿详情编辑链与实体生成弹窗链,在当前版本收口判断中已经退出主链
2. 对应的 `CustomWorldAgentDraftDetailPanel``CustomWorldDraftEditPanel``CustomWorldGenerateEntityModal``CustomWorldAgentQuickActions` 等文件已物理删除
3. 当前版本不再把这套旧副面板链作为默认待接线方向
## 0. 文档目的
这份文档用于把以下几份文档进一步收束成第四阶段实现方案:
@@ -665,6 +674,21 @@ generateAdditionalLandmarks(params)
## 10. 前端实现方案
### 当前状态补充2026-04-21
这一节描述的是第四阶段编写当时的草稿详情编辑链与实体生成弹窗链。
但当前版本已经明确:
1. `CustomWorldAgentDraftDetailPanel.tsx`
2. `CustomWorldDraftEditPanel.tsx`
3. `CustomWorldGenerateEntityModal.tsx`
4. `CustomWorldAgentQuickActions.tsx`
已在当前版本收口判断中退出主链并物理删除。
因此本节以下内容只保留历史设计参考价值,不再直接代表当前版本执行方向。
## 10.1 修改 `CustomWorldAgentDraftDetailPanel.tsx`
第四阶段它要从只读详情升级成:
@@ -851,6 +875,12 @@ editableSectionIds: []
## 13. 落地文件清单
### 当前状态补充2026-04-21
本节列的是第四阶段历史文件清单。
其中涉及草稿详情编辑链与实体生成弹窗链的部分,不再等同于当前版本待落地清单。
## 13.1 shared
必须修改:

View File

@@ -2,6 +2,15 @@
更新时间:`2026-04-14`
## 0.1 当前状态说明2026-04-21
这份文档保留为第五阶段历史落地方案。
补充边界:
1. 文中继续沿用的 `CustomWorldAgentDraftDetailPanel``CustomWorldAgentQuickActions` 等旧副面板,在当前版本已经退出主链并物理删除
2. 因此这份文档里的实现安排,不应再被直接视为当前版本执行清单
3. 当前版本是否重引入相关能力,必须基于新的主链设计重新判断
## 0. 文档目的
这份文档用于把以下几份文档进一步收束成第五阶段实现方案:
@@ -195,6 +204,19 @@
## 7.2 入口位置
### 当前状态补充2026-04-21
这一节以下描述依赖当时仍被视为现行方案的旧详情面板链与旧快捷动作链。
但当前版本已经明确:
1. `CustomWorldAgentDraftDetailPanel.tsx`
2. `CustomWorldAgentQuickActions.tsx`
已退出主链并物理删除。
因此这里关于入口位置的说明,现在只能作为历史资产工坊设计参考,不再代表当前版本 UI 入口。
### 角色卡详情入口
`CustomWorldAgentDraftDetailPanel` 中,当当前卡类型为:
@@ -548,6 +570,12 @@ mergeRoleAssetIntoDraftProfile(draftProfile, payload);
## 11. 前端实现方案
### 当前状态补充2026-04-21
本节以下内容依赖旧详情面板链与旧快捷动作面板。
这些文件当前已经退出主链并删除,所以这里不再是当前版本的直接执行清单。
## 11.1 修改 `CustomWorldAgentDraftDetailPanel.tsx`
当卡片类型为 `character` 时,新增:

View File

@@ -2,6 +2,15 @@
更新时间:`2026-04-14`
## 0.1 当前状态说明2026-04-21
这份文档保留为第六阶段历史落地方案。
补充边界:
1. 文中继续引用的 `CustomWorldAgentDraftDetailPanel``CustomWorldAgentQuickActions` 等旧副面板,在当前版本已经退出主链并物理删除
2. 这份文档可用于回看当时的资产工坊设想,但不代表当前版本仍按这里逐项补齐
3. 当前执行边界以最新优化规划和阶段四清理边界文档为准
## 0. 文档目的
这份文档用于把以下几份文档进一步收束成第六阶段实现方案:
@@ -190,6 +199,19 @@
## 7.2 入口位置
### 当前状态补充2026-04-21
这一节以下描述依赖当时仍被视为现行方案的旧详情面板链与旧快捷动作链。
但当前版本已经明确:
1. `CustomWorldAgentDraftDetailPanel.tsx`
2. `CustomWorldAgentQuickActions.tsx`
已退出主链并物理删除。
因此这里关于入口位置的说明,现在只保留历史场景资产工坊设计参考价值。
### 地点卡详情入口
`CustomWorldAgentDraftDetailPanel` 中,当当前卡类型为:

View File

@@ -2,6 +2,16 @@
更新时间:`2026-04-12`
## 0.1 当前状态说明2026-04-21
这份文档保留为历史 PRD 基线,用于回看当时的完整目标设计。
但需要特别注意:
1. 文中涉及的一部分旧 Agent 副面板与旧世界生成链,已经在 `2026-04-21` 的工程清理中判定退出当前版本主链并完成物理删除
2. 当前版本的真实执行边界,以 `docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md``docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md` 和最新工程清理批次记录为准
3. 阅读这份文档时,应把它视为“历史完整设计稿”,而不是“当前版本仍要全部继续落地的执行清单”
## 0. 文档目的
这份 PRD 用于把以下几份分析文档收束成一份可直接指导编码落地的新创作工具产品需求文档:

View File

@@ -8,7 +8,6 @@
- `src/data/buildDamage.ts`
- `src/data/buildTags.ts`
- `src/data/buildTagSimilarity.generated.ts`
现状不是“标签各自独立生效”,而是:
@@ -263,7 +262,7 @@ type BuildDamageBreakdown = {
## 4.3 相似度来源
当前仓库已有 `src/data/buildTagSimilarity.generated.ts`,但新方案不再以“标签-标签相似度矩阵”为主数据源。
旧版曾生成过标签-标签相似度矩阵,但新方案不再以“标签-标签相似度矩阵”为主数据源。
建议改为新增:
@@ -401,13 +400,13 @@ finalDamage =
### 风险 3旧数据迁移成本
问题:
现有 `buildTagSimilarity.generated.ts` 将弱化甚至失去主要用途
旧标签相似度矩阵已经不再作为主数据源
对策:
1. 本期不强制删除旧文件
2. 新逻辑只依赖新 affinity 表
3. 等新系统稳定后,再清理旧相似度矩阵和旧展示逻辑
1. 新逻辑只依赖标签定义中的属性亲和度
2. 旧相似度矩阵生成产物已从主工程移除
3. 后续若需要重新引入自动建议,也应输出为审表辅助数据,而不是运行时真相源
## 11. 一句话结论

View File

@@ -74,7 +74,6 @@
| `src/prompts/storyOrchestratorPrompts.ts` | 剧情中文修复 | `STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT` |
| `src/prompts/customWorldRolePromptDefaults.ts` | 角色资产工作台默认词唯一主源 | `buildDefaultRolePromptBundle` |
| `src/prompts/customWorldEntityActionPrompts.ts` | 编辑器技能动作词 | `buildSkillActionPrompt` |
| `src/prompts/qwenSpriteSheetToolPrompts.ts` | Qwen 精灵图工具 prompt 模型 | 主 prompt / sheet prompt / repair prompt / negative prompt 系列 |
### 3.3 共享层
@@ -91,7 +90,6 @@
- `src/services/questPrompt.ts`
- `src/services/runtimeItemAiPrompt.ts`
- `server-node/src/services/eightAnchorPromptBuilder.ts`
- `src/tools/qwenSpriteSheetToolModel.ts`
- `src/components/asset-studio/customWorldRolePromptDefaults.ts`
- `packages/shared/src/assets/qwenSprite.ts`
@@ -110,14 +108,12 @@
| --- | --- | --- |
| `server-node/src/prompts/characterAssetPrompts.ts` | 正式角色资产生成 prompt | 后端角色主图、动作试片、角色场景词主源 |
| `packages/shared/src/prompts/qwenSprite.ts` | 共享角色主 prompt 模板 | 共享给后端资产链使用的基础模板 |
| `src/prompts/qwenSpriteSheetToolPrompts.ts` | 工具链 prompt 模型 | Qwen 精灵图工具主词、分镜词、修帧词、负面词 |
| `src/prompts/customWorldRolePromptDefaults.ts` | 工作台默认词种子 | 角色视觉词、动画词、场景词默认值 |
| `src/prompts/customWorldEntityActionPrompts.ts` | 编辑器动作词 | 技能动作描述 prompt builder |
当前调用关系:
- `server-node/src/modules/assets/characterAssetRoutes.ts` 调用 `server-node/src/prompts/characterAssetPrompts.ts`
- `src/tools/QwenSpriteSheetTool.tsx` 通过兼容层消费 `src/prompts/qwenSpriteSheetToolPrompts.ts`
- `src/components/CustomWorldRoleAssetStudioModal.tsx` 通过兼容层消费 `src/prompts/customWorldRolePromptDefaults.ts`
- `src/components/CustomWorldEntityEditorModal.tsx` 直接调用 `src/prompts/customWorldEntityActionPrompts.ts`
@@ -145,7 +141,7 @@
- `src/services/customWorld.ts` 中的自定义世界分阶段 prompt 与场景背景图 prompt
- `src/services/ai.ts` 中的世界修复 / 语言修复 / JSON only system prompt
- `src/services/prompt.ts``characterChatPrompt.ts``questPrompt.ts``runtimeItemAiPrompt.ts` 这批前端 prompt 脚本
- `src/tools/qwenSpriteSheetToolModel.ts``src/components/asset-studio/customWorldRolePromptDefaults.ts``src/components/CustomWorldEntityEditorModal.tsx` 里的工具 / 编辑器 prompt 散点
- `src/components/asset-studio/customWorldRolePromptDefaults.ts``src/components/CustomWorldEntityEditorModal.tsx` 里的工具 / 编辑器 prompt 散点
## 8. 当前仍在非 Prompt 目录中的相关文件

View File

@@ -32,6 +32,14 @@
## 基础状态 Function
- `battle_attack_basic`
脚本:`src/data/functionCatalog/state/battleAttackBasic.ts`
说明:后端单行为战斗模型中的普通攻击 function。它由后端战斗 option 池下发,前端只透传 functionId不进入前端本地 `STATE_FUNCTION_DEFINITIONS` 候选池。
- `battle_use_skill`
脚本:`src/data/functionCatalog/state/battleUseSkill.ts`
说明:后端单行为战斗模型中的技能释放 function。每个技能 option 必须携带 `runtimePayload.skillId`,因此只登记文档和契约,不作为前端本地泛用 state function 生成。
- `battle_all_in_crush`
脚本:`src/data/functionCatalog/state/battleAllInCrush.ts`
说明:战斗中的正面强压动作,只在 `battle` 状态且有存活敌人时进入候选池。它会提高伤害与终结/爆发技能权重,同时抬高承伤,适合收头、压血和赌一波换血抢节奏。
@@ -110,6 +118,18 @@
脚本:`src/data/functionCatalog/npc/npcChat.ts`
说明:围绕当前话题与 NPC 继续交谈的 function。它会先生成对话正文再把真正的新选项延迟到 `story_continue_adventure` 之后展示。
- `npc_chat_quest_offer_view`
脚本:`src/data/functionCatalog/npc/npcChatQuestOffer.ts`
说明:聊天内待领取委托的查看入口,只查看 pending quest offer不立即写入正式任务日志。
- `npc_chat_quest_offer_replace`
脚本:`src/data/functionCatalog/npc/npcChatQuestOffer.ts`
说明:聊天内待领取委托的更换入口,重新走任务生成链替换当前 pending quest offer。
- `npc_chat_quest_offer_abandon`
脚本:`src/data/functionCatalog/npc/npcChatQuestOffer.ts`
说明:聊天内待领取委托的放弃入口,只清空 pending quest offer不影响已接任务。
- `npc_gift`
脚本:`src/data/functionCatalog/npc/npcGift.ts`
说明:向 NPC 送礼的入口 function。第一次点击通常只打开礼物面板确认礼物后才结算好感变化并继续剧情。
@@ -186,6 +206,7 @@
## 当前实现约定
- `src/data/stateFunctions.ts` 现在只负责基础 state function 的聚合、override 合并、运行时过滤和 option 解析。
- `src/data/stateFunctions.ts` 现在只负责前端本地基础 state function 的聚合、override 合并、运行时过滤和 option 解析。
- `battle_attack_basic` / `battle_use_skill` 虽然属于后端运行时契约中的战斗 function但不进入 `STATE_FUNCTION_DEFINITIONS`。它们由后端 runtime story / combat option 池生成,避免前端本地生成缺少 `runtimePayload` 的假选项。
- 非 state function 目前仍由各自原有流程模块执行,但它们的 `id`、标题和详细说明已经统一收口到 `functionCatalog/`
- 后续新增 function 时,建议先补独立脚本,再把运行时调用接进来,最后同步这份目录文档。

View File

@@ -0,0 +1,99 @@
# Agent 对话框与结果页精修职责边界修正
更新时间:`2026-04-21`
## 1. 结论
本次修正把“Agent 对话框”和“结果页精修”重新拆清楚:
1. `CustomWorldAgentWorkspace` 只负责八锚点信息收集、八锚点进度展示、八锚点完成后的“整理世界底稿”动作。
2. “精修”不是 Agent 对话框里的概念,不再通过 Agent 建议动作进入角色、地点、世界总卡的局部修整。
3. 已经生成底稿的草稿,从创作中心点击后进入结果页继续完善。
4. 尚未生成底稿的草稿,从创作中心点击后才恢复 Agent 对话框继续补齐八锚点。
5. 结果页负责成稿后的编辑、补全、进入世界前确认和自动保存,并通过 `sync_result_profile` 回写到当前 Agent session。
一句话:
**Agent 收八锚点,结果页做精修。**
---
## 2. 为什么要修正
旧实现把 `object_refining` 草稿卡片显示成“继续精修”,但点击后直接恢复 Agent 工作区。
这个行为会让用户产生两个误解:
1. 以为精修是 Agent 对话框里的下一阶段。
2. 以为 Agent 对话框不仅负责收集八锚点,还负责后续对象级编辑。
这和当前产品边界不一致。Agent 对话框应该保持轻量,只用于拿到足够稳定的八锚点输入;对象、场景、封面、世界档案的修整都应该在结果页完成。
---
## 3. 当前落地规则
### 3.1 创作中心草稿点击分流
`custom-world/works` 返回 `agent_session` 草稿后,前端按草稿是否已有底稿内容分流:
1. `playableNpcCount <= 0 && landmarkCount <= 0`
- 视为八锚点仍未整理成底稿。
- 点击进入 `agent-workspace`
2. `playableNpcCount > 0 || landmarkCount > 0`
- 视为已有可编辑底稿。
- 点击读取对应 Agent session编译为 `CustomWorldProfile`,进入 `custom-world-result`
### 3.2 Agent 对话框动作边界
Agent 会话建议动作只保留:
1. 总结当前设定 / 总结当前世界底稿。
2. 八锚点准备完成后的“整理一版世界底稿”。
不再在 Agent 会话快照里继续生成或兼容展示:
1. `refine_focus_target`
2. “精修角色”
3. “继续补地点”
4. “先看世界总卡”
旧 session 快照如果仍带有 `refine_focus_target`,服务端兼容层会过滤掉,避免旧数据把精修入口重新塞回 Agent 对话框。
### 3.3 结果页精修边界
Agent 来源结果页不再是冻结预览态。
当前允许在结果页继续进行成稿精修,包括:
1. 编辑世界信息。
2. 编辑角色、场景、封面等对象档案。
3. 删除或调整已有对象。
4. 自动保存到作品草稿。
5. 进入世界前通过 `sync_result_profile` 写回 Agent session。
为了保持主链简洁Agent 来源结果页仍不重新打开“通过 Agent 对话精修对象”的入口。
---
## 4. 对历史文档口径的覆盖
这份文档覆盖 [AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md](./AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md) 中“Agent 来源结果页冻结为预览态”的阶段性口径。
新的主口径是:
1. Agent 来源结果页可以编辑,因为精修本来就应该发生在结果页。
2. 需要收紧的是 Agent 对话框,不是结果页。
3. 结果页编辑后仍必须同步回 Agent session保持进入世界前的数据真相源一致。
---
## 5. 验收标准
本次修正完成后应满足:
1. 创作中心已有底稿草稿按钮文案为“继续完善”,点击进入结果页。
2. 创作中心未成稿草稿按钮仍为“继续创作”,点击进入 Agent 对话框。
3. Agent 对话框不出现“精修角色 / 补地点 / 看世界总卡”类对象精修入口。
4. Agent 来源结果页可以打开编辑弹窗进行精修。
5. 返回创作从结果页回到创作中心,不回到 Agent 对话框。

View File

@@ -0,0 +1,119 @@
# Agent 草稿结果页资产合并修复 2026-04-21
更新时间:`2026-04-21`
## 1. 问题现象
当前创作流程里,用户在“生成草稿”后反馈:
1. 角色主图没有稳定出现在结果页
2. 场景背景图有时可见,有时角色图缺失
3. 自动保存后的作品库条目里,分幕图可能已经存在,但场景角色主图仍为空
## 2. 本次真实排查结论
本轮不是单一的“没写数据库”问题,而是 `agent draft -> result profile` 桥接层存在一类更隐蔽的集合漂移问题。
排查后确认:
1. 最新 `custom_world_sessions.payload_json` 里的 `draftProfile.storyNpcs[].imageSrc` 已经存在
2. 最新 `draftProfile.sceneChapters[].acts[].backgroundImageSrc` 也已经存在
3. 对应图片文件也真实存在于仓库根 `public/`
4. 最新 `custom_world_profiles.payload_json` 里,分幕图通常已保存成功
5. 但场景角色主图可能仍为空
根因在于:
1. 结果页桥接层在 `draftProfile.legacyResultProfile` 存在时,仍把 `legacyResultProfile` 视为主列表
2. 旧逻辑只会按 `id``draftProfile` 里的图片字段回贴到 `legacyResultProfile`
3. 一旦后续草稿精修导致 `draftProfile` 的角色集合、角色 id 或角色命名发生漂移
4.`legacyResultProfile` 就会继续主导结果页和自动保存对象列表
5. 最新角色主图虽然已在 `draftProfile` 里生成完成,但会因为匹配失败而被整批吞掉
这类问题在场景角色上最明显,因为角色集合最容易在后续精修中替换。
## 3. 修复策略
本轮在:
- `src/services/customWorldAgentDraftResult.ts`
调整桥接规则:
1. `legacyResultProfile` 仍保留,继续提供运行时富字段
2. 但角色、场景、分幕等对象集合不再默认由 `legacyResultProfile` 主导
3. 最新 `draftProfile` 成为结果页对象列表的主来源
4. `legacyResultProfile` 只负责给命中的对象补运行时富字段
5. 匹配优先级为:
- 先按 `id`
- 再按名称兜底
具体规则:
1. `playableNpcs`:以最新 draft 集合为主legacy 只补富字段与旧运行时字段
2. `storyNpcs`:同上,避免旧角色列表吞掉新角色主图
3. `sceneChapterBlueprints`:以最新 draft 幕列表为主legacy 只补章节/幕已有运行时字段
4. `landmarks`:优先更新最新 draft 命中的场景对象,但保留 legacy 中未被命中的剩余运行时场景,避免丢连接与残留信息
5. `camp`:保留 legacy 基础信息,但优先取 draft 最新图片字段
## 4. 修复后的链路意义
修复后:
1. 草稿自动资产服务生成的角色主图不会再因为旧 `legacyResultProfile` 的角色集合过时而丢失
2. 分幕图继续可以稳定进入结果页与自动保存
3. 作品库自动保存时,结果页编译出的 profile 更接近“当前草稿真实快照”,而不是历史 legacy 快照
## 5. 新增验证
本轮补了前端桥接测试:
- `src/services/customWorldAgentDraftResult.test.ts`
新增验证点:
1.`draftProfile.storyNpcs``legacyResultProfile.storyNpcs` 集合漂移时
2. 结果页仍应优先展示最新 draft 角色
3. 最新角色主图与最新分幕图不能被旧 legacy 快照吞掉
## 6. 当前状态
本轮修复后,本地已验证:
1. `src/services/customWorldAgentDraftResult.test.ts`
2. `src/components/CustomWorldResultView.test.tsx`
3. `src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
4. `npm run check:encoding`
均通过。
## 7. 后续建议
这次问题再次说明:
1. `legacyResultProfile` 继续长期作为结果页主列表来源风险很高
2. 只做“按 id 回贴图片字段”在对象集合会漂移的链路里不够稳
3. 后续如果继续推进后端单一真相源,应优先把结果页对象集合完全改为服务端最新 compile/preview 结果,而不是继续让前端桥接层承担最终裁决
---
## 8. 2026-04-21 补充:新建共创会话 500 根因
后续联调里又发现一条与资产合并无关、但会直接阻断创作入口的后端问题:
1. 点击“创建新 RPG 游戏”时,`POST /api/runtime/custom-world/agent/sessions` 返回 `500`
2. 表面上前端只看到“服务器内部错误”,实际根因在路由层
本次补查确认:
1. `server-node/src/routes/customWorldAgent.ts` 这组路由内部直接使用 `request.userId!`
2. 但路由文件本身在 `2026-04-21` 修复前没有挂 `requireJwtAuth(...)`
3. 结果是 HTTP 请求虽然带了登录 token`request.userId` 并不会被注入
4. 后端继续拿 `undefined` 作为 `userId` 创建 / 读取 agent session最终在仓储写库阶段触发 `500`
修复方式:
1.`createCustomWorldAgentRoutes(...)` 顶部统一补上 `router.use(requireJwtAuth(context.config, context.userRepository))`
2. 让 session 创建、读取、发消息、执行 action、读取 operation / card 详情全部走同一层鉴权注入
这次补丁的意义不是新增功能,而是把 `custom-world agent` 路由和其他 `rpg-entry / rpg-profile / rpg-runtime` 受保护接口重新对齐,避免再出现“路由里依赖 `request.userId`,但入口没挂鉴权”的低层断线问题。

View File

@@ -0,0 +1,119 @@
# 创作流程草稿/图片/动作自动保存数据库检查 2026-04-21
更新时间:`2026-04-21`
## 1. 本次检查范围
本次检查只聚焦当前创作流程里下面这条链路:
`结果页前端编辑 -> 自动保存 -> Agent session 主链同步 -> 作品库落库`
重点核对三类内容:
1. 草稿文本类修改
2. 生成后的角色图片、地点图片、分幕图
3. 角色动作相关资产字段
## 2. 当前实际自动保存链路
当前前端主入口在:
- `src/components/game-shell/PreGameSelectionFlow.tsx`
实际行为如下:
1. 结果页编辑统一通过 `onProfileChange` 更新 `generatedCustomWorldProfile`
2. 当结果页停留在 `custom-world-result` 阶段时,前端会对 profile 做防抖自动保存
3. 如果当前结果页来源是 `agent-draft`,自动保存前会先执行 `sync_result_profile`
4. `sync_result_profile` 完成后,前端不直接保存旧内存 profile而是优先保存“从最新 session 重编译出的 profile”
5. 作品库保存最终走 `PUT /api/runtime/custom-world-library/:profileId`
6. Express 后端通过 `runtimeRepository.upsertCustomWorldProfile(...)` 把 profile 写入 `custom_world_profiles.payload_json`
所以数据库层本身是有正常落库能力的。
## 3. 本次检查前确认成立的部分
以下能力在本次检查前已经成立:
1. 结果页普通草稿字段编辑会触发自动保存
2. 自动保存会真正调用后端作品库接口并更新数据库
3. 返回创作、进入世界两条路径也会优先同步 Agent session
4. `legacyResultProfile` 已作为阶段一桥接快照保留在 session 中
## 4. 本次发现的真实风险
风险不在数据库写入本身,而在:
`sync_result_profile -> session 重编译结果页 profile`
此前 `sync_result_profile` 只回写:
1. 基础摘要字段
2. `legacyResultProfile`
但没有把结果页里已经确认过的资产字段同步回 foundation draft 对应节点。
这会导致一个阶段性风险:
1. 用户在结果页换了新的角色图
2. 或者结果页里刚确认了新的动作资产字段
3. 或者结果页里刚确认了新的地点图、分幕图
4. 自动保存前前端先做一次 session 同步
5. 同步完成后又从 session 重编译结果页 profile
6. 重编译过程会把 draft 层旧资产字段再次并入结果 profile
这样就可能出现:
**数据库自动保存成功了,但保存进去的是“被旧 draft 资产字段回退过的版本”,不是用户刚在结果页看到的最新图/动作。**
## 5. 本轮修复
本轮在:
- `server-node/src/services/customWorldAgentOrchestrator.ts`
补了一个收窄修复:
1. `sync_result_profile` 仍然保持阶段一边界,不做整套 runtime -> foundation draft 反解
2. 但会按相同 id把结果页里已确认的资产字段同步回 draft 层已有对象
3. 同步范围包括:
- 角色 `imageSrc`
- 角色 `generatedVisualAssetId`
- 角色 `generatedAnimationSetId`
- 角色 `animationMap`
- 地点 `imageSrc`
- 分幕 `backgroundImageSrc`
- 分幕 `backgroundAssetId`
这样后续再从 session 重编译结果页 profile 时,最新资产字段不会再被旧 draft 值回退。
## 6. 验证补充
本轮补了服务端测试:
- `server-node/src/services/customWorldAgentPhase4.test.ts`
新增验证点:
1. `sync_result_profile` 后,最新角色主图会写回 draft
2. 最新角色动作资产字段会写回 draft
3. 最新地点图会写回 draft
4. 最新分幕图会写回 draft
## 7. 结论
截至本轮修复后,当前创作流程里:
1. 草稿文本修改可以自动保存到数据库
2. 结果页中确认后的角色图、地点图、分幕图可以随自动保存稳定进入数据库
3. 角色动作相关资产字段可以随 session 同步和自动保存稳定保留
但仍需注意:
1. 当前仍是阶段一兼容链路,核心桥接字段仍然是 `legacyResultProfile`
2. 正式发布链 `publish_world` 还没有在当前阶段打通
3. 前端仍依赖 `buildCustomWorldProfileFromAgentDraft()` 作为 session -> 结果页兼容编译层
因此本轮结论是:
**当前“前端修改 -> 自动保存 -> 数据库”主链可用;本次已补上图片与动作资产在 session 重编译阶段的回退风险。**

View File

@@ -0,0 +1,212 @@
# Agent 结果页深度编辑回写主链方案(阶段一)
更新时间:`2026-04-20`
## 1. 这次阶段一先改什么
这次阶段一不做结果页只读化。
结果页继续保留当前已经可用、而且用户已经满意的这些能力:
1. 结果页继续允许深度编辑世界设定
2. 结果页继续允许编辑角色、场景、营地、封面
3. 结果页继续允许直接新增角色与地点
4. 结果页继续保留当前已有的浏览、自动保存、进入世界体验
这次真正要补的是:
**把结果页里产出的完整 `CustomWorldProfile`,同步回 `Agent session`,让结果页编辑不再游离在主链之外。**
---
## 2. 当前真正的问题
当前链路里,结果页虽然还能深度编辑,但数据职责是分裂的:
```text
Agent session
-> 前端 buildCustomWorldProfileFromAgentDraft()
-> 结果页本地 profile
-> 结果页继续深度编辑
-> 自动保存到 custom-world-library
-> 进入世界
```
这里最大的问题不是“结果页能编辑”,而是:
1. 结果页编辑后的最新世界结构,没有稳定回写到 `Agent session`
2. 用户从结果页返回 Agent 工作区后session 侧仍可能停留在较旧的草稿状态
3. “结果页当前看到的世界”“Agent session 当前保存的草稿”“作品库里自动保存的 profile”可能不是同一份东西
4. 进入世界时如果直接吃当前前端内存态,也会继续放大这个分叉
所以阶段一要解决的是:
**结果页仍然是深度编辑器,但它编辑的是 Agent 主链里的当前结果快照,不是脱链的本地副本。**
---
## 3. 阶段一目标状态
阶段一把链路先收成下面这样:
```text
Agent session
-> 前端 buildCustomWorldProfileFromAgentDraft() 生成结果页初始 profile
-> 用户在结果页继续深度编辑 profile
-> 前端调用新的 Agent action把完整结果 profile 同步回 session
-> session 保留:
- 当前 foundation draft
- 当前 legacyResultProfile 结果快照
- 重编译后的 draftCards / assetCoverage / suggestedActions
-> 自动保存与进入世界都优先基于已同步的 session 结果快照执行
```
这一步仍然是过渡态,不是最终态。
因为:
1. 阶段一还不打通 `publish_world`
2. 阶段一也不把结果页改造成完全原生的 draft 编辑器
3. 阶段一允许继续保留 `draftProfile.legacyResultProfile` 作为兼容桥接字段
但至少要做到:
**结果页的深度编辑,必须进入 Agent session 的单一主链。**
---
## 4. 阶段一具体实现边界
## 4.1 新增 Agent action`sync_result_profile`
阶段一新增一个面向结果页的 Agent action
```ts
{ action: 'sync_result_profile'; profile: CustomWorldProfileRecord }
```
用途只有一个:
把结果页当前完整 `CustomWorldProfile` 快照同步回 `CustomWorldAgentSessionRecord`
它不是发布动作,也不是世界编译动作。
它只是把结果页当前编辑结果认回主链。
---
## 4.2 服务端写回策略
服务端接到 `sync_result_profile` 后,按下面规则处理:
1. 读取当前 session
2. 取当前 `draftProfile`
3. 保留当前 draft 层已有的结构化字段:
- `playableNpcs / storyNpcs / landmarks / camp`
- `factions / threads / chapters / sceneChapters`
- `worldHook / playerPremise / openingSituation / iconicElements`
- 以及现有资产、scene chapter 等字段
4. 把结果页传来的完整 `CustomWorldProfile` 写入 `draftProfile.legacyResultProfile`
5. 对于 draft 层里本来就和结果页一一对应、且结果页已经改动的字段,同步覆盖基础摘要字段:
- `name`
- `subtitle`
- `summary`
- `tone`
- `playerGoal`
- `majorFactions`
- `coreConflicts`
6. 重新编译 `draftCards`
7. 重建 `assetCoverage`
8. 刷新 `suggestedActions`
9. 写入 action result message 和 checkpoint
这里故意不在阶段一做“把完整 runtime profile 反解成一整套全量 foundation draft 结构”的大重构。
原因是:
1. 结果页当前已经支持很多深度编辑字段
2. 如果现在硬做全量反编译,最容易把场景章节、多幕、资产字段写坏
3. 阶段一应该先保证“结果页编辑不脱链”,而不是一次性重做所有模型映射
---
## 4.3 前端触发策略
前端只在 `customWorldResultViewSource === 'agent-draft'` 时走这条同步链。
具体规则:
1. 结果页 profile 每次发生变化时,继续允许本地即时更新
2. 但在自动保存前,先把 profile 通过 `sync_result_profile` 同步到 Agent session
3. 返回创作时,如果要重新读 Agent 草稿,也应优先以最新 session 为准
4. 点击“进入世界”时,先拉取最新 session再重新 `buildCustomWorldProfileFromAgentDraft()`,避免吃到旧的前端缓存 profile
这样阶段一就能做到:
1. 结果页编辑体验不变
2. Agent session 成为结果页编辑后的可恢复真相源
3. 自动保存、返回创作、进入世界三条路都围绕同一份 session-backed 结果快照
---
## 5. 阶段一明确不做什么
这次阶段一明确不做:
1. 不关闭结果页当前已有的编辑器能力
2. 不删除结果页当前已有的 AI 新增角色/地点能力
3. 不打通 `publish_world`
4. 不把 `legacyResultProfile` 直接删掉
5. 不把结果页整个改写成只操作 draft card 的新系统
6. 不把旧 `custom-world/sessions` 链在本阶段直接物理移除
---
## 6. 验收标准
阶段一做完后,至少要满足下面这些结果:
1. Agent 草稿结果页继续保持当前深度编辑体验不变
2. 结果页发生编辑后Agent session 中能看到同步后的最新结果快照
3. 从结果页返回创作后,不会明显回退到较旧的草稿态
4. 点击“进入世界”时,会优先使用最新 session 重新编译结果,而不是只依赖前端旧内存态
5. 自动保存到作品库的 profile 与当前 session 结果快照保持一致
---
## 7. 一句话结论
阶段一不是收掉结果页,而是把结果页继续保留为深度编辑器,同时补上一条正式的 session 回写链,让它不再游离在 Agent 主链之外。
---
## 8. 2026-04-20 实际落地结果
本轮已经按阶段一目标完成下面这些收口:
1. 前端结果页自动保存时,若当前来源是 `agent-draft`,会先执行 `sync_result_profile`
2. `sync_result_profile` 完成后,自动保存不再直接写旧的前端内存 profile而是优先保存从最新 session 重新 `buildCustomWorldProfileFromAgentDraft()` 得到的结果快照
3. 点击“进入世界”时,仍会先同步 session再基于最新 session 重编译 profile 后进入世界
4. 点击“返回创作”时,也会先做一次结果页到 session 的同步兜底,再返回 Agent 工作区
5. 为避免用户刚从结果页返回工作区又被自动重开逻辑顶回结果页,前端补了一层显式返回抑制标记
6. 服务端 `sync_result_profile` 现已按阶段一边界收窄为“保留 foundation draft 结构,只更新基础摘要字段和 `legacyResultProfile`”,没有提前做整套 runtime -> draft 反解
这意味着阶段一当前已经把下面三条路径收回到同一条 session 主链:
1. 自动保存到作品库
2. 返回 Agent 工作区继续创作
3. 从结果页直接进入世界
## 9. 本轮仍然保留的阶段性边界
这次落地后,仍然保留文档原先约定的过渡边界:
1. 结果页深度编辑能力不做收缩
2. `draftProfile.legacyResultProfile` 继续作为兼容桥接字段保留
3. `publish_world` 仍未在这一轮打通
4. 前端仍然使用 `buildCustomWorldProfileFromAgentDraft()` 作为 session -> 结果页的兼容编译层
所以下一阶段如果要继续推进,重点应转向:
1. 降低前端对 legacy profile 编译桥接的依赖
2. 继续把发布链路收口到 Agent session / service 侧
3. 逐步缩减结果页直改 legacy profile 的历史职责

View File

@@ -0,0 +1,74 @@
# Agent 结果页与平台入口收口方案(阶段二)
更新时间:`2026-04-20`
## 1. 阶段二目标
阶段一已经把 Agent 结果页编辑快照同步回 session 主链。阶段二不继续扩大结果页编辑能力,而是把入口和职责继续收紧:
1. 平台“创作”入口统一读取 `custom-world/works` 聚合列表
2. Agent 草稿和已保存作品在同一个入口里展示
3. 草稿点击后恢复 Agent session已保存作品点击后进入作品详情
4. Agent 结果页不再暴露“继续在结果页补世界结构”的新增入口
一句话目标:
**让用户从平台创作入口能稳定找回草稿和作品,同时让结果页更像收口预览,而不是另一套编辑器。**
---
## 2. 本阶段不做什么
阶段二明确不做:
1. 不物理删除旧 `custom-world/sessions`
2. 不打通 `publish_world`
3. 不重做结果页 UI
4. 不删除已保存作品的继续编辑入口
5. 不把结果页整体改成只读
这些事项留给后续阶段继续拆。
---
## 3. 平台入口落地规则
平台“创作”Tab 改为优先展示 `listCustomWorldWorks()` 的聚合结果:
1. `agent_session` 类型展示为草稿,可点击恢复 Agent 工作区
2. `published_profile` 类型展示为作品,可点击进入作品详情
3. 聚合接口失败时保留现有作品库 `myEntries` 兜底
4. 不新增平行页面,复用已有 `CustomWorldCreationHub`
这样用户不再需要依赖隐藏 sessionId 或旧作品库入口才能找回创作。
---
## 4. 结果页职责收口规则
Agent 来源结果页继续保留:
1. 浏览世界、角色、场景
2. 自动保存
3. 返回 Agent 工作区
4. 进入世界
Agent 来源结果页本阶段收紧:
1. 不再显示直接新增可扮演角色、场景角色、场景的入口
2. 不再把“去 Agent 调整设定”设计成结果页内部继续补世界结构
3. 如需继续调整,返回 Agent 工作区
已保存作品的结果页仍保持现有编辑能力,避免破坏作品库已有体验。
---
## 5. 验收标准
阶段二完成后应满足:
1. 平台“创作”Tab 能看到 Agent 草稿和已保存作品的统一列表
2. 点击 Agent 草稿能恢复对应 Agent 工作区
3. 点击已保存作品能进入原有作品详情
4. Agent 结果页不再显示直接新增角色/地点的入口
5. 已保存作品的结果页编辑能力不受影响

View File

@@ -0,0 +1,148 @@
# Agent 结果页旧链降级与预览冻结方案(阶段三)
更新时间:`2026-04-20`
## 1. 阶段三目标
阶段一已经把结果页编辑同步回 Agent session 主链。
阶段二已经把平台“创作”入口统一到 `custom-world/works` 聚合列表,并收紧了 Agent 结果页里的新增入口。
阶段三不继续扩功能,而是继续做两件事:
1. 让旧 pipeline 在主入口里进一步降级,不再和 Agent 主链抢“草稿”职责
2. 让 Agent 来源结果页进一步冻结为“预览/收口层”,不再继续承担 legacy profile 直改编辑器职责
一句话目标:
**把还在和 Agent 主链并行的旧职责继续降级,避免系统自己和自己打架。**
---
## 2. 当前剩余问题
虽然阶段一、二已经把主链收紧了不少,但当前还保留两个明显的并行口:
### 2.1 创作中心里旧 library 草稿仍可能继续冒充主草稿
当前 `listCustomWorldWorkSummaries()` 会把 runtime library 里的所有 profile 都折成 `published_profile` 类型返回。
这意味着:
1. `visibility = 'draft'` 的 library 草稿仍会继续出现在创作中心
2. 创作中心里同时存在:
- Agent session 草稿
- library 草稿
- 已发布作品
3. 用户看到的“草稿”概念仍然可能混成两套
阶段三需要明确:
**创作中心主入口只认 Agent session 草稿 和 已发布作品,不再继续把 library draft 当主草稿展示。**
---
### 2.2 Agent 结果页仍能继续打开旧 legacy 编辑器
当前 Agent 来源结果页虽然已经不再暴露“新增角色/新增地点”入口,但仍然保留下面这些旧编辑链:
1. 点击世界概述/基本设定仍能打开 legacy world editor
2. 点击角色、场景、封面仍能继续进入旧 profile 编辑弹窗
3. 这些编辑器本质上仍然是在改 legacy `CustomWorldProfile`
这会带来两个问题:
1. Agent 结果页继续像一套“旧编辑器”
2. “去 Agent 调整设定”和“结果页直接改 legacy profile”两条路仍然并行存在
阶段三需要明确:
**Agent 来源结果页继续保留浏览、自动保存、返回创作、进入世界,但不再继续承担 legacy profile 深编辑职责。**
---
## 3. 阶段三落地规则
## 3.1 创作中心只展示两类主入口内容
`custom-world/works` 在阶段三只保留下面两类条目:
1. `agent_session`
- 统一视为草稿
- 点击后恢复 Agent 工作区
2. `published_profile`
- 统一视为已发布作品
- 点击后进入现有作品详情
明确不再把下面这类内容继续塞进创作中心主入口:
1. library 中 `visibility = 'draft'` 的兼容草稿
这些兼容草稿仍然保留在作品库/详情链路里,不在本阶段物理删除,但不再继续占创作中心“草稿主入口”。
---
## 3.2 Agent 来源结果页冻结为预览态
`customWorldResultViewSource === 'agent-draft'` 时,结果页阶段三继续保留:
1. 浏览世界信息
2. 浏览角色、地点、场景结构
3. 自动保存
4. 返回 Agent 工作区
5. 进入世界
同时阶段三进一步收紧:
1. 不再打开世界/角色/场景/封面的 legacy 编辑弹窗
2. 不再提供删除角色、删除场景等旧 profile 直改入口
3. Agent 来源结果页上的对象卡统一作为“查看详情”预览卡使用
已保存作品的结果页编辑能力继续保留,不在本阶段收缩,避免破坏已有作品库编辑体验。
---
## 3.3 结果页同步动作只在真的发生差异时执行
阶段一补的 `sync_result_profile` 仍然保留,但阶段三补一个行为约束:
1. 如果当前 Agent 结果页 profile 和最新 session 重编译结果签名一致
2. 那么返回创作、进入世界、自动保存前不再重复触发一次 `sync_result_profile`
目的不是省接口,而是明确:
**结果页同步是“有改动才回写”的主链动作,不是每次离开页面都机械重放。**
---
## 4. 阶段三明确不做什么
这次阶段三明确不做:
1. 不物理删除旧 `custom-world/sessions` 相关服务与兼容代码
2. 不打通 `publish_world`
3. 不把前端 `buildCustomWorldProfileFromAgentDraft()` 兼容编译层移除
4. 不删除 `draftProfile.legacyResultProfile`
5. 不收缩已保存作品的 legacy 编辑器能力
阶段三只做主入口降级与 Agent 结果页职责冻结,不做更大的模型替换。
---
## 5. 验收标准
阶段三完成后应满足:
1. 创作中心不再把 library draft 兼容作品继续显示为“草稿主入口”
2. 创作中心里只保留 Agent 草稿和已发布作品两类主入口内容
3. Agent 来源结果页不再能继续打开 legacy 世界/角色/场景编辑弹窗
4. 已保存作品结果页编辑能力不受影响
5. Agent 结果页在未发生改动时,返回创作/进入世界/自动保存不会重复触发无意义的 `sync_result_profile`
---
## 6. 一句话结论
阶段三不是删除兼容层,而是把它们继续降级到不会抢主流程职责的位置上:
**创作中心只认 Agent 草稿和已发布作品Agent 结果页只负责预览与收口,不再继续充当旧编辑器。**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
# 创作链路重构工作包 A 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 A命名规范与目录骨架**,约束如下:
1. 先建立 RPG 创作域的新命名落点。
2. 先提供 façade 和 barrel不迁移主流程行为。
3. 不提前修改工作包 B 到 H 的大块业务逻辑。
## 2. 本次已落地内容
## 2.1 前端目录骨架
已新增以下目录与 façade
1. `src/components/game-shell/rpg-creation-flow/`
2. `src/components/rpg-creation-result/`
3. `src/components/rpg-creation-editor/`
4. `src/services/rpg-creation/`
当前策略:
1. `RpgCreationShell` 继续桥接旧的 `PreGameSelectionFlow`
2. `RpgCreationResultView` 继续桥接旧的 `CustomWorldResultView`
3. `RpgCreationEntityEditorModal` 继续桥接旧的 `CustomWorldEntityEditorModal`
4. `rpgCreation*Client` 继续桥接 `aiService.ts``storageService.ts``customWorldCoverAssetService.ts`
5. `rpgCreationPreviewAdapter` 继续桥接旧的前端草稿编译函数,明确它只是过渡层。
## 2.2 后端目录骨架
已新增以下 RPG 创作域 façade
1. `server-node/src/routes/rpgCreationAgentRoutes.ts`
2. `server-node/src/routes/rpgWorldWorksRoutes.ts`
3. `server-node/src/routes/rpgWorldLibraryRoutes.ts`
4. `server-node/src/routes/rpgWorldGalleryRoutes.ts`
5. `server-node/src/services/RpgAgentOrchestrator.ts`
6. `server-node/src/services/RpgAgentSessionStore.ts`
7. `server-node/src/services/RpgWorldPreviewCompiler.ts`
8. `server-node/src/services/RpgWorldWorkSummaryService.ts`
当前策略:
1. Agent route 与 orchestrator/session store 先用新命名 façade 对齐。
2. works/library/gallery 路由先建立空骨架和基础 path 常量,避免下一轮迁移继续回落到旧命名。
3. `RpgWorldPreviewCompiler` 先桥接旧 `runtimeProfile.ts` 编译能力,为工作包 G 的目录化拆分预留落点。
## 2.3 共享契约骨架
已新增以下共享契约入口:
1. `packages/shared/src/contracts/rpgAgentAnchors.ts`
2. `packages/shared/src/contracts/rpgAgentDraft.ts`
3. `packages/shared/src/contracts/rpgAgentSession.ts`
4. `packages/shared/src/contracts/rpgAgentActions.ts`
5. `packages/shared/src/contracts/rpgCreationPreview.ts`
6. `packages/shared/src/contracts/rpgCreationWorkSummary.ts`
当前策略:
1. 会话、动作、作品摘要先从旧 `customWorldAgent.ts` 做类型级兼容导出。
2. `rpgAgentDraft.ts` 先把 foundation draft、draft card 等草稿相关类型收口成独立入口,给工作包 H 后续物理拆分预留稳定导入点。
3. `packages/shared/src/index.ts` 已补上对 RPG 草稿契约骨架的根导出,避免后续工作包继续回退到旧 `customWorldAgent.ts` 取类型。
4. `rpgCreationPreview.ts` 明确标记当前 preview 仍是 legacy profile 兼容载体,避免误认为 preview contract 已经完成。
## 3. 本次没有做的事
以下内容仍保持原状,留给后续工作包:
1. 没有拆 `PreGameSelectionFlow.tsx` 内部编排。
2. 没有拆 `CustomWorldResultView.tsx``CustomWorldEntityEditorModal.tsx``CustomWorldRoleAssetStudioModal.tsx` 内部 section。
3. 没有把 `runtimeRoutes.ts` 中的 works/library/gallery 真正迁出。
4. 没有改 `customWorldAgentOrchestrator.ts``customWorldAgentSessionStore.ts``runtimeProfile.ts` 的内部职责。
5. 没有改变任何线上行为或接口语义。
## 4. 对后续工作包的直接收益
1. 工作包 B 可以直接把平台壳层 hooks 落到 `src/components/game-shell/rpg-creation-flow/`
2. 工作包 C 可以直接把结果页与编辑器 section 落到新目录,而不用先讨论命名。
3. 工作包 D 可以直接从 `rpgCreation*Client` 开始迁移导入链。
4. 工作包 E、F、G、H 可以基于 `RpgAgent*``RpgWorld*``rpg*` 契约骨架继续拆分,而不需要再回头统一首轮命名。

View File

@@ -0,0 +1,106 @@
# 创作流程链路重构工作包 B 完成记录
更新时间:`2026-04-21`
## 1. 本轮目标
工作包 B 聚焦前端平台壳层与流程编排拆分,本轮目标是把平台壳层从“大编排文件”收口成“页面壳层 + 独立 hooks / coordinator”
1. `PreGameSelectionFlow.tsx` 退化为兼容入口。
2. `RpgCreationShellImpl.tsx` 只保留 stage 切换、组件装配、视觉级 loading / error。
3. 平台 bootstrap、session controller、operation polling、detail navigation、result autosave、enter-world 逻辑全部迁入 `src/components/game-shell/rpg-creation-flow/` 新目录。
4. 保证现有交互测试继续通过,不引入主链行为回退。
---
## 2. 已完成内容
### 2.1 旧入口已退化为兼容层
`src/components/game-shell/PreGameSelectionFlow.tsx` 现在只保留:
1. 旧类型导出兼容:`PreGameSelectionFlowProps``SelectionStage`
2. 旧组件名兼容:`PreGameSelectionFlow`
3. 对新实现 `RpgCreationShellImpl` 的桥接
这样现有调用方和测试仍可继续走旧路径,不会因为命名迁移立即破坏主链。
### 2.2 新目录已承接真实实现与流程 hooks
已新增或更新以下文件:
1. `src/components/game-shell/rpg-creation-flow/RpgCreationShellImpl.tsx`
2. `src/components/game-shell/rpg-creation-flow/RpgCreationShell.tsx`
3. `src/components/game-shell/rpg-creation-flow/index.ts`
4. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowTypes.ts`
5. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowShared.ts`
6. `src/components/game-shell/rpg-creation-flow/useRpgCreationPlatformBootstrap.ts`
7. `src/components/game-shell/rpg-creation-flow/useRpgCreationSessionController.ts`
8. `src/components/game-shell/rpg-creation-flow/useRpgCreationAgentOperationPolling.ts`
9. `src/components/game-shell/rpg-creation-flow/useRpgCreationDetailNavigation.ts`
10. `src/components/game-shell/rpg-creation-flow/useRpgCreationResultAutosave.ts`
11. `src/components/game-shell/rpg-creation-flow/useRpgCreationEnterWorld.ts`
其中:
1. `RpgCreationShell.tsx` 已不再桥接旧 `PreGameSelectionFlow`,而是直接桥接 `RpgCreationShellImpl.tsx`
2. `index.ts` 已开始从新目录导出 `SelectionStage`,为后续调用迁移准备统一出口。
### 2.3 平台编排已全部拆入独立 coordinator
本轮已经把原 `PreGameSelectionFlow` / `RpgCreationShellImpl` 中的主链编排拆到以下 hook
1. `useRpgCreationPlatformBootstrap.ts`
- 平台首页 works / library / gallery / history / save / dashboard 拉取
- 浏览历史写入与存档恢复
2. `useRpgCreationSessionController.ts`
- Agent session 创建 / 恢复
- 消息流、action 执行、草稿生成态与结果页自动打开
3. `useRpgCreationAgentOperationPolling.ts`
- Agent operation 轮询
- 完成态 session 刷新与失败兜底
4. `useRpgCreationDetailNavigation.ts`
- 作品详情、创作作品恢复、草稿结果页打开
- 详情页发布 / 下架 / 删除
5. `useRpgCreationResultAutosave.ts`
- 结果页自动保存
- `sync_result_profile` 协调
- 保存签名去重与延时保存
6. `useRpgCreationEnterWorld.ts`
- 进入世界前的最终草稿同步
当前 `RpgCreationShellImpl.tsx` 只保留:
1. hooks 组合
2. stage 级视图切换
3. 组件 props 装配
4. 视觉级 loading / error 展示
---
## 3. 当前状态判断
工作包 B 已达到执行方案中的验收口径:
1. `PreGameSelectionFlow.tsx` 只剩兼容导出与新壳层桥接。
2. `RpgCreationShellImpl.tsx` 不再直接持有平台请求编排、operation 轮询、自动保存或进入世界同步细节。
3. 平台侧主链已经切成壳层 + hooks / coordinator。
4. 现有 `PreGameSelectionFlow.agent.interaction.test.tsx` 的 14 个场景全部通过。
---
## 4. 本轮刻意未做
1. 还没有物理删除 `PreGameSelectionFlow.tsx`,当前继续保留旧入口兼容层,避免影响并行工作包的调用路径。
2. 还没有让所有调用方统一显式改走 `RpgCreationShell` 新入口,当前仍允许旧入口桥接到新壳层。
3. 还没有把结果页 preview 数据源从前端兼容 adapter 切到服务端正式 preview contract当前仍使用 `rpgCreationPreviewAdapter` 作为阶段性兼容层,这属于后续工作包 G / H 与 Phase 3 范围。
4. 还没有清理所有 legacy 兼容导出与 façade当前优先稳定主链与测试口径。
---
## 5. 验证结果
1. `npx eslint "src/components/game-shell/PreGameSelectionFlow.tsx" "src/components/game-shell/rpg-creation-flow/*.ts" "src/components/game-shell/rpg-creation-flow/*.tsx"`
2. `npx vitest run src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
3. `npm run check:encoding`
以上检查在本轮修改后均已通过。

View File

@@ -0,0 +1,106 @@
# 创作链路重构工作包 D 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 D前端 custom world client 收口**,约束如下:
1. 把创作链主路径依赖的 custom world 请求从 `aiService.ts``storageService.ts` 中迁入 `src/services/rpg-creation/`
2. 首轮允许旧 service 兼容导出,追加清理轮必须删除已无调用方的旧命名导出。
3. 不改后端接口语义,不扩写结果页 UI 逻辑,不借机重构工作包 B / C 的内部状态编排。
## 2. 本次已落地内容
## 2.1 RPG 创作域请求基座已独立
已新增以下请求基座文件:
1. `src/services/rpg-creation/rpgCreationRuntimeClient.ts`
2. `src/services/rpg-creation/rpgCreationRequestHelpers.ts`
当前策略:
1. runtime 读写重试策略不再散落在 `storageService.ts` 内部,而是作为 RPG 创作域专属 runtime client 复用。
2. Agent SSE、POST JSON 请求辅助能力收口到 `rpgCreationRequestHelpers.ts`,避免再把流式解析细节写回通用 service。
## 2.2 五类 rpgCreation client 已持有真实请求实现
以下 client 已不再桥接旧 service而是直接持有真实网络实现
1. `src/services/rpg-creation/rpgCreationAgentClient.ts`
2. `src/services/rpg-creation/rpgCreationWorkClient.ts`
3. `src/services/rpg-creation/rpgCreationLibraryClient.ts`
4. `src/services/rpg-creation/rpgCreationAssetClient.ts`
5. `src/services/rpg-creation/rpgCreationGenerationClient.ts`
本轮已完成的具体收口:
1. Agent session 创建、读取、消息发送、消息流、action 执行、operation 查询、card detail 查询已经正式迁入 `rpgCreationAgentClient.ts`
2. works 列表查询已经正式迁入 `rpgCreationWorkClient.ts`
3. library / publish / unpublish / gallery / gallery detail 已经正式迁入 `rpgCreationLibraryClient.ts`
4. 结果页与编辑器依赖的场景图、场景 NPC、可扮演角色、场景角色、场景生成请求已经正式迁入 `rpgCreationAssetClient.ts`
5. `generateCustomWorldProfile()` 已正式迁入 `rpgCreationGenerationClient.ts`,世界生成入口也已进入 RPG 创作域 client。
6. `src/services/rpg-creation/index.ts` 已收口为 RPG 命名导出,创作主链不再从 barrel 暴露 `createCustomWorldAgentSession / listCustomWorldWorks / upsertCustomWorldProfile` 等旧命名入口。
## 2.3 旧 service 兼容导出已删除
追加清理轮已完成以下删除:
1. `src/services/aiService.ts` 不再 re-export RPG 创作 Agent / works / 结果页生成接口,继续只服务 story/chat 等通用 AI 运行时能力。
2. `src/services/storageService.ts` 已物理删除,运行时存档、设置、资料、浏览历史能力已迁入 `src/services/rpg-entry/``src/services/rpg-runtime/`
3. `rpgCreationAgentClient.ts``rpgCreationWorkClient.ts``rpgCreationLibraryClient.ts``rpgCreationAssetClient.ts` 已删除 `CustomWorld*` 兼容具名导出,只保留 `Rpg*` 主命名。
4. 源码扫描已确认不再存在 `createCustomWorldAgentSession / executeCustomWorldAgentAction / listCustomWorldWorks / upsertCustomWorldProfile` 等旧主链函数引用。
## 2.4 主链调用已开始直接使用 RPG 创作域 client
本轮已把以下主链入口切到 `src/services/rpg-creation/`
1. `src/components/rpg-entry/useRpgCreationSessionController.ts`
2. `src/components/rpg-entry/useRpgCreationResultAutosave.ts`
3. `src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
4. `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx`
5. 新增世界生成入口 `generateRpgWorldProfile()` 通过 `src/services/rpg-creation/` barrel 暴露,后续新代码不必再从旧 `aiService.ts` 进入。
配套收口:
1. 结果页与编辑器相关测试 mock 已改到 `rpgCreationAssetClient`,不再盯住 `aiService.ts` 的兼容层。
2. `CustomWorldResultView.test.tsx``CustomWorldEntityEditorModal.test.tsx` 已改为直接消费 `RpgCreationResultView / RpgCreationEntityEditorModal` 新入口,不再通过旧组件 façade。
## 2.5 本轮验证结果
已完成以下针对性验证:
1. `npm run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts src/services/storageService.test.ts src/components/CustomWorldEntityEditorModal.test.tsx src/components/CustomWorldResultView.test.tsx src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx`
2. `npm run check:encoding`
验证结果:
1. 上述 5 组定向测试全部通过。
2. 编码检查通过,未写坏中文文件。
追加清理轮已完成以下验证:
1. `npm run test -- src/services/rpg-creation/rpgCreationPreviewAdapter.test.ts src/components/CustomWorldResultView.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`
结果:通过,`42` 项测试全部通过。
2. `npm run test -- src/services/rpg-creation/rpgCreationGenerationClient.test.ts`
结果:通过,`2` 项测试全部通过。
3. `npm run check:encoding`
结果:通过,`1929` 个文件编码检查通过。
4. 源码扫描确认 `src / packages / server-node` 中不再存在本轮删除的旧主链函数与旧组件入口符号引用。
## 3. 本次刻意未做的事
以下内容明确留给后续工作包,不在本轮越界处理:
1. 没有改后端 works/library/gallery/agent route 的语义与 contract。
2. 没有拆 `PreGameSelectionFlow.tsx` 内部编排;这部分仍属于工作包 B。
3. 没有继续物理拆散 `RpgCreationEntityEditorShared.tsx`;这部分仍属于工作包 C 后续细拆。
4. 没有强行重命名历史数据结构类型,例如 `CustomWorldProfile` 与 runtime contract response 名称;这些仍是现有契约类型,不等同于旧脚本依赖。
5. 没有删除旧 `src/services/ai.ts` 中的 legacy 世界生成实现;它已不在当前 RPG 创作主链 client 上,后续应按独立 dead code 批次评估。
## 4. 对后续工作包的直接收益
1. 工作包 B 后续拆平台壳层时,可以直接从 `src/services/rpg-creation/` 消费 Agent / works / library / gallery 请求,不必继续回到旧 service 文件找接口。
2. 工作包 C 后续继续拆结果页和编辑器时,资产生成请求已经有稳定的 RPG 创作域入口。
3. 后续清理 `aiService.ts``storageService.ts` 时,创作链主路径已经完成真实迁出,不会再被“通用 service 同时承载创作域请求”拖住。

View File

@@ -0,0 +1,150 @@
# 创作链路重构工作包 E 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 E后端 Agent 编排拆分**,并严格遵守这一轮的写入边界:
1. 只改后端应用服务层,不动前端壳层。
2. 先把 `customWorldAgentOrchestrator.ts` 从“大分支调度 + 派生状态重建 + 结果回写细节”里拆薄。
3. 补齐 action executor 真实落点与 `supportedActions` 主链字段,但不在这一轮顺手重构 session store 和 runtime compiler。
## 2. 本次已落地内容
## 2.1 orchestrator 已退化为应用服务 façade
本轮后,`server-node/src/services/customWorldAgentOrchestrator.ts` 的职责开始收口为:
1. session 级入口方法保留。
2. 创建 operation 记录。
3. 调用 action registry 拿到执行计划。
4. 把消息轮转、foundation 生成、实体生成、角色资产同步等主链事务串起来。
这轮明确移出的内容:
1. `action -> executor` 的分支校验和分发。
2. `sync_result_profile` 的字段回写细节。
3. 多个 action 共用的 draftCards / assetCoverage / suggestedActions / qualityFindings 派生重建逻辑。
## 2.2 已新增 action registry 与 executor 目录,并完成真实执行迁移
已新增:
1. `server-node/src/services/customWorldAgentActionRegistry.ts`
2. `server-node/src/services/customWorldAgentActionExecutors/index.ts`
3. `server-node/src/services/customWorldAgentActionExecutors/types.ts`
本轮收口结果:
1. registry 统一处理 `draft_foundation``update_draft_card``sync_result_profile``generate_characters``generate_landmarks``generate_role_assets``sync_role_assets` 的可用性校验。
2. `publish_world``generate_scene_assets``sync_scene_assets``expand_long_tail``revert_checkpoint` 已完成真实 executor 装配,不再只是 registry 层面的“已声明但未开放”动作。
3. `lock_cards``unlock_cards``regenerate_scope` 仍统一通过 registry 返回禁用原因,不再继续堆在 orchestrator 分支里。
4. `customWorldAgentActionExecutors/` 已补 `draftFoundationExecutor.ts``updateDraftCardExecutor.ts``syncResultProfileExecutor.ts``generateCharactersExecutor.ts``generateLandmarksExecutor.ts``generateRoleAssetsExecutor.ts``syncRoleAssetsExecutor.ts``generateSceneAssetsExecutor.ts``syncSceneAssetsExecutor.ts``expandLongTailExecutor.ts``publishWorldExecutor.ts``revertCheckpointExecutor.ts`,真实 action 执行已从 orchestrator 物理迁入目录。
5. `customWorldAgentActionExecutors/helpers.ts``executorShared.ts` 已收口 action_result / summary message 构造、operation 更新和 session 读取共用逻辑,避免 executor 间重复堆样板代码。
## 2.3 已新增 message turn / suggested action / snapshot / quality gate / result sync service
已新增:
1. `server-node/src/services/customWorldAgentMessageTurnService.ts`
1. `server-node/src/services/customWorldAgentSuggestedActionService.ts`
2. `server-node/src/services/customWorldAgentSnapshotBuilder.ts`
3. `server-node/src/services/customWorldAgentQualityGateService.ts`
4. `server-node/src/services/customWorldAgentResultSyncService.ts`
本轮收口结果:
1. `CustomWorldAgentMessageTurnService` 已接管 session 初始派生状态与 message turn 的真实执行,`customWorldAgentOrchestrator.ts` 只保留 façade 委托。
1. `CustomWorldAgentSuggestedActionService` 统一维护 `foundation_review``object_refining``visual_refining` 的建议动作生成,不再散落在 orchestrator 和 session compatibility。
2. `CustomWorldAgentSnapshotBuilder` 统一承接 message turn、foundation draft、结果页回写、角色/地点追加、角色资产同步后的派生字段重建。
3. `CustomWorldAgentQualityGateService` 已形成独立 finding 入口,当前先输出角色缺失、地点缺失、玩家目标缺失、角色资产待补齐、场景资产待补齐等基础 gate finding。
4. `CustomWorldAgentResultSyncService` 接管了 `sync_result_profile` 的字段回写细节,明确这一轮只允许“摘要 + 资产确认结果 + legacyResultProfile 快照”回写进 draft profile。
## 2.4 `supportedActions` 已接入 session snapshot 主链
这一轮已把 registry 产出的能力矩阵正式装配到 `CustomWorldAgentSessionSnapshot.supportedActions`
1. `createSession``getSessionSnapshot`、stream message 完成态、各 action 完成后的 session 拉取都会返回真实 `supportedActions`
2. `supportedActions` 的启用状态按 session 当前阶段与草稿可用性计算,不再由前端根据 action 字面量自行猜测。
3. 具体 payload 校验仍保留在 action 执行阶段,能力矩阵只表达“当前阶段是否允许发起这类动作”。
## 2.5 action 主链行为保持不变,但派生状态已开始统一
这一轮没有改变现有 action contract也没有新增前端依赖字段但已经把以下重复派生逻辑统一改走 snapshot builder
1. `draft_foundation`
2. `update_draft_card`
3. `sync_result_profile`
4. `generate_characters`
5. `generate_landmarks`
6. `generate_role_assets`
7. `sync_role_assets`
8. message turn 结束后的 stage / suggested actions / quality findings / asset coverage 重建
这意味着:
1. 后续新增 action 时,不必再复制一整段 `draftCards + assetCoverage + suggestedActions + recommendedReplies` patch 拼装代码。
2. `qualityFindings` 已开始成为真实后端派生字段,而不只是 session store 中的空占位。
3. `sync_result_profile` 的边界已经能单独测试和继续收缩。
## 2.6 工作包 E 第三轮已补齐的真实闭环
本轮把工作包 E 前两轮遗留的 5 个动作补成了真实后端闭环:
1. `generate_scene_assets`
已通过 `CustomWorldAgentAssetBridgeService.buildSceneAssetStudioContext()` 打通场景图工坊上下文准备,支持营地与地点单场景进入。
2. `sync_scene_assets`
已通过 `applySceneAssetPublishResult()` 写回营地/地点正式场景图,并同步刷新对应 `sceneChapters[].acts` 的背景图与背景资产 ID。
3. `expand_long_tail`
已接入实体生成服务与 snapshot builder能真实追加长尾角色、地点并把阶段推进到 `long_tail_review`
4. `publish_world`
已改为走 `CustomWorldAgentPublishingService + RpgWorldProfileRepository` 主链,正式把 draft session 编译、写入并发布到作品库。
5. `revert_checkpoint`
已依赖 checkpoint snapshot 元数据与 `restoreCheckpoint()` 主链完成真实回滚,不再只是开放 action 名称。
这一轮同时补齐了 4 个关键收口:
1. 发布链已经统一改走 `CustomWorldAgentPublishingService``customWorldAgentOrchestrator.ts``customWorldAgentActionExecutors/index.ts``publishWorldExecutor.ts``server.ts` 的注入口径已经对齐;作者展示名优先走 `resolveAuthorDisplayName`,同时保留 `userRepository` 兼容兜底。
2. `publish_world` 的 readiness 与正式发布已经收口到同一个服务,`profileId` 固定优先沿用 legacy 结果页 ID否则回退为 `agent-draft-${sessionId}`,避免发布产物继续使用临时时间戳。
3. `buildCheckpointSnapshot()` 已接入 `draft_foundation``update_draft_card``sync_result_profile``generate_characters``generate_landmarks``sync_role_assets``sync_scene_assets``expand_long_tail``publish_world` 等关键 executorcheckpoint 现在保存的是真正可恢复的派生快照,而不是只记一段残缺 patch。
4. `rebuildRoleAssetCoverage()` 已补营地 / 地点正式场景资产 fallback 汇总,并收口为“只有真实正式场景图已存在时才补 standalone summary”这样 `sync_scene_assets` 写回后的 camp/landmark asset coverage 在 snapshot 重建、works 读模型与 checkpoint 回放里都不会丢失,也不会误伤 phase3 自动资产回归。
## 2.7 本轮验证结果
已完成以下验证:
1. `npm --prefix server-node run build`
2. `npm --prefix server-node run test -- customWorldAgentPhase3.test.ts customWorldAgentActionRegistry.test.ts customWorldAgentPhase5.test.ts`
本轮重点关注的回归范围:
1. `customWorldAgentActionRegistry.test.ts`
2. `customWorldAgentPhase3.test.ts`
3. `customWorldAgentPhase5.test.ts`
4. `publish_world`
5. `generate_scene_assets / sync_scene_assets`
6. `expand_long_tail`
7. `revert_checkpoint`
验证结果:
1. `server-node` 构建通过。
2. 定向回归通过,共 `208` 项测试全部通过。
3. Phase 3 与 Phase 5 已同时确认通过,说明这轮对 `sceneAssets` fallback summary 的收口没有打坏前序自动资产链。
## 3. 本次刻意未做的事
以下内容明确留给后续工作包或下一轮工作包 E不在本轮越界处理
1. 还没有进入 Phase 4 的“进入世界统一走发布态 gate”收口当前这轮只完成了发布动作本身的后端闭环。
2. 还没有改 `customWorldAgentSessionStore.ts` 内部 compatibility / snapshot 输出结构,这部分仍属于工作包 F。
3. 还没有把 result preview 正式接到 `resultPreview` 主链字段,这部分仍需要和工作包 G / H 协作。
4.`customWorldAgentPublishGateService.ts``customWorldAgentPublishService.ts` 仍作为历史兼容文件保留,但工作包 E 主链已经不再走它们;这一轮没有继续做物理删除与引用清扫,避免越界碰到 Phase 4/Phase 5 之外的兼容入口。
## 4. 对后续工作包的直接收益
1. 工作包 F 可以在不碰 orchestrator 大分支的前提下,继续拆 session/store/repository。
2. 工作包 G 可以直接围绕 `CustomWorldAgentResultSyncService``CustomWorldAgentQualityGateService` 对接服务端 preview compiler 与 publish gate。
3. 工作包 H 可以基于已落地的 `supportedActions`、action registry 和 quality gate 继续推进 preview contract 与 contract tests。
4. 后续继续拆 action executor 时,已经有 `customWorldAgentActionExecutors/` 目录和注册表,不需要再回到 orchestrator 里重新铺路。

View File

@@ -0,0 +1,92 @@
# 创作链路重构工作包 F 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 F后端 session/store/repository 拆分**,约束如下:
1. 不改动现有主链接口与行为语义。
2. 保留 `customWorldAgentSessionStore.ts``runtimeRepository.ts``customWorldWorkSummaryService.ts` 作为兼容 façade。
3. 把 session 兼容补齐、session 持久化、profile 持久化、works 读模型组装从大文件中物理拆出。
## 2. 本次已落地内容
## 2.1 session store 内部分层
已新增以下 RPG Agent session 拆分文件:
1. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionRecord.ts`
2. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionCompatibility.ts`
3. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionFactory.ts`
4. `server-node/src/services/rpg-agent-session-store/rpgAgentSessionRepositoryAdapter.ts`
当前策略:
1. `customWorldAgentSessionStore.ts` 继续保留旧类名和旧方法签名。
2. sessionId 前缀、snapshot 输出结构、operation/checkpoint 写入语义保持兼容。
3. 旧 session 的兼容补齐逻辑集中收口到 `rpgAgentSessionCompatibility.ts`,不再继续堆在 store 主文件里。
4. `customWorldAgentSessionStore.ts` 已正式改为依赖 `RpgAgentSessionRepositoryPort`phase2~5 与 works 集成测试也已切到新的 session 仓储端口。
## 2.2 custom world 仓储从 runtime 大仓储中拆出
已新增以下 RPG 世界仓储文件:
1. `server-node/src/repositories/RpgAgentSessionRepository.ts`
2. `server-node/src/repositories/RpgWorldProfileRepository.ts`
3. `server-node/src/repositories/rpgWorldRepositoryShared.ts`
当前策略:
1. `RuntimeRepositoryPort` 继续保留兼容 façade`context.ts``server.ts``runtimeRoutes.ts`、同步脚本已开始直接注入并使用 `RpgAgentSessionRepository``RpgWorldProfileRepository`
2. `runtimeRepository.ts` 内的 custom world session/profile/gallery SQL 已改成委托新仓储。
3. `runtimeRepository.ts` 继续只保留 runtime 快照、设置、浏览历史、档案等通用能力,以及少量尚未迁走的快照同步编排。
## 2.3 works 读模型拆分
已新增以下 works 读模型相关文件:
1. `server-node/src/services/RpgWorldWorkCoverResolver.ts`
2. `server-node/src/services/RpgWorldWorkSummaryAssembler.ts`
3. `server-node/src/services/RpgWorldWorkSummaryService.ts`
并将:
1. `server-node/src/services/customWorldWorkSummaryService.ts`
退化为兼容入口,仅负责桥接新 `RpgWorldWorkSummaryService`
当前策略:
1. works service 只保留服务入口,不再内嵌标题、摘要、封面、资产覆盖率等全部组装细节。
2. 草稿封面与发布态封面解析统一走 resolver避免后续重复理解封面规则。
3. 草稿态与发布态 work summary 的字段语义保持不变,继续支持“继续创作”和“进入世界”入口判定。
4. `runtimeRoutes.ts` 中的 works/library/gallery 路由已切到 `rpgWorldWorkSummaryService``rpgWorldProfileRepository` 直接注入,不再经由 `runtimeRepository` 中转 custom world 读模型。
## 3. 验证结果
本次已完成以下定向回归:
1. 运行 `node --test --test-concurrency=1 --import tsx src/services/customWorldAgentPhase2.test.ts src/services/customWorldAgentPhase3.test.ts src/services/customWorldAgentPhase4.test.ts src/services/customWorldAgentPhase5.test.ts src/services/customWorldWorkSummaryService.integration.test.ts`
2. 以上 21 个 custom world / agent / works 相关测试全部通过。
同时确认:
1. 全量 `npx tsc -p server-node/tsconfig.json --noEmit` 当前仍被仓库里既有的跨模块类型问题阻塞。
2. 这些全量类型错误大多与本工作包无关,因此本轮仍以 custom world 定向测试通过作为主验证口径。
3. 工作包 F 本轮新增的 `RpgWorldWorkSummaryService.ts`、新仓储注入链和测试 helper未在定向回归中引入新的行为回归。
## 4. 当前兼容保留项
以下内容属于阶段性兼容保留,不再视为工作包 F 未完成项:
1. `RuntimeRepositoryPort` 仍保留 custom world 相关兼容方法,避免一次性冲击 story/runtime 其他调用方。
2. `customWorldAgentSessionStore.ts``customWorldWorkSummaryService.ts` 仍保留旧文件名 façade后续统一命名治理时再清理。
3. runtime 快照同步与 custom world profile 自动回写的进一步解耦,仍留待后续围绕 `runtimeRepository.ts` 继续收口。
## 5. 对后续工作包的直接收益
1. 工作包 E 可以在不继续挤压 `customWorldAgentSessionStore.ts` 的情况下,把 orchestrator 的 result sync / snapshot builder 接到更清晰的 session 持久化边界。
2. 工作包 G 后续若需要让 preview compiler / publish gate 落库,不必再继续往 `runtimeRepository.ts` 堆 custom world SQL。
3. 工作包 H 已能直接围绕 `rpg-agent-session-store/``RpgWorldWorkSummaryAssembler.ts``RpgWorldWorkSummaryService.ts` 与新仓储端口补充更细粒度回归,而不必穿透大文件。
4. 后续若继续拆 route 命名或清理旧 façade已有 `context -> server -> runtimeRoutes -> script -> tests` 的新仓储注入链可直接复用。

View File

@@ -0,0 +1,92 @@
# 创作链路重构工作包 G 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 G后端 preview compiler 与 runtime profile 目录化**,并把目录化拆分推进到文档目标结构:
1. 先把 `runtimeProfile.ts` 退化成兼容 façade。
2.`runtime-profile/` 真正拆成 `normalize/build/schema/creatorIntentBridge` 等独立模块。
3. 把服务端 result preview compiler 从 foundation draft 流程中抽出独立入口。
4. 不直接改路由层,不直接接前端结果页。
## 2. 本次已落地内容
## 2.1 runtime profile 已完成目录化完整拆分
已完成以下结构调整:
1. 新增 `server-node/src/modules/custom-world/runtime-profile/index.ts` 作为目录入口。
2.`server-node/src/modules/custom-world/runtimeProfile.ts` 已退化为兼容 façade只负责 re-export。
3. `server-node/src/modules/custom-world/runtime-profile/runtimeProfileCompiler.ts` 已退化为兼容 façade不再承载主实现。
4. 已新增并落地以下目标模块:
1. `normalizeShared.ts`
2. `normalizeRole.ts`
3. `normalizeLandmark.ts`
4. `normalizeSceneChapter.ts`
5. `normalizeCamp.ts`
6. `buildCompiledProfile.ts`
7. `buildAttributeSchema.ts`
8. `creatorIntentBridge.ts`
当前策略:
1. 先保证旧导入路径不失效,避免放大工作包 G 首轮改动范围。
2. 新代码优先改走 `runtime-profile/` 目录入口。
3. `runtimeProfile.ts``runtimeProfileCompiler.ts` 后续只允许继续收缩,不再接受新增主逻辑。
## 2.2 服务端 preview compiler 已从 foundation draft 流程中抽出
已完成以下收口:
1. `server-node/src/services/RpgWorldPreviewCompiler.ts` 不再只是别名导出,已提供:
1. `buildRpgWorldPreviewProfile()`
2. `normalizeRpgWorldPreviewProfile()`
3. `buildRpgWorldPreviewEnvelope()`
4. `normalizeRpgWorldPreviewEnvelope()`
2. `packages/shared/src/contracts/rpgCreationPreview.ts` 已补 `RpgCreationPreviewSource`,把 preview 来源语义显式化。
3. `customWorldAgentFoundationDraftService.ts` 已把 LLM foundation draft 主生成链改成“直接组装 foundation draft + 单独保留 `legacyResultProfile` 兼容快照”,不再通过 preview compiler 反解草稿主字段。
这轮的边界变化是:
1. foundation draft 主字段已经不再依赖“先编 legacy runtime profile再转回 draft”的双重编译。
2. `legacyResultProfile` 仍保留,但只作为结果页兼容快照,不再主导 foundation draft 生成。
3. “服务端 preview 编译入口”继续独立存在,并在 Phase 5 后补上 `rpgCreationPreviewProfileBuilder.ts`,统一承接 preview 与 publish 的兼容合并规则。
4. preview source 已在 Phase 5 后正式收口为 `session_preview`,不再继续沿用兼容期的 `legacy_custom_world_profile` 标记。
## 2.3 已补最小测试与目录化回归验证
本次新增:
1. `server-node/src/services/RpgWorldPreviewCompiler.test.ts`
2. `server-node/src/services/customWorldAgentFoundationDraftService.test.ts`
当前覆盖重点:
1. 验证 preview compiler 可以输出服务端兼容预览 envelope。
2. 验证 envelope 的 `source` 保持为 `session_preview`
3. 验证 preview profile 仍保留 runtime 编译生成的关键字段,例如 `scenarioPackId``campaignPackId`
4. 验证 Phase 5 新增的 preview builder 可以在服务端保留 `legacyResultProfile` 富字段并合并最新草稿资产。
5. 验证 foundation draft service 的 LLM 路径已经直接生成 draft 主字段,不再依赖 preview compiler 反解。
6. 验证 `runtimeProfile.ts` façade 在目录化拆分后仍保持旧调用兼容。
本轮额外验证已通过:
1. `npm run check:encoding`
2. `node --test --test-concurrency=1 --import tsx server-node/src/services/customWorldAgentFoundationDraftService.test.ts server-node/src/modules/custom-world/runtimeProfile.test.ts server-node/src/services/RpgWorldPreviewCompiler.test.ts`
## 3. 本次刻意没有做的事
以下内容仍留给后续阶段:
1. 还没有让 `RpgWorldPreviewCompiler` 输出真正独立于 legacy profile 的 preview view model。
2. 还没有把 `RpgWorldPreviewCompiler` 的 preview 载体从当前 runtime-profile 兼容对象升级成真正独立的 preview view model。
3. `legacyResultProfile` 仍保留为兼容快照,结果页与自动保存链还没有完全脱离 legacy profile 富字段。
4. 还没有删除 `runtimeProfile.ts``runtimeProfileCompiler.ts` 这两个兼容 façade。
## 4. 对后续工作包的直接收益
1. 工作包 E 可以围绕 `RpgWorldPreviewCompiler` 继续补 result sync / snapshot builder 的 preview 接口。
2. 工作包 H 可以基于 `RpgCreationPreviewEnvelope` 继续细化正式 preview contract 和 contract tests。
3. Phase 3 把结果页切到服务端 preview 时,已经有稳定的后端编译入口和目录化 normalize/build 模块,不需要再回头拆 `runtimeProfile.ts` 大文件。

View File

@@ -0,0 +1,137 @@
# 创作链路重构工作包 H 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 H共享契约与测试基建**,约束如下:
1. 把 RPG 创作域共享契约从“类型别名骨架”推进到“真实定义 + 兼容出口”。
2. 补齐可复用的 fixture避免前后端测试继续各自复制一套假数据。
3. 补齐 unit / contract / integration / regression 最小闭环,不越界重构 UI、路由和仓储主逻辑。
## 2. 本次已落地内容
## 2.1 共享契约已完成物理拆分与兼容收口
本轮已把以下文件从工作包 A 的骨架态推进为真实定义:
1. `packages/shared/src/contracts/rpgAgentAnchors.ts`
2. `packages/shared/src/contracts/rpgAgentDraft.ts`
3. `packages/shared/src/contracts/rpgAgentActions.ts`
4. `packages/shared/src/contracts/rpgAgentSession.ts`
5. `packages/shared/src/contracts/rpgCreationPreview.ts`
6. `packages/shared/src/contracts/rpgCreationWorkSummary.ts`
本轮收口重点:
1. `rpgAgent*``rpgCreation*` 文件不再只是从旧 `customWorldAgent.ts` 做类型别名转发,而是承载真实契约定义。
2. `rpgAgentSession.ts` 已显式加入 `supportedActions?``resultPreview?` 可选字段,为工作包 E/G 后续正式接入 registry 与服务端 preview compiler 预留稳定契约入口。
3. `rpgCreationPreview.ts` 已补 `source / generatedAt / qualityFindings / blockers`,把“预览载体”和“预览来源/质量门槛”拆开。
4. `rpgCreationWorkSummary.ts` 已收口 works 列表稳定字段,明确 `canResume / canEnterWorld` 的读模型语义。
## 2.2 旧 `customWorld*` 契约已补齐兼容分文件
本轮没有直接删除旧入口,而是把旧命名收口成“聚合出口 + 分文件兼容层”:
1. 当前旧 `customWorldAgent.ts` 不再承载主定义,而是统一聚合:
- `customWorldAgentAnchors.ts`
- `customWorldAgentDraft.ts`
- `customWorldAgentActions.ts`
- `customWorldAgentSession.ts`
- `customWorldResultPreview.ts`
- `customWorldWorkSummary.ts`
2. 现有前后端直接导入 `customWorldAgent.ts` 的代码不需要在本轮一起大改,避免把工作包 H 扩成全仓导入迁移。
3. 后续工作包可以逐步把新代码改到 `rpgAgent* / rpgCreation*` 路径;如果暂时仍需旧命名,也可以先切到更细的兼容分文件,而不是继续依赖单一大聚合文件。
## 2.3 已补共享 fixture总线样本开始统一
本轮新增:
1. `packages/shared/src/contracts/rpgCreationFixtures.ts`
当前已提供并复用的样本包括:
1. 八锚点 fixture
2. foundation draft fixture
3. session snapshot fixture
4. preview envelope fixture
5. published profile fixture
6. library entry fixture
7. works response fixture
这些样本的作用是:
1. 前端 contract test、后端 integration test、后续 preview/compiler 回归可以共用同一批样本。
2. 避免继续在各测试文件里手写不一致的 session/profile/works 假数据。
3. 把工作包 H 文档中要求的“最小 eight-anchor / preview / published profile / works 样本”先落成统一入口。
## 2.4 已补 unit / contract / integration / regression 最小闭环
本轮新增测试:
1. `packages/shared/src/contracts/rpgContracts.test.ts`
2. `server-node/src/services/customWorldWorkSummaryService.integration.test.ts`
3. `server-node/src/services/RpgWorldPreviewCompiler.fixture.test.ts`
4. `server-node/src/services/RpgWorldWorkSummaryAssembler.fixture.test.ts`
5. `server-node/src/services/customWorldAgentActionRegistry.test.ts`
6. `server-node/src/services/customWorldAgentResultSyncService.test.ts`
同时补充:
1. `vitest.config.ts` 已把 `packages/shared/src/**/*.test.ts` 纳入前端 Vitest 测试入口。
2. shared contract test 当前覆盖:
- session fixture、preview fixture、published profile fixture、works/library fixture 对齐关系
- `supportedActions` 能力矩阵样本
- 旧命名兼容分文件的类型消费
- 角色动作资产、分幕背景、works 门槛字段不会在 fixture 演进时悄悄回退
3. server unit / regression test 当前覆盖:
- preview compiler 可以直接消费 shared fixture
- works assembler 输出与 shared works fixture 保持一致
- 角色主图、动作集、分幕背景资产字段在 normalize / assemble 后仍能保留
- action registry 的 capability enable/disable 与 payload validate/normalize
- result sync service 只回写摘要与匹配资产,不让 runtime-only 结构反向污染 foundation draft
4. server integration test 当前验证共享 fixture 可以被 `customWorldWorkSummaryService` 正常消费,并输出和共享 works 响应样本一致的草稿/发布条目。
## 2.5 根导出已补齐
本轮已把:
1. `packages/shared/src/contracts/rpgCreationFixtures.ts`
2. `packages/shared/src/contracts/customWorldAgent.ts`
接入:
1. `packages/shared/src/index.ts`
这样后续前端和后端若要消费共享 fixture 或新契约,不需要再回退到旧单文件入口。
## 3. 本次验证结果
已完成以下定向验证:
1. `npm run test -- packages/shared/src/contracts/rpgContracts.test.ts`
2. `node --test --test-concurrency=1 --import tsx src/services/customWorldAgentActionRegistry.test.ts src/services/customWorldAgentResultSyncService.test.ts src/services/customWorldWorkSummaryService.integration.test.ts src/services/RpgWorldPreviewCompiler.fixture.test.ts src/services/RpgWorldWorkSummaryAssembler.fixture.test.ts`
3. `npm run check:encoding`
验证重点:
1. shared 契约样本可直接通过 Vitest 执行。
2. preview compiler、works assembler、works service 三层都可以直接消费 shared fixture不需要额外复制一套测试数据。
3. 中文文档与代码文件经过编码检查,没有把文本写坏。
## 4. 本次刻意未做的事
以下内容明确留给后续工作包或下一轮继续推进:
1. 还没有把仓库里所有 `customWorldAgent.ts` 旧导入物理迁成 `rpgAgent* / rpgCreation*` 新导入。
2. 还没有让后端 session snapshot 真正填充 `supportedActions`
3. 还没有让服务端 preview compiler 真正把 `resultPreview` 写入主链 snapshot。
4. 没有改 UI、路由、数据库仓储或 orchestrator 主逻辑,严格控制在 shared contracts 与测试基建写入边界内。
## 5. 对后续工作包的直接收益
1. 工作包 E 可以直接复用 `supportedActions` 契约入口,把 action registry 的真实能力矩阵接进 session snapshot。
2. 工作包 G 可以直接复用 `resultPreview``RpgCreationPreviewEnvelope`,继续把服务端 preview compiler 接回主链。
3. 后续前后端测试都可以从 shared fixture 取样本,不需要继续维护多套彼此漂移的 session/profile 假数据。
4. 旧命名导入可以先切到兼容分文件,再逐步替换到 `rpg*` 新契约,迁移路径更平滑。

View File

@@ -0,0 +1,36 @@
# 创作页移动端 UI 修复记录
日期:`2026-04-21`
## 问题定位
本轮修复只处理创作页表现层,不新增创作流程。
当前移动端问题主要来自三处:
1. 平台页在 `platformTab === 'create'` 时直接渲染 `CustomWorldCreationHub`,绕过了 `PlatformHomeView` 的移动端外壳,导致底部 Tab 栏没有挂载。
2. 创作中心内部仍混用 `pixel-*` 九宫格样式、`bg-black/*``text-white``border-white/*` 等暗色 Tailwind 类,亮色主题下会出现深色块和低对比文字。
3. 创作中心根节点自带 `h-full overflow-y-auto`,放回平台页后容易与平台页主滚动区抢滚动权,手机上会显得布局混乱。
## 落地约束
1. 创作页仍复用现有平台首页,不新增页面和新系统。
2. 移动端底部 Tab 必须始终由 `PlatformHomeView` 统一渲染,创作页只作为 `create` Tab 的内容。
3. 创作中心内部不再使用深色硬编码作为默认底色,普通卡片、筛选 Tab、空状态和按钮统一使用 `platform-*` token。
4. 创作中心不再自建整页滚动,只把内容交给平台页主滚动区,避免嵌套滚动。
5. UI 中不增加规则说明类文案,只保留必要入口、状态和作品信息。
## 编码方案
1.`PlatformHomeView` 增加可选的 `createTabContent`,让当前 Agent 创作中心接回平台页统一外壳。
2. `PreGameSelectionFlow` 不再在 `platformTab === 'create'` 时绕过 `PlatformHomeView`,而是把 `CustomWorldCreationHub` 作为创作 Tab 内容传入。
3. `CustomWorldCreationHub` 改为无内部整页滚动的内容容器,标题、返回、计数、错误、加载骨架都使用平台 token。
4. `CustomWorldCreationStartCard``CustomWorldWorkCard` 从像素暗色面板切换为平台卡片样式,保留游戏化主视觉但跟随亮暗主题。
5. `CustomWorldWorkTabs` 改用 `platform-tab`,并保持横向滚动与清晰选中态。
## 验收要点
1. 手机宽度下进入“创作”后,底部“首页 / 创作 / 存档 / 我的”Tab 始终可见。
2. 亮色主题下创作页默认卡片不出现大面积黑色底板。
3. 创作页只有平台页主内容区滚动,底部 Tab 不随作品列表滚走。
4. 桌面端仍可通过左侧平台导航进入创作页。

View File

@@ -0,0 +1,115 @@
# Agent 创作流四阶段收口检查与旧链清理边界
更新时间:`2026-04-21`
补充修正:`2026-04-21` 本文档的“草稿恢复优先回 Agent 工作区”和“Agent 来源结果页冻结为预览收口层”属于阶段性收口口径,已被 [AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md](./AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md) 覆盖。当前主口径是Agent 对话框只收集八锚点,已有底稿的草稿从创作中心进入结果页继续完善。
## 1. 结论先行
当前这条 Agent 创作流已经完成阶段一到阶段三的主要收口。
阶段四中的“文档清理”已经开始做,但还没有形成独立、完整的新主链审计闭环。
因此这轮可以执行的清理现在有两类:
1. 删除已经不再从当前主入口可达的旧 `custom-world/sessions` 世界生成链
2. 删除已经完全脱离 `CustomWorldAgentWorkspace` 主链、只剩孤立互相引用与自测覆盖的 `custom-world-agent` 旧面板
3. 保留仍在服务 `Agent session` 主链或已保存作品兼容编辑体验的底层能力
这轮不做:
1. 不删 `Agent session` 的底层持久化能力
2. 不删已保存作品结果页的 legacy 编辑器兼容能力
3. 不删 `custom-world/works` 聚合入口
---
## 2. 阶段完成度
### 2.1 阶段一
已完成。
证据:
1. 结果页新增了 `sync_result_profile`
2. 结果页编辑后的快照可以回写到 `Agent session`
3. 自动保存、返回创作、进入世界都优先走 session 主链
### 2.2 阶段二
已完成。
证据:
1. 平台创作入口已切到 `custom-world/works`
2. 草稿恢复优先回 Agent 工作区
3. Agent 结果页不再继续新增旧编辑入口
### 2.3 阶段三
已完成。
证据:
1. 创作中心不再把 library draft 当主草稿入口
2. Agent 来源结果页冻结为预览收口层
3. 重复同步动作已收敛为有差异才执行
### 2.4 阶段四
未完全完成。
原因:
1. 文档清理已经开始,但还没有完整收束到单一结论文档
2.`custom-world/sessions` 生成链已经完成物理清理,但与之相关的审计/PRD/知识图谱文档仍需继续统一口径
3. `custom-world-agent` 孤岛面板已经完成第二轮物理清理,但阶段四文档总收口仍未完全覆盖所有历史 PRD 口径
---
## 3. 本轮允许删除的旧链
允许删除:
1. `src/services/aiService.ts` 里的旧 `custom-world/sessions` 请求函数
2. `server-node/src/routes/runtimeRoutes.ts` 里的旧 `custom-world/sessions` 路由
3. `server-node/src/services/customWorldGenerationService.ts`
4. 与这条旧链对应的测试
5. `server-node/src/services/customWorldSessionStore.ts`
6. `src/components/custom-world-agent/CustomWorldAgentLockBar.tsx`
7. `src/components/custom-world-agent/CustomWorldAgentQuickActions.tsx`
8. `src/components/custom-world-agent/CustomWorldAgentSummaryPanel.tsx`
9. `src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.tsx`
10. `src/components/custom-world-agent/CustomWorldAgentClarificationPanel.tsx`
11. `src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx`
12. `src/components/custom-world-agent/CustomWorldDraftCardDetailModal.tsx`
13. `src/components/custom-world-agent/CustomWorldDraftEditPanel.tsx`
14. `src/components/custom-world-agent/CustomWorldGenerateEntityModal.tsx`
15. 仅为上述孤岛面板存在的对应测试文件
不允许删除:
1. `server-node/src/repositories/runtimeRepository.ts` 中被 Agent session 复用的 session 持久化能力
2. `src/services/aiService.ts` 里仍在使用的 `generateCustomWorldProfile` 及其现代封装
3. 已保存作品结果页仍在使用的 legacy 编辑器兼容能力
4. `src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx` 及其仍在主链上的 5 个子模块:
- `CustomWorldAgentHeader`
- `EightAnchorProgressBar`
- `CustomWorldAgentOperationBanner`
- `CustomWorldAgentThread`
- `CustomWorldAgentComposer`
---
## 4. 删除完成后的判断标准
如果旧链清理成功,应满足:
1. `src/services/aiService.ts` 不再暴露旧 `custom-world/sessions` 请求函数
2. `server-node/src/routes/runtimeRoutes.ts` 不再挂旧 session 路由
3. `server-node/src/services/customWorldSessionStore.ts``server-node/src/services/customWorldGenerationService.ts` 已物理删除
4. 仓库里不再有主流程可达的旧世界生成入口
5. `CustomWorldAgentWorkspace.tsx` 只保留当前正式主链需要的 5 个子模块
6. 与旧 Agent 草稿面板相关的孤岛 UI 与自测不再继续占据正式目录注意力
7. Agent 主链与已保存作品编辑链仍然可用

View File

@@ -26,10 +26,6 @@
- `GET /api/assets/character-animation/jobs/:taskId`
- `POST /api/assets/character-animation/import-video`
- `GET /api/assets/character-animation/templates`
- `POST /api/assets/qwen-sprite/master`
- `POST /api/assets/qwen-sprite/sheet`
- `POST /api/assets/qwen-sprite/frame-repair`
- `POST /api/assets/qwen-sprite/save`
---
@@ -50,7 +46,6 @@
- 状态行为覆盖保存
- 角色主形象生成、发布与任务查询
- 角色动作生成、导入、发布、模板读取与任务查询
- Qwen 精灵主图、精灵表、修帧与资产保存
---

View File

@@ -0,0 +1,85 @@
# 主流程外编辑器入口清理说明2026-04-21
日期:`2026-04-21`
## 1. 文档目标
记录本轮对“挂在主流程路由外的旧编辑器入口”和“仍把这些入口当现役能力的残留说明”做的收口,避免后续开发再次把历史入口误判成正式能力。
---
## 2. 本轮清理结论
本轮确认后,当前前端正式入口只保留游戏主流程:
- `src/routing/appRoutes.tsx` 仅保留 `game`
本轮删除或收口的对象:
- 独立前端工具路由 `qwen-sprite-tool`
- 仅服务该独立入口的前端页面 `src/tools/QwenSpriteSheetTool.tsx`
- 仅服务该独立入口的工具模型与持久化封装
- 仅服务该独立入口的后端路由 `server-node/src/modules/assets/qwenSpriteRoutes.ts`
- 路由测试里把旧编辑器 / 独立工具入口当作现役分支的断言
- README、经验文档、类型检查配置中已经失效的旧编辑器文件引用
---
## 3. 为什么可以删除
本轮删除对象满足下面几个条件:
1. 不在当前玩家主流程中可达
2. 没有继续嵌入正式创作主链
3. 当前仓库已有主流程内嵌的替代能力
4. 保留它们只会继续制造“看起来还能进、实际上已经不走”的假入口
其中需要特别区分的是:
- `src/editor/shared/editorApiClient.ts`
- `server-node/src/modules/editor/editorRoutes.ts`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `src/components/CustomWorldNpcVisualEditor.tsx`
- `src/components/CustomWorldRoleAssetStudioModal.tsx`
这些仍然服务当前主流程内嵌编辑能力,因此本轮不删除。
---
## 4. 当前保留的编辑能力边界
当前保留的是“嵌入主流程的编辑能力”,不是“独立编辑器站点”:
- 自定义世界实体编辑
- 自定义世界角色形象编辑
- 主流程内的角色资产工坊模态
- 与这些能力配套的 `/api/editor/*``/api/assets/character-*` 接口
后续如果还要新增编辑能力,应优先:
1. 先确认是否真的需要独立入口
2. 默认优先接回主流程模态或正式创作链
3. 如果只是内部工具,不要长期挂在正式前端路由里
---
## 5. 本轮同步更新
本轮已同步更新:
- `README.md`
- `docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md`
- `src/routing/appRoutes.tsx`
- `src/routing/appRoutes.test.ts`
- `server-node/src/app.ts`
- `tsconfig.typecheck-guardrails.json`
---
## 6. 后续建议
后续继续清理时,优先沿着这条规则推进:
1. 先识别是否还在主流程可达
2. 再判断是否仍有正式嵌入点
3. 若只剩文档、测试、兼容判断或独立路由壳,直接成批收口

View File

@@ -0,0 +1,188 @@
# 前端逻辑后移实施方案2026-04-21
更新时间:`2026-04-21`
## 1. 目标
本方案只回答一件事:
**怎样把当前仍残留在前端的正式运行时逻辑、正式会话真相与正式生成编排,继续收回到 Express 后端。**
这份文档不是泛泛而谈的方向说明,而是直接面向本轮与后续几轮编码落地的实施基线。
---
## 2. 本轮确定的硬边界
根据仓库约束与当前审计结果,本轮继续冻结以下边界:
1. 前端只负责表现、输入采集、临时 UI 状态与服务端结果渲染。
2. 后端负责正式鉴权、正式会话、正式运行时快照、正式任务生成、正式运行时物品意图生成、正式自定义世界生成。
3. `codex/backend-rewrite-spacetimedb` 目标分支的鉴权仍以服务端签发 JWT、前端 Bearer token 携带为准;本轮合入不采用 `codex/dev` 的 access cookie 会话方案。
4. 浏览器内不再把浏览历史作为本地正式真相,不再保留正式 quest / runtime item / custom world 生成编排。
5. 运行时主链必须继续向“前端提交意图,后端解释快照并返回展示模型”收敛。
---
## 3. 现状拆分
当前残留问题已经收敛为三批:
### 3.1 第一批:正式真相仍在前端
1. `src/services/apiClient.ts`
- 浏览器当前仍保存 access token并在请求层拼接 `Authorization: Bearer ...`
- 该链路在 `codex/backend-rewrite-spacetimedb` 仍是既定正式实现,不再按 cookie access session 改写
2. `src/services/authService.ts`
- 登录、微信绑定、回调消费流程都要与 JWT/Bearer 方案保持一致,避免混入 access cookie 分支语义
3. `src/components/game-shell/PreGameSelectionFlow.tsx`
- 浏览历史仍是本地写入 + 后端回填的双真相
4. `src/services/platformBrowseHistory.ts`
- 维护浏览历史本地存储、迁移标记与同步状态
### 3.2 第二批:运行时主链仍依赖前端预写快照
1. `src/hooks/story/runtimeStoryCoordinator.ts`
- 在请求 runtime state / runtime action 前,仍先 `PUT /runtime/save/snapshot`
2. `src/hooks/story/npcEncounterActions.ts`
- 待接委托的“更换任务”“放弃任务”仍由前端正式结算
### 3.3 第三批:正式生成编排仍残留在浏览器
1. `src/services/questDirector.ts`
2. `src/services/runtimeItemAiDirector.ts`
3. `src/services/aiService.ts` 的 custom world profile 生成入口
4. `src/services/ai.ts` 中仍保留的浏览器侧 legacy AI orchestration
---
## 4. 分批实施策略
## 4.1 第一批:先收正式真相
### 鉴权
目标状态:
1. 后端继续通过 JWT 承载 access token并只从 `Authorization: Bearer ...` 读取当前访问身份。
2. 前端请求层继续负责保存、刷新和携带 access token公开请求与静默探测不得误清正式 token。
3. access cookie 会话方案不进入 `codex/backend-rewrite-spacetimedb`,避免和目标分支已有 JWT 方案并存。
本批涉及:
1. `server-node/src/routes/authRoutes.ts`
2. `server-node/src/middleware/auth.ts`
3. `src/services/apiClient.ts`
4. `src/services/authService.ts`
5. `src/components/auth/AuthGate.tsx`
### 浏览历史
目标状态:
1. 浏览历史唯一真相在 `runtimeRepository`
2. 前端不再保留本地浏览历史、迁移标记、同步标记。
3. 浏览历史只通过 `storageService` 读取和写入。
本批涉及:
1. `src/components/game-shell/PreGameSelectionFlow.tsx`
2. `src/components/game-shell/PlatformHomeView.tsx`
3. `src/services/storageService.ts`
4. `src/services/platformBrowseHistory.ts`
## 4.2 第二批:把 runtime story 快照解释权收回后端
目标状态:
1. 前端不再通过单独的 `PUT /runtime/save/snapshot` 预写快照再触发动作。
2. runtime state / runtime action 允许前端提交当前快照上下文,由后端内部决定是否写入、如何解释、何时持久化。
3. NPC 待接委托的 replace / abandon / accept 全部走后端 runtime action。
建议实施方式:
1. 扩展 `packages/shared/src/contracts/story.ts`
- `RuntimeStoryActionRequest` 增加可选 `snapshot`
- 新增 `RuntimeStoryStateRequest`
2. 新增 `POST /api/runtime/story/state/resolve`
3. `storyActionService` 内部统一处理“请求携带快照上下文时的服务端同步”
4.`npc_chat_quest_offer_replace` / `npc_chat_quest_offer_abandon` 接到后端 runtime action
## 4.3 第三批:把正式生成编排收成后端唯一出口
目标状态:
1. `questDirector` 只保留轻量 SDK。
2. `runtimeItemAiDirector` 只保留轻量 SDK。
3. custom world profile 正式生成走后端 route。
4. 浏览器侧 `src/services/ai.ts` 不再承担正式浏览器主链。
建议实施方式:
1. `server-node/src/routes/runtimeRoutes.ts`
-`custom-world/profile` 正式 route
2. `src/services/aiService.ts`
- custom world 入口改走后端
3. `src/services/questDirector.ts`
- 只请求 `/api/runtime/quests/generate`
4. `src/services/runtimeItemAiDirector.ts`
- 只请求 `/api/runtime/items/runtime-intent`
---
## 5. 本轮落地范围
本轮优先完成以下内容:
1. 鉴权维持 `codex/backend-rewrite-spacetimedb` 既有 JWT/Bearer 方案,不合入 `codex/dev` 的 access cookie 访问认证。
2. 浏览历史从前端本地真相后移到后端唯一真相。
3. custom world profile 正式生成入口补齐后端 route并把前端收成 SDK。
4. `questDirector` / `runtimeItemAiDirector` 收缩为前端 SDK。
5. runtime story contract 开始补“随请求提交快照上下文”的后端承接能力,并把 NPC 待接委托 replace / abandon 接到后端。
### 5.1 已完成
1. `codex/backend-rewrite-spacetimedb` 本轮保留 JWT access token + refresh cookie 组合方案,不合入 access cookie 写入与读取链路。
2. 浏览历史已收敛为后端唯一真相,前端不再维护正式本地 browse history 链。
3. runtime story 已支持随请求提交 snapshot由后端内部解释与持久化。
4. NPC 待接委托 `replace / abandon / accept` 已以后端 runtime action 为准。
5. custom world profile 浏览器正式入口已改走后端 route。
6. `questDirector` / `runtimeItemAiDirector` 已收缩为前端 SDK不再承担正式浏览器编排。
7. NPC 招募正式结算已迁到后端:
- 前端只负责招募对白展示与 release 目标选择
- 后端负责 `npcStates / companions / roster / currentEncounter / storyHistory` 正式结算
- 满员换队招募已由后端承接
### 5.2 剩余未完成
1. `src/services/ai.ts` 仍保留 legacy fallback / test 能力,尚未彻底压缩出正式浏览器主链。
2. 仍需继续审视是否存在其他 NPC / 运行时分支,把正式状态裁决留在前端。
---
## 6. 验收标准
### 第一批验收
1. 浏览器继续保存 access token并由 `fetchWithApiAuth` 稳定拼接 `Authorization: Bearer ...`
2. 401 刷新链只在已发送 Bearer token 时触发,并且刷新响应必须返回新的 JWT。
3. 浏览历史仅通过远端接口读写。
4. `src/services/platformBrowseHistory.ts` 不再是正式链路依赖。
### 第二批验收
1. `runtimeStoryCoordinator.ts` 不再在动作前独立 `PUT /runtime/save/snapshot`
2. `NPC` 待接委托 replace / abandon / accept 都以后端返回结果为准。
### 第三批验收
1. `questDirector.ts``runtimeItemAiDirector.ts` 不再保留正式 fallback orchestration。
2. custom world profile 的浏览器正式入口不再直接 import legacy `./ai`
---
## 7. 一句话结论
这轮迁移的重点不是“把几个 helper 挪到 server-node 目录”,而是:
**把前端里仍然承担正式真相、正式运行时解释和正式生成编排的那一层职责,继续收回到 Express 后端。**

View File

@@ -150,10 +150,11 @@ JWT 现状:
- `POST /api/custom-world/scene-image`
- `POST /api/runtime/story/initial`
- `POST /api/runtime/story/continue`
- `POST /api/runtime/custom-world/sessions`
- `GET /api/runtime/custom-world/sessions/:sessionId`
- `POST /api/runtime/custom-world/sessions/:sessionId/answers`
- `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream`
- `POST /api/runtime/custom-world/agent/sessions`
- `GET /api/runtime/custom-world/agent/sessions/:sessionId`
- `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages`
- `POST /api/runtime/custom-world/agent/sessions/:sessionId/actions`
- `GET /api/runtime/custom-world/works`
- `POST /api/runtime/chat/character/suggestions`
- `POST /api/runtime/chat/character/summary`
- `POST /api/runtime/chat/character/reply/stream`
@@ -183,10 +184,6 @@ JWT 现状:
- `GET /api/assets/character-animation/jobs/:taskId`
- `POST /api/assets/character-animation/import-video`
- `GET /api/assets/character-animation/templates`
- `POST /api/assets/qwen-sprite/master`
- `POST /api/assets/qwen-sprite/sheet`
- `POST /api/assets/qwen-sprite/frame-repair`
- `POST /api/assets/qwen-sprite/save`
编辑器与资产接口门禁:
@@ -227,9 +224,7 @@ Custom World
编辑器与资产工具层:
- `src/editor/shared/editorApiClient.ts`
- `src/editor/shared/useJsonSave.ts`
- `src/components/preset-editor/characterAssetStudioPersistence.ts`
- `src/tools/qwenSpriteSheetToolPersistence.ts`
## 10. 当前 Vite 角色

View File

@@ -11,7 +11,6 @@
- `server-node/src/services/eightAnchorPromptBuilder.ts`
- `server-node/src/modules/assets/characterAssetRoutes.ts`
- `src/services/**`
- `src/tools/qwenSpriteSheetToolModel.ts`
- `src/components/**`
问题主要有三类:
@@ -50,7 +49,6 @@ src/prompts/
├─ customWorldPrompts.ts
├─ customWorldRolePromptDefaults.ts
├─ questPrompts.ts
├─ qwenSpriteSheetToolPrompts.ts
├─ runtimeItemPrompts.ts
├─ storyOrchestratorPrompts.ts
└─ storyPromptBuilders.ts
@@ -82,8 +80,6 @@ src/prompts/
- 八锚点状态推断、模式规则与正式单轮共创 prompt
- `src/prompts/customWorldPrompts.ts`
- 自定义世界分阶段生成 prompt 与场景背景图 prompt
- `src/prompts/qwenSpriteSheetToolPrompts.ts`
- 精灵图工具主词 / 分镜词 / 修帧词 / 负面词
- `src/prompts/customWorldRolePromptDefaults.ts`
- 角色资产工作台默认 prompt 种子唯一主源
- `src/prompts/customWorldEntityActionPrompts.ts`
@@ -127,7 +123,6 @@ src/prompts/
- `src/services/characterChatPrompt.ts`
- `src/services/questPrompt.ts`
- `src/services/runtimeItemAiPrompt.ts`
- `src/tools/qwenSpriteSheetToolModel.ts`
- `src/components/asset-studio/customWorldRolePromptDefaults.ts`
- `packages/shared/src/assets/qwenSprite.ts`

View File

@@ -676,7 +676,7 @@ PixelMotion 很关键的一点,不是要求 16 帧都完美,而是允许修
### 13.4 推荐目录结构
```text
pixelmotion-qwen/
pixelmotion-workflow/
refs/
master.png
pose_board_run.png

View File

@@ -30,11 +30,39 @@
- [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSSWeb 端只负责签名读下载。
- [AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md](./AXUM_TO_SPACETIMEDB_ASSET_OBJECT_CONFIRM_CALL_DESIGN_2026-04-21.md):冻结 `POST /api/assets/objects/confirm` 从 Axum 通过 Rust SDK 调用 `SpacetimeDB procedure` 的最小落地方案,明确本地 server、数据库名、procedure/reducer 分工与 `spacetime-client` 边界。
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md)把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
- [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。
- [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。
- [CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md](./CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md):世界草稿生成失败后等待页误显示为“卡在编译草稿卡”的根因拆解、主链与增强链路边界,以及本次修复策略。
- [CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md](./CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md):世界草稿里“资产已生成但结果页看不到”的根因拆解,包含角色主形象展示、分幕背景露出和 fallback 资源格式修复。
- [CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md](./CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md)Phase4 新增角色/地点后草稿作品卡数量统计与测试断言的语义对齐说明。
- [AGENT_RESULT_PROFILE_SYNC_PHASE1_2026-04-20.md](./AGENT_RESULT_PROFILE_SYNC_PHASE1_2026-04-20.md):阶段一保持结果页深度编辑能力不变,同时把结果页完整世界快照同步回 Agent session 主链的方案说明。
- [AGENT_RESULT_PROFILE_SYNC_PHASE2_2026-04-20.md](./AGENT_RESULT_PROFILE_SYNC_PHASE2_2026-04-20.md):阶段二把平台创作入口统一到聚合作品列表,并收紧 Agent 结果页的新增入口职责边界。
- [AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md](./AGENT_RESULT_PROFILE_SYNC_PHASE3_2026-04-20.md):阶段三继续降级旧 pipeline让创作中心只认 Agent 草稿与已发布作品,并把 Agent 结果页冻结为预览收口层。
- [AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md](./AGENT_DIALOG_AND_RESULT_REFINEMENT_BOUNDARY_2026-04-21.md):修正 Agent 对话框与结果页职责边界,明确 Agent 只收集八锚点,已有底稿的精修进入结果页完成。
- [CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md](./CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md):对照当前优化计划核查四阶段完成度,并明确这轮只允许物理删除旧 `custom-world/sessions` 世界生成链,不误伤 Agent 主链与已保存作品兼容编辑链。
- [CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):梳理当前创作入口到结果页自动保存再到进入世界的全链前后端脚本地图,并给出文件级重构拆分方案、目标分层与阶段验收标准。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):梳理当前 RPG 从平台入口、继续游戏、角色选择到营地开场、冒险运行态与 runtime story 后端结算的全链脚本地图,并给出 RPG 专属命名规范、目标分层和可并行执行的工作包。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_A_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_A_PROGRESS_2026-04-21.md):记录 RPG 进入游戏与运行时链路工作包 A 已完成的新目录骨架、前后端 façade、按域路由 path 常量与兼容仓储入口。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_B_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_B_PROGRESS_2026-04-21.md):记录工作包 B 已完成的前端 RPG 入口壳层真实迁移、`rpg-entry` 新入口 hooks 收口,以及旧 `game-shell` / `rpg-creation-flow` 路径降级为兼容层的状态。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_C_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_C_PROGRESS_2026-04-21.md):记录工作包 C 已完成的 `rpg-session` 主链迁移、snapshot / save archive client 收口、旧 `useGame*` 降级为兼容 façade以及定向回归结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_D_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_D_PROGRESS_2026-04-21.md):记录工作包 D 已完成的前端运行态 shell / stage router / panel router 真实迁移、AdventurePanel section 拆分,以及旧 `GameShell*` 热点降级为兼容桥的现状。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_F_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_F_PROGRESS_2026-04-21.md):记录 RPG 进入游戏与运行时链路工作包 F 已完成的后端 route 真正拆边界、`app.ts` 新域挂载、旧 `runtimeRoutes` / `storyActionRoutes` 兼容降级,以及定向路由回归验证结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_E_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_E_PROGRESS_2026-04-21.md):记录工作包 E 已完成的前端 runtime story 主链真实迁移、NPC 交互与 gateway/client 收口、旧入口兼容降级,以及定向回归验证结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_G_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_G_PROGRESS_2026-04-21.md):记录工作包 G 已完成的后端 runtime session / action service 物理迁移、新域原语导出、旧热点兼容降级,以及定向 runtime story 回归验证结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_H_PROGRESS_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_H_PROGRESS_2026-04-21.md):记录工作包 H 已完成的 RPG 运行时仓储拆分、shared runtime contract 分文件、旧 `story.ts` façade 兼容与定向回归结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_PARALLEL_BATCH_AUDIT_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_PARALLEL_BATCH_AUDIT_2026-04-21.md):对照执行计划逐项复核第一批与第二批并行工作的真实落地状态,记录本轮确认到的测试合流收口遗漏与文档索引补齐结果。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_PHASE3_CLOSURE_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_PHASE3_CLOSURE_2026-04-21.md):记录 RPG 执行计划第三批收口已完成的前端新域主链接回、后端新仓储接线、shared contract 直连收紧、旧兼容脚本物理删除,以及明确未扩到 UI 和无关历史文档的边界。
- [RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_OLD_SCRIPT_REMOVAL_2026-04-21.md](./RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_OLD_SCRIPT_REMOVAL_2026-04-21.md):记录 RPG 主链旧 `GameShell``useGame*``hooks/story``runtimeRoutes``modules/story/*``contracts/story.ts` 脚本的物理删除范围、残留依赖扫描和定向验证结果。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_A_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_A_PROGRESS_2026-04-21.md):记录创作链路重构工作包 A 已落地的 RPG 创作域目录骨架、兼容 façade以及补齐后的共享契约骨架入口。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_E_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_E_PROGRESS_2026-04-21.md):记录工作包 E 已完成的后端 Agent 编排拆分、executor 物理迁移、发布链切到 `CustomWorldAgentPublishingService`、checkpoint 真快照、场景资产 coverage 收口,以及 Phase3/Phase5 定向回归结果。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_H_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_H_PROGRESS_2026-04-21.md):记录工作包 H 已完成的共享契约物理拆分、旧命名兼容分文件、统一 fixture以及 shared contract test / preview compiler / works assembler / works service 回归基建。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_B_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_B_PROGRESS_2026-04-21.md):记录工作包 B 已完成的前端平台壳层编排拆分、平台 hooks / coordinator 接入、旧入口兼容保留,以及交互回归验证结果。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_F_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_F_PROGRESS_2026-04-21.md):记录工作包 F 已完成的后端 session/store/repository 拆分、works 读模型 service 收口、route/context 直接注入新仓储,以及定向 custom world 回归验证结果。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_D_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_D_PROGRESS_2026-04-21.md):记录工作包 D 已完成的前端 custom world client 真正迁出、旧 service 兼容降级,以及平台壳层/结果页测试切换到 `rpgCreation` 域入口的现状。
- [CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_G_PROGRESS_2026-04-21.md](./CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_G_PROGRESS_2026-04-21.md):记录工作包 G 已完成的 runtime profile 目录化、服务端 preview compiler 收口,以及 foundation draft 主生成链与 preview 编译边界的直接拆开。
- [CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-工作包-c前端结果页与编辑器拆分](./CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-%E5%B7%A5%E4%BD%9C%E5%8C%85-c%E5%89%8D%E7%AB%AF%E7%BB%93%E6%9E%9C%E9%A1%B5%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8%E6%8B%86%E5%88%86):记录工作包 C 已完成的结果页壳层拆分、编辑器目标分发与 mapper 收口、角色资产工坊 section/workflow 拆分,以及仍保留的阶段性 shared 实现边界。
- [CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md](./CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md):创作页移动端底部 Tab、亮色主题 token 与滚动权责修复记录。
- [TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md](./TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md):把外部仓库 TXT 模式完整迁入当前项目的冻结边界、模块映射、分阶段计划与验收清单。
- [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md):由 `server-node/src/manifest/backendCapabilityManifest.ts` 生成的 Node 后端模块职责、挂载面与接口索引,后续新增模块/接口时同步更新这一份。
- [NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md](./NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md):当前 Node 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。

View File

@@ -0,0 +1,999 @@
# RPG 进入游戏与运行时链路重构执行方案
更新时间:`2026-04-21`
## 0. 文档目标
本文只处理一件事:
**把当前 RPG 玩法从“平台入口/继续游戏/世界详情开始游戏”到“角色选择/营地开场/冒险运行态/runtime story 后端动作结算”的整条前后端脚本链路,整理成一份可以直接指导后续并行重构的执行方案。**
本轮不直接修改业务玩法,不新增需求,只明确:
1. 当前链路上的真实脚本地图
2. 当前命名、目录、边界和可读性问题
3. 面向 RPG 类型游戏的专属命名规范
4. 目标分层与文件级拆分建议
5. 可同时并行推进的工作包与阶段验收标准
同时补充一条必须冻结的执行约束:
**本次以及后续按本文推进的 RPG 链路重构,只允许调整脚本结构、命名、职责边界、数据流和兼容 façade不允许修改任何前端交互界面设计。**
---
## 1. 范围与依据
### 1.1 本文覆盖的 RPG 进入游戏链路
```text
平台首页 / 作品详情 / 继续游戏
-> 选择世界或恢复存档
-> 角色选择
-> 初始化 GameState / Snapshot / Session
-> 营地开场
-> 冒险运行态 shell
-> 冒险面板 / 角色面板 / 背包面板
-> runtime story 选项解析与动作结算
-> 服务端快照持久化 / 状态回写 / 继续游戏恢复
```
### 1.2 本文主要依据
1. `docs/prd/AI_NATIVE_SCENE_CHAPTER_GAMEPLAY_PRD_AND_EXECUTION_PLAN_2026-04-08.md`
2. `docs/design/SCENE_CHAPTER_LOOP_AND_FIRST_ENTRY_CHAPTER_QUEST_DESIGN_2026-04-08.md`
3. `docs/experience/CURRENT_GAME_FULL_FLOW_PLAYTEST_REPORT_2026-04-07.md`
4. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md`
5. `docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md`
6. `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md`
### 1.3 本文刻意不覆盖的链路
本文不处理以下内容:
1. RPG 创作流程链
2. Agent 八锚点共创流程
3. 自定义世界结果页编辑器内部资产工坊链
4. 非 RPG 平台公共功能的全面改造
这些内容已有独立文档,本文只关注**进入 RPG 运行态之后的主玩法链**。
### 1.4 前端界面冻结约束
本次重构对前端界面的约束必须写死:
1. 不修改任何前端交互界面设计。
2. 不修改现有页面的视觉层级、主布局结构、按钮位置、tab 组织、弹窗/独立面板的出现方式。
3. 不以“顺手优化体验”为理由调整入口页、选角页、冒险页、角色页、背包页的交互路径。
4. 重构允许做的事情只包括脚本重命名、目录迁移、hook/service 拆分、view model 收口、后端路由与服务拆分、兼容 façade 搭建。
5. 如果个别脚本拆分必须调整 props 传递或组件装配方式,最终渲染结果与交互结果必须和当前版本保持一致。
6. 任何会影响 UI 结构、交互节奏、面板开合形式的改动,都不属于本文工作范围,必须另开设计文档与实现任务。
---
## 2. 当前链路真实脚本地图
## 2.1 前端入口与进入世界链
| 文件 | 当前职责 | 当前问题 |
| --- | --- | --- |
| `src/App.tsx` | 应用入口,直接挂载 `useGameShellRuntime()``GameShellRuntime` | 入口极薄,但把“平台入口 + RPG 运行态”全部抽象成 `GameShell`,命名过泛 |
| `src/hooks/useGameShellRuntime.ts` | 串起 `useGameFlow``useGamePersistence``useStoryGeneration`、同伴与音乐逻辑,组装整套运行时 props | 已经是事实上的 RPG 主流程装配器,但命名仍像通用 shell |
| `src/hooks/useGameFlow.ts` | 世界选择、角色确认、`GameState` 初始化、营地遭遇创建 | 负责“进入游戏”的核心初始化,但文件名过泛,且把世界选择、选角、开局初始化混在一起 |
| `src/hooks/useGamePersistence.ts` | 远端快照加载、自动存档、保存退出、继续游戏恢复 | 快照加载、存档写入、恢复后 runtime story 刷新耦合在同一 hook 中 |
| `src/components/game-shell/PreGameSelectionFlow.tsx` | 平台首页、详情页、存档继续、创作入口、进入世界前流程壳层 | 仍承载过多平台级编排,不只负责 RPG 进入游戏链 |
| `src/components/game-shell/PlatformHomeView.tsx` | 平台首页、继续游戏、公开广场、存档/个人 tab 表现层 | 视觉层文件过大且“平台首页”与“RPG 进入游戏入口”没有显式命名边界 |
| `src/components/game-shell/PlatformWorldDetailView.tsx` | 世界详情与“开始游戏/继续创作/发布”等操作 | `开始游戏` 是 RPG 入口动作,但当前文件名和职责仍偏平台通用详情页 |
| `src/components/game-shell/CharacterSelectionFlow.tsx` | 角色选择、角色自定义草稿、确认进入营地 | 已是纯 RPG 选角页面,但命名仍是泛化 `SelectionFlow` |
## 2.2 前端运行态壳层与面板链
| 文件 | 当前职责 | 当前问题 |
| --- | --- | --- |
| `src/components/game-shell/GameShellRuntime.tsx` | 运行态最外层 shell装配画布、主内容、各种 overlay | 同时承担平台主题外壳和 RPG 运行态外壳,命名与职责都偏泛 |
| `src/components/game-shell/GameShellMainContent.tsx` | 根据 `worldType / playerCharacter / selectionStage` 在平台、选角、冒险面板三种主阶段间切换 | 实际上是 RPG 主阶段路由器,但文件名没有表达“入口阶段切换” |
| `src/components/game-shell/GameShellStoryPanels.tsx` | 冒险/角色/背包三个主标签切换,挂载 `AdventurePanel``CharacterPanel``InventoryPanel` | 运行态主面板路由器与 tab 容器混在一起 |
| `src/components/AdventurePanel.tsx` | 冒险主面板、对话流、选项区、任务/设置/统计 overlay、NPC 聊天输入、奖励面板 | 单文件过大,是当前前端 RPG 运行时最大热点之一 |
| `src/components/game-shell/useGameShellRuntimeViewModel.ts` | 运行态视图模型、可见状态、过场、统计、对话指示器 | 负责运行态展示编排,但仍以 `GameShell` 泛名承载 |
| `src/components/game-shell/useGameShellViewModel.ts` | overlay/modal/选中实体/selectionStage 的壳层状态 | 进入游戏前与进入游戏后 UI 状态混在一起,命名不够领域化 |
## 2.3 前端剧情运行时协调链
| 文件 | 当前职责 | 当前问题 |
| --- | --- | --- |
| `src/hooks/useStoryGeneration.ts` | 运行态故事主入口,拼装 runtime controller、goal session、interaction coordinator | 事实上的 RPG 叙事运行时入口,但命名过于抽象 |
| `src/hooks/story/useStoryRuntimeController.ts` | 当前故事、AI 错误、故事请求入口、fallback story 和 commit 动作 | 浏览器 AI 请求与服务端 runtime story 共同挂在同一 controller 上 |
| `src/hooks/story/useStoryFlowCoordinator.ts` | 汇总 goal option、interaction、session 行为,输出完整故事流程能力 | 多层 coordinator 套娃,可读性差 |
| `src/hooks/story/useStoryGoalSessionCoordinator.ts` | 任务领奖、重置 story、恢复 story、地图移动 | 任务会话动作与 story 生命周期控制混在一起 |
| `src/hooks/story/useStoryInteractionCoordinator.ts` | 选择分发、NPC 交互、宝藏交互、背包动作、战斗奖励、聊天输入 | 交互中心职责太重,是第二个热点文件 |
| `src/hooks/story/npcEncounterActions.ts` | NPC 聊天、切磋、委托接受/替换/放弃、服务端 runtime action、聊天 UI 细节 | 依然是巨型多职责文件,且混合 UI 组装、状态更新、服务端请求 |
| `src/hooks/story/runtimeStoryCoordinator.ts` | 继续游戏后恢复 runtime story、状态解析、server action 调用 | 是前端对接服务端 runtime story 的真正 gateway但命名像临时协调器 |
| `src/hooks/story/storyRequestCoordinator.ts` | AI 故事请求参数编排、server option catalog 决策 | 与 runtime story 主链存在交叉语义,边界不直观 |
| `src/hooks/story/sessionActions.ts` | 地图移动、任务奖励、story reset/hydrate | “session action” 命名过泛,而且同时处理 quest/chapter/story 三类状态 |
## 2.4 前端 service / client 链
| 文件 | 当前职责 | 当前问题 |
| --- | --- | --- |
| `src/services/storageService.ts` | 存档、设置、个人看板、浏览历史、作品库、继续游戏、世界详情 | RPG 快照/存档接口与平台资料/作品库接口混在一个通用 client 中 |
| `src/services/runtimeStoryService.ts` | `/api/runtime/story` 的状态读取、动作提交、story moment 转换 | 已接近 RPG 专属 client但文件名仍偏通用 |
| `src/services/aiService.ts` | 初始剧情/续写、角色聊天、NPC 聊天、runtime item、quest、custom world 共创接口 | RPG 运行时 AI、角色聊天、创作 Agent 接口混在一起,领域过宽 |
## 2.5 后端路由与运行时主链
| 文件 | 当前职责 | 当前问题 |
| --- | --- | --- |
| `server-node/src/server.ts` | 组装 `AppContext`,注入 runtimeRepository、customWorldAgentOrchestrator 等依赖 | 依赖对象过于集中RPG 运行态缺少显式模块边界 |
| `server-node/src/app.ts` | 注册 `/api/auth``/api/runtime/story``/api` 等总路由 | RPG 进入世界链、平台路由、编辑器路由全部在总 app 中汇合,语义不够清晰 |
| `server-node/src/routes/runtimeRoutes.ts` | 资料、存档、浏览历史、作品库、runtime AI、世界生成等大杂糅路由 | 当前后端最大热点之一,平台资料与 RPG runtime 接口强耦合 |
| `server-node/src/modules/story/storyActionRoutes.ts` | runtime story 状态读取、动作结算 | 路由层本身还算薄,但命名仍然过于通用 |
| `server-node/src/modules/story/storyActionService.ts` | runtime story 状态/动作主应用服务,拼接 combat、npc、quest、treasure、LLM story | 是当前后端 RPG 运行时主热点,承担过多动作路由和 story 组装细节 |
| `server-node/src/modules/story/runtimeSession.ts` | runtime snapshot 归一化、option 构建、viewModel 编译、legacy currentStory 构建、rawGameState 同步 | 运行态编译中心过重,加载器、编译器、同步器、兼容层全部混在一起 |
| `server-node/src/modules/npc/npcInteractionService.ts` | NPC help/chat/fight/spar/recruit 等动作结算 | 与 runtime story 仍存在大量双向耦合 |
| `server-node/src/modules/quest/questStoryActionService.ts` | 委托接受、交付、待接委托读取与结算 | 已承担正式 quest 语义,但入口仍埋在 storyActionService 下游 |
| `server-node/src/repositories/runtimeRepository.ts` | 快照、存档列表、看板、浏览历史、作品库、会话等持久化读写 | 仓储过大,按技术分组而不是按 RPG 领域分组 |
| `server-node/src/modules/runtime/runtimeSnapshotHydration.ts` | 存档/gameState/currentStory 归一化、迁移补丁、默认值填充 | 既承担快照迁移,又承担业务字段补齐,是基础设施与领域逻辑混合点 |
| `packages/shared/src/contracts/story.ts` | 前后端 runtime story / npc chat / quest / runtime item 等共享契约 | RPG 运行时契约体量过大story/action/state/chat 混放,难以独立演进 |
---
## 3. 当前结构性问题
## 3.1 命名没有体现“这是 RPG 进入游戏主链”
当前主链上充满以下泛化命名:
1. `GameShell`
2. `MainContent`
3. `SelectionFlow`
4. `runtimeRoutes`
5. `storyActionService`
6. `sessionActions`
这些命名的问题不是“不好看”,而是:
1. 无法一眼区分平台入口、RPG 进入游戏、RPG 运行态
2. 无法一眼看出文件属于前端壳层、状态协调器、还是后端应用服务
3. 后续非 RPG 流程接入时,很容易继续误复用这些泛化热点文件
## 3.2 平台入口与 RPG 进入游戏链混在一起
`PreGameSelectionFlow.tsx` 当前同时承担:
1. 平台首页
2. 详情页
3. 存档恢复
4. 创作入口
5. 进入世界
这会导致:
1. 任何平台改动都可能碰到 RPG 进入游戏主链
2. 任何 RPG 进入游戏改动都要穿过平台杂项状态
3. 文件变成事实上的超大编排中心
## 3.3 `GameState` 初始化、快照恢复、运行态切入没有明确分层
`useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts` 当前共同承担:
1. 新开局初始化
2. 世界选择
3. 角色选择
4. 自动存档
5. 继续游戏恢复
6. runtime story 恢复刷新
结果是:
1. “进入游戏前的 session bootstrap”与“进入游戏后的自动持久化”没有明确边界
2. 继续游戏逻辑很难单独替换或扩展
3. 任何存档策略变更都容易影响开局链
## 3.4 前端剧情运行时协调层过多,职责分散却仍然耦合
当前前端 story 主链至少经过:
1. `useStoryGeneration`
2. `useStoryRuntimeController`
3. `useStoryFlowCoordinator`
4. `useStoryGoalSessionCoordinator`
5. `useStoryInteractionCoordinator`
6. `npcEncounterActions`
7. `runtimeStoryCoordinator`
问题在于:
1. 层数多,但不是稳定分层,而是热点文件之间互相穿透
2. 有些层是 view model有些层是 action dispatcher有些层是 server gateway命名看不出来
3. 浏览器 AI 续写链与服务端 runtime story 链还没有完全收口为两个明确通道
## 3.5 后端路由层过于“大 runtime 大入口”
`runtimeRoutes.ts` 当前同时覆盖:
1. profile dashboard
2. browse history
3. save archives
4. custom world library/gallery
5. custom world profile generation
6. runtime story 外围 AI 接口
7. runtime item / quest 生成接口
这会导致:
1. RPG 进入游戏链难以抽出独立模块
2. 平台资料接口和 RPG runtime 接口一起变更时风险高
3. route 文件越来越像“后端总控清单”
## 3.6 `runtimeSession.ts` 是当前后端最大可读性瓶颈之一
这个文件当前同时做了:
1. snapshot 载入
2. rawGameState 归一化
3. option interaction 构建
4. battle option 编译
5. NPC option 编译
6. viewModel 编译
7. legacy currentStory 兼容输出
8. rawGameState 回写同步
这会直接造成:
1. 新增动作时很难判断应该改哪里
2. 任何小调整都容易触碰多个职责
3. 单测难以按职责拆开
## 3.7 持久化仓储按技术堆叠,没有按 RPG 域拆开
`runtimeRepository.ts` 把:
1. snapshot
2. save archives
3. settings
4. dashboard
5. browse history
6. custom world library
7. custom world sessions
全部堆在一起。
对 RPG 进入游戏链来说,至少应该显式分开:
1. 运行时快照
2. 存档归档
3. 平台资料
4. 世界库/详情
否则“继续游戏链”与“平台资料链”永远无法清晰拆边界。
---
## 4. 目标分层架构
## 4.1 目标原则
后续重构必须统一遵守 7 条原则:
1. **平台入口只负责进入 RPG会话真相不留在页面壳层。**
2. **世界选择、角色选择、新开局、继续游戏恢复属于 RPG session 入口域。**
3. **进入世界后的运行态壳层、冒险面板、story runtime 网关必须显式分层。**
4. **前端只保留展示状态、输入状态、UI 过场状态;正式快照、正式动作、正式 story option 以后端为准。**
5. **后端 route / application service / compiler / repository 必须按 RPG 域拆开,不再扩大“大 runtime 单文件”。**
6. **所有新命名都要显式表达“这是 RPG 类型游戏专属流程”,不能继续依赖 `GameShell / runtime / flow` 这类泛化词。**
7. **重构期间严格冻结前端交互界面设计,脚本重组不能改变任何页面结构与交互表现。**
## 4.2 目标链路
```text
RPG 平台入口壳层
-> RPG session bootstrap
-> RPG 角色选择
-> RPG 运行态 shell
-> RPG 运行态面板路由
-> RPG runtime story gateway
-> RPG runtime story routes
-> RPG runtime action/state services
-> RPG runtime session loader/compiler
-> RPG snapshot repository / save archive repository
```
## 4.3 推荐目录骨架
### 前端
```text
src/
├─ components/
│ ├─ rpg-entry/
│ ├─ rpg-runtime-shell/
│ ├─ rpg-runtime-panels/
│ └─ rpg-runtime-overlays/
├─ hooks/
│ ├─ rpg-session/
│ └─ rpg-runtime-story/
└─ services/
├─ rpg-entry/
└─ rpg-runtime/
```
### 后端
```text
server-node/src/
├─ routes/
│ ├─ rpg-entry/
│ ├─ rpg-profile/
│ └─ rpg-runtime/
├─ modules/
│ └─ rpg-runtime-story/
├─ services/
│ ├─ rpg-entry/
│ └─ rpg-runtime/
└─ repositories/
├─ rpg-entry/
└─ rpg-runtime/
```
---
## 5. RPG 专属命名规范
## 5.1 命名根
后续进入游戏与运行态链统一使用以下命名根:
1. `rpgEntry`
- 平台首页、详情页、世界进入、角色选择、继续游戏入口
2. `rpgSession`
- 新开局、继续游戏恢复、快照 persistence、开局 bootstrap
3. `rpgRuntime`
- 游戏内 shell、tab、面板、overlay、view model
4. `rpgRuntimeStory`
- story state/action client、gateway、route、service、compiler
5. `rpgProfile`
- dashboard、browse history、save archive 等玩家资料域
## 5.2 命名规则
1. React 组件文件统一使用 `Rpg...` 前缀。
2. hooks 统一使用 `useRpg...` 前缀。
3. 前端 client/gateway/adapter 统一使用 `rpg...` 小驼峰前缀。
4. 后端 route 使用 `rpg...Routes.ts`
5. 后端应用服务使用 `Rpg...Service.ts`
6. 后端编译器/装配器使用 `Rpg...Compiler.ts` / `Rpg...Assembler.ts`
7. 后端仓储使用 `Rpg...Repository.ts`
8. 共享契约文件优先拆成 `rpgEntry...``rpgRuntimeStory...``rpgProfile...`
## 5.3 命名禁忌
后续重构中禁止继续新增以下主命名:
1. `GameShell*`
2. `PreGame*`
3. `SelectionFlow*`
4. `runtimeRoutes.ts` 这种单文件总入口命名
5. `storyActionService.ts` 这种过宽的单域名
6. `sessionActions.ts``flowCoordinator.ts``manager.ts``helper.ts` 作为主业务模块名
## 5.4 关键文件重命名建议
| 当前文件 | 目标命名 | 说明 |
| --- | --- | --- |
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `src/components/rpg-entry/RpgEntryFlowShell.tsx` | 平台进入世界与选角前阶段壳层 |
| `src/components/game-shell/PlatformHomeView.tsx` | `src/components/rpg-entry/RpgEntryHomeView.tsx` | RPG 平台首页 |
| `src/components/game-shell/PlatformWorldDetailView.tsx` | `src/components/rpg-entry/RpgEntryWorldDetailView.tsx` | 世界详情与开始游戏入口 |
| `src/components/game-shell/CharacterSelectionFlow.tsx` | `src/components/rpg-entry/RpgEntryCharacterSelectView.tsx` | RPG 选角页 |
| `src/hooks/useGameFlow.ts` | `src/hooks/rpg-session/useRpgSessionBootstrap.ts` | 新开局/世界选择/角色确认 |
| `src/hooks/useGamePersistence.ts` | `src/hooks/rpg-session/useRpgSessionPersistence.ts` | 自动存档/继续游戏恢复 |
| `src/hooks/useGameShellRuntime.ts` | `src/hooks/rpg-session/useRpgRuntimeSession.ts` | RPG 主运行态装配器 |
| `src/components/game-shell/GameShellRuntime.tsx` | `src/components/rpg-runtime-shell/RpgRuntimeShell.tsx` | 运行态总外壳 |
| `src/components/game-shell/GameShellMainContent.tsx` | `src/components/rpg-runtime-shell/RpgRuntimeStageRouter.tsx` | 平台/选角/冒险阶段切换 |
| `src/components/game-shell/GameShellStoryPanels.tsx` | `src/components/rpg-runtime-panels/RpgRuntimePanelRouter.tsx` | 冒险/角色/背包主标签路由 |
| `src/components/AdventurePanel.tsx` | `src/components/rpg-runtime-panels/RpgAdventurePanel.tsx` | 冒险主面板 |
| `src/hooks/useStoryGeneration.ts` | `src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts` | 前端 story 运行态主入口 |
| `src/hooks/story/useStoryRuntimeController.ts` | `src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.ts` | 当前 story 与请求控制 |
| `src/hooks/story/useStoryFlowCoordinator.ts` | `src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts` | story 主编排 |
| `src/hooks/story/useStoryInteractionCoordinator.ts` | `src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts` | runtime 交互分发 |
| `src/hooks/story/npcEncounterActions.ts` | `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` | NPC 交互与聊天动作 |
| `src/hooks/story/runtimeStoryCoordinator.ts` | `src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts` | 前端到后端 runtime story 网关 |
| `src/services/runtimeStoryService.ts` | `src/services/rpg-runtime/rpgRuntimeStoryClient.ts` | `/api/runtime/story` client |
| `src/services/storageService.ts` | 拆成 `rpgProfileClient.ts` / `rpgEntryLibraryClient.ts` / `rpgSnapshotClient.ts` | 按领域拆 client |
| `server-node/src/routes/runtimeRoutes.ts` | 拆成 `rpgProfileRoutes.ts` / `rpgEntryRoutes.ts` / `rpgRuntimeAiRoutes.ts` / `rpgWorldLibraryRoutes.ts` | 拒绝单文件总路由 |
| `server-node/src/modules/story/storyActionRoutes.ts` | `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts` | runtime story 专属路由 |
| `server-node/src/modules/story/storyActionService.ts` | `RpgRuntimeStoryActionService.ts` + `RpgRuntimeStoryStateService.ts` | 动作结算与状态读取拆开 |
| `server-node/src/modules/story/runtimeSession.ts` | `RpgRuntimeSessionLoader.ts` + `RpgRuntimeOptionCompiler.ts` + `RpgRuntimeSnapshotSync.ts` | 按职责拆分 |
| `server-node/src/repositories/runtimeRepository.ts` | `RpgRuntimeSnapshotRepository.ts` 等多个仓储 | 按领域拆仓储 |
| `packages/shared/src/contracts/story.ts` | 拆成 `rpgRuntimeStory.ts` / `rpgRuntimeChat.ts` / `rpgRuntimeAction.ts` | 契约拆分 |
---
## 6. 前端重构拆分方案
## 6.1 RPG 入口壳层拆分
### 当前问题
`PreGameSelectionFlow.tsx` 当前同时承担平台首页、详情页、进入世界、存档恢复与部分创作入口逻辑。
### 目标拆分
保留一个极薄的 `RpgEntryFlowShell.tsx`,只负责:
1. 入口阶段切换
2. 装配子页面
3. loading / error 壳层
从当前文件拆出:
1. `useRpgEntryBootstrap.ts`
2. `useRpgEntryNavigation.ts`
3. `useRpgEntrySaveResume.ts`
4. `useRpgEntryLibraryDetail.ts`
5. `RpgEntryHomeView.tsx`
6. `RpgEntryWorldDetailView.tsx`
7. `RpgEntryCharacterSelectView.tsx`
### 关键要求
1. 入口壳层不再直接操作作品库、浏览历史、看板的多路加载细节。
2. 入口壳层不再直接处理“继续游戏后刷新 runtime story”的逻辑。
3. 创作入口与 RPG 进入世界入口要显式分段,不再共享大文件。
4. 页面视觉结构、按钮布局、tab 形式和独立面板交互方式保持不变。
## 6.2 RPG session bootstrap / persistence 拆分
### 当前问题
`useGameFlow.ts``useGamePersistence.ts` 共同承担了开局初始化、世界进入、存档自动保存和恢复。
### 目标拆分
建议拆出:
1. `useRpgSessionBootstrap.ts`
2. `useRpgCharacterEntry.ts`
3. `useRpgSnapshotPersistence.ts`
4. `useRpgContinueGame.ts`
5. `rpgSnapshotClient.ts`
### 关键要求
1. 新开局初始化与继续游戏恢复是两条显式流程。
2. `GameState` 初始化逻辑不再和自动存档逻辑放在同一 hook 里。
3. 存档自动保存不再直接夹带 UI 层状态决策。
4. 脚本拆分后不改变用户看到的进入游戏流程和交互顺序。
## 6.3 RPG 运行态 shell 与面板拆分
### 当前问题
`GameShellRuntime.tsx``GameShellMainContent.tsx``GameShellStoryPanels.tsx``AdventurePanel.tsx` 共同组成了一个过于耦合的壳层群。
### 目标拆分
建议形成:
1. `RpgRuntimeShell.tsx`
2. `RpgRuntimeStageRouter.tsx`
3. `RpgRuntimePanelRouter.tsx`
4. `RpgAdventurePanel.tsx`
5. `RpgRuntimeOverlayHost.tsx`
6. `useRpgRuntimeShellViewModel.ts`
### 关键要求
1. 运行态最外层壳层只管布局、背景、过场和 overlay host。
2. 主阶段路由器只管“平台/选角/冒险”的分流。
3. 面板路由器只管“冒险/角色/背包”的主 tab 分流。
4. `AdventurePanel` 内部继续按“story 区 / option 区 / overlay 区”拆 section。
5. 不重做任何面板样式、信息层次和交互布局,拆分只发生在脚本内部。
## 6.4 前端 runtime story 主链拆分
### 当前问题
前端 runtime story 主链层数太多,但边界并不稳定。
### 目标拆分
建议收成四层:
1. `useRpgRuntimeStory.ts`
- 作为唯一对上输出入口
2. `useRpgRuntimeStoryState.ts`
- 管当前 story、loading、error、hydration
3. `useRpgRuntimeInteractionFlow.ts`
- 分发 NPC / 战斗 / 宝藏 / 任务 / 地图动作
4. `rpgRuntimeStoryGateway.ts`
- 只负责和后端 runtime story client 交互
### 关键要求
1. 浏览器侧 AI 续写逻辑与 server runtime story 逻辑必须显式分离。
2. `npcEncounterActions.ts` 中的 UI 细节和正式动作结算必须拆开。
3. 任何 `resolveServerRuntimeChoice(...)` 都应只通过统一 gateway 入口调用。
4. 交互按钮、对话区、输入区和奖励展示的前端交互形式保持现状不变。
## 6.5 前端 service/client 收口方案
### 当前问题
`storageService.ts``aiService.ts` 的领域过宽。
### 目标拆分
建议新增:
1. `src/services/rpg-entry/rpgEntryLibraryClient.ts`
2. `src/services/rpg-entry/rpgProfileClient.ts`
3. `src/services/rpg-runtime/rpgSnapshotClient.ts`
4. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts`
5. `src/services/rpg-runtime/rpgRuntimeChatClient.ts`
### 关键要求
1. `storageService.ts` 逐步降级为兼容 façade。
2. `aiService.ts` 逐步只保留通用 AI 与创作域 client不继续承接 RPG runtime story 主链。
3. 进入游戏链不能再依赖过宽的通用 client 文件。
---
## 7. 后端重构拆分方案
## 7.1 route 层拆分
### 当前问题
`runtimeRoutes.ts` 过重,平台资料、作品库和 RPG runtime 都在一起。
### 目标拆分
建议拆成:
1. `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts`
2. `server-node/src/routes/rpg-entry/rpgEntrySaveRoutes.ts`
3. `server-node/src/routes/rpg-entry/rpgWorldLibraryRoutes.ts`
4. `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts`
5. `server-node/src/routes/rpg-runtime/rpgRuntimeAiAssistRoutes.ts`
### 关键要求
1. RPG 进入游戏链相关接口必须从“大 runtime 总路由”中抽离。
2. `app.ts` 中要能一眼看出平台资料、世界库、runtime story 的路由边界。
3. route 层继续保持薄,不直接承载 story 业务决策。
## 7.2 runtime story service 拆分
### 当前问题
`storyActionService.ts` 当前同时承担状态读取、动作结算、LLM story 包装和 snapshot 持久化。
### 目标拆分
建议拆为:
1. `RpgRuntimeStoryStateService.ts`
2. `RpgRuntimeStoryActionService.ts`
3. `RpgRuntimeCombatActionService.ts`
4. `RpgRuntimeNpcActionService.ts`
5. `RpgRuntimeStoryPresentationCompiler.ts`
6. `RpgRuntimeStorySnapshotCommitService.ts`
### 关键要求
1. 状态读取与动作结算分开。
2. 各子域动作要能各自单测,不再全部挂在一个 service 里。
3. LLM 二次包装 story 文本不能继续散在动作服务主文件中。
## 7.3 `runtimeSession.ts` 目录化拆分
### 当前问题
`runtimeSession.ts` 已经集中了太多运行时编译职责。
### 目标拆分
建议新增目录:
```text
server-node/src/modules/rpg-runtime-story/session/
├─ RpgRuntimeSessionLoader.ts
├─ RpgRuntimeEncounterNormalizer.ts
├─ RpgRuntimeOptionCompiler.ts
├─ RpgRuntimeBattleOptionCompiler.ts
├─ RpgRuntimeNpcOptionCompiler.ts
├─ RpgRuntimeViewModelCompiler.ts
├─ RpgRuntimeLegacyStoryAdapter.ts
└─ RpgRuntimeSnapshotSync.ts
```
### 关键要求
1. loader、compiler、legacy adapter、snapshot sync 必须物理拆开。
2. “是否生成 legacy currentStory”要成为单独兼容层而不是散落在核心编排里。
3. interaction 语义编译要可单测、可扩展。
## 7.4 repository 拆分
### 当前问题
`runtimeRepository.ts` 不是 RPG 进入游戏链友好的结构。
### 目标拆分
建议拆成:
1. `RpgRuntimeSnapshotRepository.ts`
2. `RpgSaveArchiveRepository.ts`
3. `RpgProfileDashboardRepository.ts`
4. `RpgBrowseHistoryRepository.ts`
5. `RpgWorldLibraryRepository.ts`
### 关键要求
1. snapshot、save archive 与资料型仓储分离。
2. 世界库详情读取与运行时快照读写不再耦合在同一仓储。
3. 后续“继续游戏”链可以只依赖 snapshot/save archive 仓储。
## 7.5 shared contract 拆分
### 当前问题
`packages/shared/src/contracts/story.ts` 已经过大。
### 目标拆分
建议拆为:
1. `rpgRuntimeStoryAction.ts`
2. `rpgRuntimeStoryState.ts`
3. `rpgRuntimeChat.ts`
4. `rpgRuntimeQuestAssist.ts`
### 关键要求
1. runtime story 主链契约要能独立演进,不被 NPC chat / quest / item 附属能力拖累。
2. 前后端只在必要范围共享契约,减少大而全文件。
---
## 8. 可并行重构工作包
本次执行计划必须拆成多个可以同时推进的工作部分。
总原则如下:
1. 每个工作包只负责一组明确文件。
2. 每个工作包先建新目录和 façade再迁真实调用再清旧层。
3. 同一阶段允许并行,但禁止多人同时大改同一热点主文件。
## 8.1 工作包 ARPG 命名规范与目录骨架
### 目标
先建立 RPG 进入游戏链的新命名与目录落点。
### 负责范围
1. `src/components/rpg-entry/`
2. `src/components/rpg-runtime-shell/`
3. `src/components/rpg-runtime-panels/`
4. `src/hooks/rpg-session/`
5. `src/hooks/rpg-runtime-story/`
6. `src/services/rpg-entry/`
7. `src/services/rpg-runtime/`
8. `server-node/src/routes/rpg-*/`
9. `server-node/src/modules/rpg-runtime-story/`
10. `server-node/src/repositories/rpg-*/`
### 写入边界
1. 只建目录、入口 façade、导出索引和基础命名规范。
2. 不负责大规模迁移老逻辑。
### 前置依赖
无,可立即开始。
## 8.2 工作包 B前端 RPG 入口壳层拆分
### 目标
把平台首页/详情页/继续游戏/进入世界从 `PreGameSelectionFlow.tsx` 拆出来。
### 负责范围
1. `PreGameSelectionFlow.tsx`
2. `PlatformHomeView.tsx`
3. `PlatformWorldDetailView.tsx`
4. `CharacterSelectionFlow.tsx`
5. 入口阶段相关新 hooks
### 写入边界
1. 只改前端入口链。
2. 不改后端接口语义。
3. 不拆运行态冒险面板。
4. 不修改任何前端交互界面设计。
### 前置依赖
依赖工作包 A 的目录骨架。
## 8.3 工作包 C前端 session/bootstrap/persistence 拆分
### 目标
`useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts` 收成 RPG session 域。
### 负责范围
1. `useGameFlow.ts`
2. `useGamePersistence.ts`
3. `useGameShellRuntime.ts`
4. `storageService.ts` 中 snapshot/save archive 相关调用入口
### 写入边界
1. 主要改 session bootstrap、自动存档、继续游戏恢复。
2. 不改 AdventurePanel UI。
3. 不改后端 storyActionService 语义。
4. 不修改任何前端交互界面设计。
### 前置依赖
依赖工作包 A可与 B、D、F、G、H 并行。
## 8.4 工作包 D前端运行态 shell 与面板拆分
### 目标
`GameShellRuntime.tsx``GameShellMainContent.tsx``GameShellStoryPanels.tsx``AdventurePanel.tsx` 拆成 RPG runtime shell 体系。
### 负责范围
1. 运行态 shell
2. 主阶段路由器
3. 面板路由器
4. 冒险主面板 section
### 写入边界
1. 主改组件与 view model。
2. 不改 runtime story 后端协议。
3. 不改平台入口链。
4. 不修改任何前端交互界面设计。
### 前置依赖
依赖工作包 A可与 B、C、E、F、G、H 并行。
## 8.5 工作包 E前端 runtime story 与 NPC 交互链拆分
### 目标
把多层 story coordinator 收成稳定的 RPG runtime story 结构。
### 负责范围
1. `useStoryGeneration.ts`
2. `useStoryRuntimeController.ts`
3. `useStoryFlowCoordinator.ts`
4. `useStoryGoalSessionCoordinator.ts`
5. `useStoryInteractionCoordinator.ts`
6. `npcEncounterActions.ts`
7. `runtimeStoryCoordinator.ts`
8. `runtimeStoryService.ts`
### 写入边界
1. 只改前端 runtime story 主链。
2. 不拆平台首页和世界详情壳层。
3. 不直接修改后端动作语义。
4. 不修改任何前端交互界面设计。
### 前置依赖
依赖工作包 A可与 B、C、D、F、G、H 并行。
## 8.6 工作包 F后端 route 边界拆分
### 目标
`runtimeRoutes.ts``storyActionRoutes.ts` 按 RPG 域拆边界。
### 负责范围
1. `app.ts`
2. `runtimeRoutes.ts`
3. `storyActionRoutes.ts`
4.`rpgProfileRoutes.ts`
5.`rpgEntry...Routes.ts`
6.`rpgRuntimeStoryRoutes.ts`
### 写入边界
1. 只调整路由组织和 façade。
2. 不重写下游全部 service 逻辑。
### 前置依赖
依赖工作包 A可与 B、C、D、E、G、H 并行。
## 8.7 工作包 G后端 runtime session / action service 拆分
### 目标
`storyActionService.ts``runtimeSession.ts` 目录化拆开。
### 负责范围
1. `storyActionService.ts`
2. `runtimeSession.ts`
3. `npcInteractionService.ts`
4. `questStoryActionService.ts`
5. 新 action service / compiler / adapter 文件
### 写入边界
1. 主改后端运行时 story 主链。
2. 不改前端入口与 UI。
3. 不负责仓储拆分。
### 前置依赖
依赖工作包 A可与 B、C、D、E、F、H 并行。
## 8.8 工作包 H仓储、契约与测试基建
### 目标
把 snapshot/profile/library 仓储与 shared contract 收成独立层,并补齐测试。
### 负责范围
1. `runtimeRepository.ts`
2. `runtimeSnapshotHydration.ts`
3. `packages/shared/src/contracts/story.ts`
4. runtime story / snapshot / continue game 相关测试
### 写入边界
1. 只负责仓储、契约、fixture、测试。
2. 不直接改前端 UI。
3. 不重写 route 层。
### 前置依赖
依赖工作包 A可与 B、C、D、E、F、G 并行。
## 8.9 推荐并行顺序
```text
第一批并行:
工作包 A
第二批并行:
工作包 B + 工作包 C + 工作包 D + 工作包 E + 工作包 F + 工作包 G + 工作包 H
第三批收口:
把 B~H 的 façade 接回主链
-> 联调继续游戏 / 开始游戏 / 角色选择 / 冒险 runtime / 存档恢复
-> 清理旧命名与兼容导出
```
## 8.10 并行协作约束
1. 工作包 B 独占 `PreGameSelectionFlow.tsx``PlatformHomeView.tsx``PlatformWorldDetailView.tsx``CharacterSelectionFlow.tsx`
2. 工作包 C 独占 `useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts`
3. 工作包 D 独占 `GameShellRuntime.tsx``GameShellMainContent.tsx``GameShellStoryPanels.tsx``AdventurePanel.tsx`
4. 工作包 E 独占 story runtime hooks 与 `runtimeStoryService.ts`
5. 工作包 F 独占 `runtimeRoutes.ts``storyActionRoutes.ts``app.ts`
6. 工作包 G 独占 `storyActionService.ts``runtimeSession.ts` 与相关 runtime story modules。
7. 工作包 H 独占 `runtimeRepository.ts``runtimeSnapshotHydration.ts` 与 shared runtime story contracts。
---
## 9. 分阶段落地计划
## Phase 0冻结命名与边界口径
### 目标
先冻结“RPG 入口链 / RPG session / RPG runtime / RPG runtime story / RPG profile”五类命名边界。
### 验收标准
1. 后续新增文件不再继续进入 `GameShell*``runtimeRoutes.ts``storyActionService.ts` 这类旧热点。
2. 团队对文件落点和命名根达成一致。
3. 团队对“脚本重构不改前端交互界面设计”的冻结边界达成一致。
## Phase 1目录骨架与前端入口拆分
### 目标
先建立目录骨架,并把进入游戏前的前端壳层从大文件中拆开。
### 验收标准
1. 平台首页/详情页/选角能落到 `rpgEntry` 目录。
2. `PreGameSelectionFlow.tsx` 退化为兼容壳层或 façade。
3. 页面视觉结构与交互方式和当前线上版本保持一致。
## Phase 2session/bootstrap/persistence 收口
### 目标
把“新开局/继续游戏/自动存档/恢复 runtime story”从组件与面板层抽离。
### 验收标准
1. `useGameFlow.ts``useGamePersistence.ts` 不再是直接主入口命名。
2. 继续游戏与自动存档都能走独立 session hook。
3. 用户看到的开始游戏/继续游戏交互顺序不变。
## Phase 3运行态 shell 与 runtime story 主链拆分
### 目标
把冒险运行态 UI 壳层与 runtime story 协调层分别重构。
### 验收标准
1. `AdventurePanel.tsx` 不再是大一统主热点。
2. 前端 runtime story 主链收成 3~4 个稳定层级,而不是多层 coordinator 套娃。
3. 冒险态界面布局、tab 交互与 overlay 呈现方式不变。
## Phase 4后端 route / service / compiler / repository 目录化
### 目标
把后端“大 runtime route + 大 storyActionService + 大 runtimeSession + 大 runtimeRepository”按 RPG 域拆开。
### 验收标准
1. `runtimeRoutes.ts` 退化为 façade 或被拆空。
2. `storyActionService.ts` 只保留兼容导出。
3. `runtimeSession.ts` 只保留 façade 或兼容导出。
4. `runtimeRepository.ts` 不再承载全部 RPG 资料与快照职责。
5. 前端交互界面设计在整个后端重构阶段保持零变更。
## Phase 5兼容层清理
### 目标
在主链稳定后清理旧命名与旧 façade。
### 验收标准
1. 进入游戏主链只剩 RPG 专属命名文件。
2. `GameShell*``runtimeRoutes.ts` 不再是主链真实入口。
3. 文档、契约、测试口径一致。
---
## 10. 验收标准
本次重构方案最终要达成以下结果:
1. 从“平台首页/详情页开始游戏”到“进入 RPG 运行态”的脚本链能一眼看出前端入口、session、runtime、story 的层级。
2. 进入游戏主链不再依赖过于泛化的 `GameShell / runtime / flow` 命名。
3. 前端自动存档、继续游戏恢复、runtime story server gateway 具备独立文件边界。
4. 后端 route、runtime story service、session compiler、snapshot repository 分层清晰。
5. 所有新主文件都以 RPG 域命名,不再继续扩大旧热点文件。
6. 整个重构过程不改变任何前端交互界面设计,用户可见交互保持一致。
---
## 11. 结论
当前 RPG 进入游戏后的主玩法链,真正的问题不是单点 bug而是
**平台入口、会话初始化、运行态壳层、前端 runtime story 协调链、后端 route 总入口、后端 runtime session 编译器和仓储层同时过重,而且命名没有体现“这是 RPG 专属主链”。**
后续正确的重构方向不是继续在 `GameShell``runtimeRoutes.ts``storyActionService.ts` 这些热点文件上打补丁,而是把主链收成:
**RPG 入口域 -> RPG session 域 -> RPG runtime shell 域 -> RPG runtime story 域 -> RPG snapshot / profile 域**
只有这样,这条“开始游戏 -> 进入运行态 -> 推进剧情 -> 自动存档 -> 继续游戏”的链路,才会真正具备后续可读、可扩、可并行维护的工程形态。

View File

@@ -0,0 +1,107 @@
# RPG 进入游戏与运行时链路旧脚本删除收口记录
更新时间:`2026-04-21`
## 1. 本次目标
本轮继续按 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 收口,目标只有一个:
**在新 `rpg-entry`、`rpg-session`、`rpg-runtime-shell`、`rpg-runtime-panels`、`rpg-runtime-story` 主链已经接回后,物理删除旧 RPG 入口与运行态脚本,并确认源码和当前后端构建产物不再依赖旧路径。**
本轮不改 UI 布局、不改按钮位置、不改 tab 组织、不改弹层交互方式,也不把历史审计文档里的旧文件名当成运行时代码依赖处理。
## 2. 已删除旧脚本范围
前端旧入口与旧兼容层已从源码中移除:
1. `src/components/game-shell/`
2. `src/components/game-shell/rpg-creation-flow/`
3. `src/components/AdventurePanel.tsx`
4. `src/hooks/story/`
5. `src/hooks/useGameFlow.ts`
6. `src/hooks/useGamePersistence.ts`
7. `src/hooks/useGameShellRuntime.ts`
8. `src/hooks/useStoryGeneration.ts`
9. `src/services/runtimeStoryService.ts`
10. `src/services/storageService.ts`
后端与共享契约旧入口已从源码中移除:
1. `server-node/src/routes/runtimeRoutes.ts`
2. `server-node/src/modules/story/runtimeSession.ts`
3. `server-node/src/modules/story/storyActionRoutes.ts`
4. `server-node/src/modules/story/storyActionService.ts`
5. `packages/shared/src/contracts/story.ts`
## 3. 新主链落点
删除旧脚本后RPG 主链只允许继续落在以下新域:
1. 前端入口:`src/components/rpg-entry/`
2. 前端 session`src/hooks/rpg-session/`
3. 前端运行态 shell`src/components/rpg-runtime-shell/`
4. 前端运行态面板:`src/components/rpg-runtime-panels/`
5. 前端 runtime story`src/hooks/rpg-runtime-story/`
6. 前端 client`src/services/rpg-entry/``src/services/rpg-runtime/`
7. 后端 route`server-node/src/routes/rpg-entry/``server-node/src/routes/rpg-profile/``server-node/src/routes/rpg-runtime/`
8. 后端 runtime story`server-node/src/modules/rpg-runtime-story/`
9. 后端仓储:`server-node/src/repositories/rpg-entry/``server-node/src/repositories/rpg-profile/``server-node/src/repositories/rpg-runtime/`
10. 共享契约:`packages/shared/src/contracts/rpgRuntimeStoryAction.ts``packages/shared/src/contracts/rpgRuntimeStoryState.ts``packages/shared/src/contracts/rpgRuntimeChat.ts``packages/shared/src/contracts/rpgRuntimeQuestAssist.ts`
## 4. 残留依赖检查
本轮使用旧路径和旧入口名扫描源码与当前后端构建产物,确认以下路径不存在且没有运行时代码引用:
1. `src/components/game-shell`
2. `src/hooks/story`
3. `src/hooks/useGameFlow.ts`
4. `src/hooks/useGamePersistence.ts`
5. `src/hooks/useGameShellRuntime.ts`
6. `src/hooks/useStoryGeneration.ts`
7. `src/services/runtimeStoryService.ts`
8. `src/services/storageService.ts`
9. `server-node/src/routes/runtimeRoutes.ts`
10. `server-node/src/modules/story/runtimeSession.ts`
11. `server-node/src/modules/story/storyActionRoutes.ts`
12. `server-node/src/modules/story/storyActionService.ts`
13. `packages/shared/src/contracts/story.ts`
补充处理:
1. `npm --prefix server-node run build` 已重新生成当前 `server-node/dist/server.cjs`
2. 旧的忽略产物 `server-node/dist/server.js``server-node/dist/server.js.map``2026-04-18` 遗留 bundle仍包含旧路径 sourcemap本轮已定点删除避免本地误跑旧构建。
3. 当前 `server-node/dist/` 只保留 `server.cjs``server.cjs.map`,旧主入口路径扫描无命中。
## 5. 本轮补丁
本轮额外修正了迁移后测试 prop 类型不一致的问题:
1. `src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx`
2. `src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx`
`npcChatQuestOfferUi.replacePendingOffer` 在新面板类型中是同步布尔返回,测试 mock 已从 `async () => false` 改为 `() => false`,避免旧异步 mock 继续伪装成兼容层行为。
## 6. 验证结果
已通过:
1. `npm run check:encoding`
2. `npm --prefix server-node run build`
3. `npx vitest run src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx src/components/rpg-runtime-shell/useRpgRuntimeShellViewModel.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeStoryState.test.ts src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.test.ts`
定向前端回归结果:`8` 个测试文件、`37` 个测试通过。
未完全通过但不属于本轮 RPG 旧脚本删除阻塞:
1. `npm run typecheck`
- 剩余错误集中在 `packages/shared/src/contracts/rpgCreationFixtures.ts``src/components/auth/AccountModal.test.tsx``src/data/customWorldLibrary.ts``src/services/customWorldCover.test.ts`
- 本轮已清掉 `RpgAdventurePanel*.test.tsx` 中与迁移相关的 `Promise<boolean>` 类型错误。
2. `npm --prefix server-node run test`
- RPG runtime story、RPG entry save、RPG world library、RPG profile route 等相关测试通过。
- 剩余失败为 `custom world agent` HTTP 相关 `12` 个用例返回 `500 !== 200`,属于创作链/Agent 链路问题,不属于本轮 RPG 旧脚本删除范围。
## 7. 结论
当前 RPG 进入游戏与运行时主链已经不再依赖旧 `GameShell``useGame*``useStoryGeneration``hooks/story``runtimeStoryService``runtimeRoutes``modules/story/*``contracts/story.ts` 脚本。
后续如果继续开发 RPG 入口、运行态、runtime story 或存档恢复,只应扩展新 `rpg-*` 目录与分文件契约,不应重新创建旧路径 façade。

View File

@@ -0,0 +1,132 @@
# RPG 进入游戏与运行时链路重构第一批第二批并行工作复核记录
更新时间:`2026-04-21`
## 1. 复核目标
本次复核只做一件事:
**对照 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`,检查第一批并行与第二批并行工作是否存在遗漏、未完成收口或仍停留在“看起来已完成但工程状态未闭合”的问题。**
执行边界保持如下:
1. 只检查工作包 A 到 H 是否达到各自文档声明的落地状态。
2. 只补齐确认属于本次重构范围的遗漏项。
3. 不顺手扩展 UI、玩法、协议或无关模块。
## 2. 复核范围
本次逐项核对了以下内容:
1. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
2. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_A_PROGRESS_2026-04-21.md`
3. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_B_PROGRESS_2026-04-21.md`
4. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_C_PROGRESS_2026-04-21.md`
5. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_D_PROGRESS_2026-04-21.md`
6. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_E_PROGRESS_2026-04-21.md`
7. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_F_PROGRESS_2026-04-21.md`
8. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_G_PROGRESS_2026-04-21.md`
9. `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_WORK_PACKAGE_H_PROGRESS_2026-04-21.md`
10. 前端 `rpg-entry``rpg-session``rpg-runtime-shell``rpg-runtime-panels``rpg-runtime-story` 新域真实实现
11. 后端 `routes/rpg-*``modules/rpg-runtime-story``repositories/rpg-*` 新域真实实现
12. shared contract 分文件与兼容 façade
13. 与工作包 E / F / G / H 直接相关的定向测试与工作树状态
## 3. 复核结论
## 3.1 第一批并行结论
第一批并行只包含工作包 A。
复核结果:
1. 工作包 A 要求的目录骨架、façade、barrel 与按域命名落点已经建立。
2. 工作包 A 没有发现需要继续补做的编码遗漏。
3. 当前 A 的剩余内容本来就属于后续工作包 B 到 H 的真实迁移,不属于遗漏。
结论:
**第一批并行无新增遗漏项。**
## 3.2 第二批并行总体结论
对照工作包 B 到 H 的进度文档与实际主链代码,当前状态如下:
1. 工作包 B`rpg-entry` 已承接真实入口实现,旧 `game-shell` / `rpg-creation-flow` 路径已降级为兼容层。
2. 工作包 C`rpg-session` 已承接真实 session / persistence 主链,`storageService.ts` 已退化为兼容转发层。
3. 工作包 D`rpg-runtime-shell``rpg-runtime-panels` 已承接真实实现,旧 `GameShell*` / `AdventurePanel` 已降级为兼容入口。
4. 工作包 E前端 runtime story 主链已迁入 `rpg-runtime-story`,但测试工作树存在未收口的合流状态。
5. 工作包 F`app.ts` 已按 `rpgProfile / rpgEntry / rpgRuntimeStory / rpgRuntimeAiAssist` 挂载新域路由,旧 `runtimeRoutes.ts` 已降级。
6. 工作包 G后端 runtime story action / state / session 真实实现已迁入 `modules/rpg-runtime-story`,旧热点已退化为兼容导出。
7. 工作包 H仓储、shared contract 与定向测试基建已落到新域命名与分文件结构。
结论:
**第二批并行的主链改造基本已经完成,本轮确认到的真实遗漏主要集中在“工程收口状态”而不是“功能未落地”。**
## 4. 本轮确认的遗漏项
## 4.1 工作包 E 相关测试文件仍处于未解决合流状态
复核时发现:
1. `src/hooks/story/npcEncounterActions.test.ts` 的文件内容已经切到 `../rpg-runtime-story/rpgRuntimeStoryGateway` 新路径。
2. 但 Git 索引仍把该文件标记为 `UU`,说明这份并行工作没有真正完成收口。
这会导致:
1. 工作包 E 虽然逻辑上已完成迁移,但工程状态仍不能算完全闭合。
2. 后续继续判断第二批并行是否完成时,会被未解决合流状态误判为仍未完成。
## 4.2 后端总测试文件仍保留同一轮并行改动的未解决合流状态
复核时发现:
1. `server-node/src/app.test.ts` 仍处于 `UU` 状态。
2. 该文件中的两侧改动并不冲突:
- 一侧是把微信登录测试的 cookie 解析统一改成 `readCookieValue(...)`
- 另一侧是补充 custom world agent SSE enriched session 回归测试
3. 当前文件内容已经同时包含两侧结果,但索引没有完成正式收口。
4. 合并后还残留了一个未被使用的 `accessCookie` 中间变量,属于典型的合流尾巴。
这同样属于:
**不是功能缺失,而是并行工作未彻底收口。**
## 4.3 技术文档索引未完整登记本轮工作包文档
复核时发现:
1. `docs/technical/README.md` 已登记 A、D、E、F、G、H 的进度文档。
2. 但缺少工作包 B 与工作包 C 的进度文档入口。
3. 这会让后续按文档索引回溯第二批并行工作时出现“文档已存在但目录索引缺失”的信息断层。
这属于文档收口遗漏,应当在本轮一并补齐。
## 5. 本轮补齐动作
本轮只补以下缺口:
1. 清理 `server-node/src/app.test.ts` 中合流后残留的未使用变量,保留两侧都应保留的测试结果。
2.`src/hooks/story/npcEncounterActions.test.ts` 作为工作包 E 已确认的正确内容进行正式收口。
3.`server-node/src/app.test.ts` 作为同轮并行改动的正确合并结果进行正式收口。
4. 更新 `docs/technical/README.md`,补齐工作包 B、工作包 C 与本复核文档入口。
## 6. 本轮明确没有做的事
为了避免过度开发,本轮没有继续做以下事情:
1. 没有继续拆 UI 组件。
2. 没有继续清理旧兼容 façade。
3. 没有额外重构任何 route / service / repository。
4. 没有调整任何前端交互界面设计。
5. 没有顺手处理与本次 RPG 并行重构无直接关系的其他噪音文件。
## 7. 复核后状态结论
在本轮补齐之后,可以对第一批与第二批并行工作给出如下结论:
1. 第一批并行没有发现新增遗漏。
2. 第二批并行的主链功能改造已经基本齐备。
3. 本轮确认并补齐的遗漏主要是测试合流收口与文档索引缺口。
4. 当前不需要继续扩大改造范围,后续可以按执行计划进入第三批统一收口。

View File

@@ -0,0 +1,191 @@
# RPG 进入游戏与运行时链路重构第三批收口记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **第三批收口**,严格遵守以下边界:
1. 把第二批工作包 B 到 H 已完成的新域 façade 真正接回主链。
2. 对照执行计划逐项检查“开始游戏 / 继续游戏 / 角色选择 / 冒险 runtime / 存档恢复”是否仍残留旧命名真实依赖。
3. 只清理确认属于 RPG 主链的新域反向依赖与未接线仓储不扩到创作链、UI 设计或玩法语义。
## 2. 本次完成的第三批收口
## 2.1 前端运行态主链不再反向依赖旧 `GameShell` 热点
本轮把以下真实实现从旧 `game-shell` 命名下摘回 `rpg-runtime-shell`
1. `src/components/rpg-runtime-shell/RpgRuntimeCanvasStage.tsx`
2. `src/components/rpg-runtime-shell/rpgRuntimeLoaders.tsx`
3. `src/components/rpg-runtime-shell/useRpgSceneTransitionModel.ts`
同时完成以下主链接线:
1. `src/components/rpg-runtime-shell/RpgRuntimeShell.tsx` 直接使用 `RpgRuntimeCanvasStage`
2. `src/components/rpg-runtime-shell/RpgRuntimeOverlayHost.tsx` 直接使用 `rpgRuntimeLoaders`
3. `src/components/rpg-runtime-shell/useRpgRuntimeShellViewModel.ts` 直接使用 `useRpgSceneTransitionModel`
4. `src/components/rpg-runtime-panels/RpgRuntimePanelRouter.tsx` 直接从 `rpg-runtime-shell` 读取 `PanelLoadingFallback`
当前结果:
1. 运行态主链真实实现已经不再依赖旧 `GameShellCanvasStage``GameShellLoaders``useSceneTransitionModel`
2.`game-shell` 文件只保留兼容 re-export不再作为 RPG 运行态真实落点。
## 2.2 前端入口域主链不再反向依赖旧平台展示 helper
本轮把以下入口域通用展示能力收回 `rpg-entry`
1. `src/components/rpg-entry/RpgEntryBrandLogo.tsx`
2. `src/components/rpg-entry/rpgEntryWorldPresentation.ts`
3. `src/components/rpg-entry/RpgEntryCreationTypeModal.tsx`
并完成以下主链接线:
1. `RpgEntryHomeView.tsx` 改为直接使用 `RpgEntryBrandLogo``rpgEntryWorldPresentation`
2. `RpgEntryWorldDetailView.tsx` 改为直接使用 `rpgEntryWorldPresentation`
3. `RpgEntryFlowShellImpl.tsx` 改为直接使用 `RpgEntryCreationTypeModal`
当前结果:
1. `rpg-entry` 真实实现不再依赖旧 `PlatformBrandLogo``platformWorldPresentation``PlatformCreationTypeModal`
2.`game-shell` 对应文件只保留兼容导出。
## 2.3 工作包 H 新仓储已真正接回后端 RPG 主链
本轮把工作包 H 中已建立但尚未接回主链的新仓储正式注入 `AppContext`
1. `rpgProfileDashboardRepository`
2. `rpgBrowseHistoryRepository`
3. `rpgSaveArchiveRepository`
4. `rpgRuntimeSnapshotRepository`
并完成以下主链接线:
1. `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts`
- 资料看板、钱包流水、游玩统计走 `rpgProfileDashboardRepository`
- 浏览历史走 `rpgBrowseHistoryRepository`
- 设置读写走 `rpgProfileDashboardRepository`
2. `server-node/src/routes/rpg-entry/rpgEntrySaveRoutes.ts`
- snapshot 读写删除走 `rpgRuntimeSnapshotRepository`
- save archive 列表与恢复走 `rpgSaveArchiveRepository`
3. `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts`
- runtime story 状态读取与动作结算走 `rpgRuntimeSnapshotRepository`
当前结果:
1. `rpgProfile``rpgEntrySave``rpgRuntimeStory` 主链已经不再直接把大 `runtimeRepository` 当作唯一注入边界。
2. 工作包 H 的新仓储不再停留在“命名骨架已存在但主链未接线”的状态。
## 2.4 新域 shared contract 进一步脱离旧 `story.ts` façade
本轮继续把 RPG 新主链中的 shared contract 直接切到分文件:
1. `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts`
- 改为直接使用 `rpgRuntimeStoryState.ts`
2. `server-node/src/routes/rpg-runtime/rpgRuntimeAiAssistRoutes.ts`
- 改为直接使用 `rpgRuntimeChat.ts``rpgRuntimeQuestAssist.ts`
3. `server-node/src/modules/rpg-runtime-story/RpgRuntimeSessionDomain.ts`
- 改为直接使用 `rpgRuntimeStoryAction.ts`
- snapshot 类型改为使用 `RpgRuntimeSavedSnapshot`
4. `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts`
- runtime story request/response 类型改为直接使用 `rpgRuntimeStoryState.ts`
- snapshot 仓储端口改为使用 `RpgRuntimeSnapshotRepositoryPort`
当前结果:
1. RPG 新域主链已经显著减少对旧 `packages/shared/src/contracts/story.ts` façade 的反向依赖。
2. `story.ts` 继续保留兼容职责,但不再是第三批已收口主链的真实首选入口。
## 2.5 补齐世界库主链接线遗漏
代码级复核后,本轮额外补齐了第三批范围内一个真实遗漏:世界库主链虽然已经有 `rpg-entry` 路由和 `RpgWorldLibraryRepository`,但此前并未完整接回主链。
本次补齐包括:
1. `server-node/src/context.ts``server-node/src/server.ts`
- 正式注入 `rpgWorldLibraryRepository`
2. `server-node/src/routes/rpg-entry/rpgWorldLibraryRoutes.ts`
- 作品库/广场/详情/发布/下架/删除等读写改为直接走 `rpgWorldLibraryRepository`
- `rpgWorldProfileRepository` 继续保留给 Agent 发布链与发布服务使用
3. `src/services/rpg-entry/rpgEntryLibraryClient.ts`
- 改为直接承接 `/api/runtime/custom-world-library``/api/runtime/custom-world-gallery` 请求
- 不再反向依赖旧 `storageService.ts` 兼容层
4. `src/services/rpg-entry/rpgEntryLibraryClient.test.ts`
- 补齐入口世界库 client 的定向回归
当前结果:
1. “平台首页 / 世界详情 / 开始游戏”所依赖的世界库链路,现在已经和 save/profile 一样真正回到 `rpg-entry` + `rpg-entry repository` 主链。
2. 第三批范围内不再残留“新命名已建好,但真实主链仍穿旧兼容层/旧仓储”的世界库遗漏。
## 3. 老代码物理删除补充
根据后续收口要求,本轮在主链稳定后继续删除旧兼容层,不再让 RPG 入口与运行态链路通过旧脚本名兜底。
已删除的旧前端入口包括:
1. `src/components/AdventurePanel.tsx`
2. `src/components/game-shell/*`
3. `src/components/game-shell/rpg-creation-flow/*`
4. `src/hooks/useGameFlow.ts`
5. `src/hooks/useGamePersistence.ts`
6. `src/hooks/useGameShellRuntime.ts`
7. `src/hooks/useStoryGeneration.ts`
8. `src/services/runtimeStoryService.ts`
9. `src/services/storageService.ts`
已删除的旧后端与共享契约入口包括:
1. `server-node/src/routes/runtimeRoutes.ts`
2. `server-node/src/modules/story/runtimeSession.ts`
3. `server-node/src/modules/story/storyActionService.ts`
4. `server-node/src/modules/story/storyActionRoutes.ts`
5. `packages/shared/src/contracts/story.ts`
同步完成的主链迁移包括:
1. `RpgEntryFlowShellImpl.tsx` 直接使用 `src/components/rpg-entry/useRpgCreation*` hooks不再反向依赖 `game-shell/rpg-creation-flow`
2. runtime story client 测试迁到 `src/services/rpg-runtime/rpgRuntimeStoryClient.test.ts`
3. profile / world library 路由测试迁到 `src/services/rpg-entry/rpgEntryClients.routing.test.ts`
4. 冒险面板测试迁到 `src/components/rpg-runtime-panels/RpgAdventurePanel*.test.tsx`
5. `AdventurePanelOverlays` 子模块改名并迁到 `src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx`
6. 后端与前端 shared contract import 已切到 `rpgRuntimeChat.ts``rpgRuntimeQuestAssist.ts``rpgRuntimeStoryAction.ts``rpgRuntimeStoryState.ts`
本轮仍然刻意没有继续扩大到以下内容:
1. 没有重命名历史审计、旧 PRD、旧技术方案中作为历史记录出现的旧文件名。
2. 没有继续拆分 `runtimeRepository.ts` 剩余实现。
3. 没有改 UI 布局、按钮位置、tab 组织、弹层方式或任何用户可见交互设计。
4. 没有清理与本执行计划无关的创作链旧文件;只删除 RPG 入口 / 运行态链路已经不再依赖的旧脚本。
## 4. 本轮检查后确认的未扩范围
为了避免过度开发,本轮明确没有继续扩到以下内容:
1. 没有继续改 UI 布局、按钮位置、tab 组织、弹层方式或任何用户可见交互设计。
2. 没有继续拆分 `runtimeRepository.ts` 剩余实现,也没有扩大到更多仓储接口重命名。
3. 没有顺手清理历史文档里的旧文件名引用,只更新本次执行收口文档。
## 5. 第三批遗漏复核结论
对照执行计划中的第三批要求,本轮确认如下:
1. **把 B 到 H 的 façade 接回主链**:已完成
- 前端 `rpg-entry``rpg-runtime-shell``rpg-runtime-panels`
- 后端 `rpg-profile``rpg-entry-save``rpg-runtime-story`
2. **联调继续游戏 / 开始游戏 / 角色选择 / 冒险 runtime / 存档恢复**:本轮已把这些链路上的主注入边界和真实落点接回新域
3. **清理旧命名与兼容导出**:已完成主链级清理,并按本次补充要求物理删除旧兼容脚本
当前仍保留但不计为遗漏的部分:
1. 历史审计、旧 PRD、旧技术方案中仍会提到旧文件名这是历史记录不代表运行时代码依赖。
2. `src/components/rpg-runtime-panels/RpgAdventurePanel.tsx` 仍保持原交互结构,只做脚本路径与命名收口。
## 6. 结论
在“不改 UI、不改玩法、不扩到创作链执行方案”的前提下本轮已经完成 RPG 执行计划中的第三批主链收口:
1. 新域真实实现已经接回前后端主链。
2. RPG 主链对旧 `GameShell` / `runtimeRoutes` / `story.ts` façade 的真实依赖已经删除。
3. 复核中发现并补齐了世界库主链接线遗漏后,当前没有再发现属于本次第三批范围、且必须继续补做的遗漏项。

View File

@@ -0,0 +1,101 @@
# RPG 进入游戏与运行时链路重构工作包 A 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 ARPG 命名规范与目录骨架**,严格遵守以下边界:
1. 先建立 RPG 进入游戏链的新目录与命名落点。
2. 先补 façade、barrel、path 常量和按域仓储入口,不提前迁移主流程逻辑。
3. 不修改现有前端交互界面设计,不提前实现工作包 B 到 H 的真实拆分。
## 2. 本次已落地内容
## 2.1 前端目录骨架
已新增以下前端目录与兼容 façade
1. `src/components/rpg-entry/`
2. `src/components/rpg-runtime-shell/`
3. `src/components/rpg-runtime-panels/`
4. `src/hooks/rpg-session/`
5. `src/hooks/rpg-runtime-story/`
6. `src/services/rpg-entry/`
7. `src/services/rpg-runtime/`
当前策略:
1. `RpgEntryFlowShell` 先桥接当前真实存在的 `src/components/game-shell/rpg-creation-flow/RpgCreationShell.tsx`,不再依赖已删除的旧 `PreGameSelectionFlow.tsx`
2. `RpgEntryHomeView``RpgEntryWorldDetailView``RpgEntryCharacterSelectView` 继续桥接旧平台首页、世界详情和选角视图。
3. `RpgRuntimeShell``RpgRuntimeStageRouter``RpgRuntimePanelRouter``RpgAdventurePanel``RpgRuntimeOverlayHost` 继续桥接旧 `GameShell*``AdventurePanel` 组件。
4. `useRpgSessionBootstrap``useRpgSessionPersistence``useRpgRuntimeSession` 继续桥接 `useGameFlow``useGamePersistence``useGameShellRuntime`
5. `useRpgRuntimeStory``useRpgRuntimeStoryController``useRpgRuntimeStoryFlow``useRpgRuntimeInteractionFlow``useRpgRuntimeNpcInteraction``rpgRuntimeStoryGateway` 继续桥接旧 story runtime hooks 与 gateway。
## 2.2 前端 service/client 骨架
已新增以下 service/client 落点:
1. `src/services/rpg-entry/rpgEntryLibraryClient.ts`
2. `src/services/rpg-entry/rpgProfileClient.ts`
3. `src/services/rpg-runtime/rpgSnapshotClient.ts`
4. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts`
5. `src/services/rpg-runtime/rpgRuntimeChatClient.ts`
当前策略:
1. `rpgEntryLibraryClient` 继续桥接 `storageService.ts` 中的世界库、世界广场与发布相关接口。
2. `rpgProfileClient` 继续桥接资料看板、浏览历史、设置与继续游戏归档相关接口。
3. `rpgSnapshotClient` 继续桥接快照读写接口。
4. `rpgRuntimeStoryClient` 继续桥接 `/api/runtime/story` 的旧 client。
5. `rpgRuntimeChatClient` 继续桥接 `aiService.ts` 中的角色聊天、NPC 对话与招募对话接口。
## 2.3 后端目录骨架
已新增以下后端目录与 façade
1. `server-node/src/routes/rpg-entry/`
2. `server-node/src/routes/rpg-profile/`
3. `server-node/src/routes/rpg-runtime/`
4. `server-node/src/modules/rpg-runtime-story/`
5. `server-node/src/repositories/rpg-entry/`
6. `server-node/src/repositories/rpg-profile/`
7. `server-node/src/repositories/rpg-runtime/`
当前策略:
1. `createRpgProfileRoutes()``createRpgEntrySaveRoutes()``createRpgWorldLibraryRoutes()``createRpgRuntimeAiAssistRoutes()` 当前只提供空路由骨架与稳定 path 常量。
2. `createRpgRuntimeStoryRoutes()` 继续桥接旧 `createStoryActionRoutes()`,确保 runtime story 路由已经有新命名落点。
3. `RpgRuntimeStoryActionService``RpgRuntimeSessionLoader``RpgRuntimeOptionCompiler``RpgRuntimeSnapshotSync` 继续桥接旧 `storyActionService.ts``runtimeSession.ts`
4. `RpgRuntimeSnapshotRepository``RpgSaveArchiveRepository``RpgProfileDashboardRepository``RpgWorldLibraryRepository` 先以委托 runtimeRepository 的方式建立按域命名仓储入口。
## 3. 本次没有做的事
以下内容仍保持原状,留给后续工作包:
1. 没有改 `GameShellMainContent.tsx``GameShellRuntime.tsx``AdventurePanel.tsx` 的内部实现。
2. 没有拆 `useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts` 的真实逻辑。
3. 没有拆 `useStoryGeneration.ts``npcEncounterActions.ts``runtimeStoryCoordinator.ts` 的内部职责。
4. 没有改 `server-node/src/app.ts``server-node/src/routes/runtimeRoutes.ts``server-node/src/modules/story/storyActionService.ts``server-node/src/modules/story/runtimeSession.ts` 的真实挂载与内部逻辑。
5. 没有补 shared contract 新文件,本轮执行方案的工作包 A 范围未把共享契约骨架列为必做项,因此保持到工作包 H 统一收口。
6. 没有修改任何前端交互界面设计。
## 4. 验证与现状说明
本轮已执行:
1. `npm run check:encoding`
验证结果:
1. 编码检查通过。
2. 全量 `npm run typecheck` 当前未通过,但失败项主要来自工作树中已存在的并行修改与历史类型问题。
3. 本轮已修正新 façade 中对已删除旧入口 `PreGameSelectionFlow.tsx` 的失效导入,当前 `RpgEntryFlowShell` 已改为桥接真实存在的 `rpg-creation-flow` 入口。
## 5. 对后续工作包的直接收益
1. 工作包 B 可以直接把平台入口与选角真实实现落到 `src/components/rpg-entry/`,而不必再次讨论命名与目录。
2. 工作包 C 可以直接把 session/bootstrap/persistence 的调用方迁到 `src/hooks/rpg-session/``src/services/rpg-runtime/`
3. 工作包 D 可以直接让运行态壳层与面板消费 `rpg-runtime-shell``rpg-runtime-panels` 新入口。
4. 工作包 E 可以直接把 story runtime 与 NPC 交互迁到 `src/hooks/rpg-runtime-story/``src/services/rpg-runtime/`
5. 工作包 F、G、H 可以直接基于 `server-node/src/routes/rpg-*``server-node/src/modules/rpg-runtime-story/``server-node/src/repositories/rpg-*/` 继续做真实迁移,而不用重新搭第一层命名骨架。

View File

@@ -0,0 +1,108 @@
# RPG 进入游戏与运行时链路重构工作包 B 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 B前端 RPG 入口壳层拆分**,严格遵守以下边界:
1. 只改前端入口链,不改后端接口语义。
2. 只处理平台首页、世界详情、继续游戏、进入世界、选角相关入口壳层与入口 hooks。
3. 不拆运行态冒险面板,不修改任何前端交互界面设计。
## 2. 本次已完成内容
## 2.1 `rpg-entry` 已成为真实入口目录
以下文件现在承载真实实现,不再只是 façade
1. `src/components/rpg-entry/RpgEntryFlowShell.tsx`
2. `src/components/rpg-entry/RpgEntryFlowShellImpl.tsx`
3. `src/components/rpg-entry/RpgEntryHomeView.tsx`
4. `src/components/rpg-entry/RpgEntryWorldDetailView.tsx`
5. `src/components/rpg-entry/RpgEntryCharacterSelectView.tsx`
6. `src/components/rpg-entry/rpgEntryTypes.ts`
7. `src/components/rpg-entry/rpgEntryShared.ts`
这意味着当前 RPG 平台入口、详情页和选角页的真实物理落点已经从旧 `game-shell` 命名转入 `rpg-entry` 域,满足执行方案里“平台首页/详情页/选角能落到 `rpgEntry` 目录”的验收要求。
## 2.2 入口相关 hooks 已补齐到 `rpg-entry`
本轮新增或迁入以下入口域 hooks
1. `src/components/rpg-entry/useRpgEntryBootstrap.ts`
2. `src/components/rpg-entry/useRpgEntryLibraryDetail.ts`
3. `src/components/rpg-entry/useRpgEntryNavigation.ts`
4. `src/components/rpg-entry/useRpgEntrySaveResume.ts`
5. `src/components/rpg-entry/useRpgEntryCharacterSelect.ts`
其中:
1. `useRpgEntryBootstrap` 负责平台 works / library / gallery / history / save / dashboard 拉取与继续游戏恢复入口。
2. `useRpgEntryLibraryDetail` 负责详情页打开、作品详情读取、继续创作入口、发布/下架/删除动作。
3. `useRpgEntryNavigation` 收口入口阶段跳转,避免壳层里继续散落匿名 `setSelectionStage(...)`
4. `useRpgEntrySaveResume` 明确“继续游戏”动作入口。
5. `useRpgEntryCharacterSelect` 补齐选角页回退与确认动作的入口域命名。
## 2.3 旧 `game-shell` / `rpg-creation-flow` 路径已降级为兼容层
以下旧文件已改为兼容 re-export不再持有真实实现
1. `src/components/game-shell/PlatformHomeView.tsx`
2. `src/components/game-shell/PlatformWorldDetailView.tsx`
3. `src/components/game-shell/CharacterSelectionFlow.tsx`
4. `src/components/game-shell/rpg-creation-flow/RpgCreationShell.tsx`
5. `src/components/game-shell/rpg-creation-flow/RpgCreationShellImpl.tsx`
6. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowTypes.ts`
7. `src/components/game-shell/rpg-creation-flow/rpgCreationFlowShared.ts`
8. `src/components/game-shell/rpg-creation-flow/useRpgCreationPlatformBootstrap.ts`
9. `src/components/game-shell/rpg-creation-flow/useRpgCreationDetailNavigation.ts`
兼容策略如下:
1. 旧命名继续导出,避免并行工作包与现有测试断开。
2. 真实实现统一回落到 `rpg-entry`
3. 本轮不清理 legacy façade只让它们退化成稳定桥接层。
## 2.4 主链调用方已接回 `rpg-entry`
本轮已把主入口相关调用改为直接消费 `rpg-entry`
1. `src/components/game-shell/GameShellMainContent.tsx`
2. `src/components/game-shell/useGameShellViewModel.ts`
3. `src/components/rpg-entry/index.ts`
当前结果:
1. 主阶段路由器 lazy import 已直接走 `RpgEntryFlowShell``RpgEntryCharacterSelectView`
2. `SelectionStage` 类型已从 `rpg-entry` 暴露,不再依赖旧 `rpg-creation-flow` 作为主命名根。
3. 旧路径仍可用,但已经不再是主链真实入口。
## 3. 对照执行方案的完成判断
工作包 B 本轮已完成以下计划项:
1. 已把平台首页/详情页/继续游戏/进入世界从旧 `PreGameSelectionFlow` 所属旧命名体系中收进 `rpg-entry` 域。
2. 已让 `rpg-entry` 成为真实实现目录,而不是只保留 façade。
3. 已补齐入口阶段相关新 hooks。
4. 已让旧路径退化为兼容桥接层。
5. 没有修改任何前端交互界面设计。
当前仍刻意保留的边界:
1. 没有拆运行态冒险面板,这属于工作包 D。
2. 没有改 session/bootstrap/persistence 主逻辑,这属于工作包 C。
3. 没有改后端接口语义与 route/service 边界,这属于工作包 F/G/H。
4. 没有提前清理所有 legacy re-export本轮以稳定主链为先。
## 4. 验证与遗漏核查
本轮需要重点核查的遗漏项已经逐项确认:
1. `RpgEntryFlowShell` 不再桥接旧实现,已直接使用 `rpg-entry` 目录下的真实壳层。
2. `PlatformHomeView``PlatformWorldDetailView``CharacterSelectionFlow` 的旧路径已不再持有真实实现。
3. `GameShellMainContent` 已直接 lazy import `rpg-entry` 新入口。
4. `SelectionStage` 已从 `rpg-entry` 作为主出口暴露。
5. 复核时补齐了一个第二批收口遗漏:`RpgEntryFlowShellImpl``useRpgEntryBootstrap``useRpgEntryLibraryDetail` 这些 `rpg-entry` 真实实现已改为直接消费 `rpg-entry` 新域 client不再反向依赖旧 `storageService.ts` 作为主链入口。
本轮未执行全量 `typecheck`,原因是工作树存在并行改动与未解决冲突文件;但已把本工作包范围内最容易遗漏的主链接线点和兼容层路径全部补齐。

View File

@@ -0,0 +1,115 @@
# RPG 进入游戏与运行时链路重构工作包 C 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 C前端 session / bootstrap / persistence 拆分**,严格遵守以下边界:
1.`useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts` 的真实实现收进 `rpg-session` 域。
2. 把 snapshot / save archive 相关 client 从 `storageService.ts` 抽到 `src/services/rpg-runtime/``src/services/rpg-entry/`
3. 旧文件只保留兼容导出,不再继续承载主实现。
4. 不修改 AdventurePanel UI不修改后端 story 动作语义,不修改任何前端交互界面设计。
## 2. 本次已落地内容
## 2.1 `rpg-session` 主实现已承接 session 链
本轮已把以下真实实现迁入新域目录:
1. `src/hooks/rpg-session/useRpgSessionBootstrap.ts`
2. `src/hooks/rpg-session/useRpgSessionPersistence.ts`
3. `src/hooks/rpg-session/useRpgRuntimeSession.ts`
4. `src/hooks/rpg-session/rpgSessionTypes.ts`
落地结果:
1. `useRpgSessionBootstrap` 现在直接承载世界选择、角色确认、新开局 `GameState` 初始化逻辑。
2. `useRpgSessionPersistence` 现在直接承载自动存档、继续游戏恢复、远端快照拉取与 runtime story 恢复刷新。
3. `useRpgRuntimeSession` 现在直接组合 bootstrap / persistence / story / combat / companion 等链路,成为主运行态装配入口。
## 2.2 旧 `useGame*` 文件已退化为兼容 façade
以下旧文件已不再承载主实现,只保留兼容导出:
1. `src/hooks/useGameFlow.ts`
2. `src/hooks/useGamePersistence.ts`
3. `src/hooks/useGameShellRuntime.ts`
当前策略:
1. 旧调用方仍可继续工作,避免影响并行工作包。
2. 正式主入口已经切到 `useRpgRuntimeSession`
3. `BottomTab` 类型已从 `rpg-session` 域提供,避免继续绑在旧 hook 文件上。
## 2.3 snapshot / save archive client 已迁到新域
本轮已把快照与继续游戏归档的真实请求实现迁入:
1. `src/services/rpg-runtime/rpgRuntimeRequest.ts`
2. `src/services/rpg-runtime/rpgSnapshotClient.ts`
3. `src/services/rpg-entry/rpgProfileClient.ts`
落地结果:
1. `rpgSnapshotClient` 现在直接承载 `/api/runtime/save/snapshot` 的读取、写入、删除。
2. `rpgProfileClient` 现在直接承载设置、个人资料、浏览历史、继续游戏归档相关请求。
3. `storageService.ts` 已退化为兼容转发层,不再继续作为 snapshot / save archive 主实现落点。
## 2.4 主入口与直接类型依赖已切换
本轮已更新以下直接调用点:
1. `src/App.tsx` 改为直接使用 `useRpgRuntimeSession`
2. `src/components/game-shell/types.ts`
3. `src/components/game-shell/GameShellMainContent.tsx`
4. `src/components/game-shell/GameShellStoryPanels.tsx`
当前状态:
1. 运行态主入口已经不再直接依赖 `useGameShellRuntime`
2. `GameShell` 相关组件仍保持 UI 与结构不变,只调整了 session 域导入路径。
## 2.5 测试补齐
本轮补齐或切换了以下定向测试:
1. `src/hooks/runtimeAuthGuards.test.tsx`
2. `src/hooks/useGameFlow.customWorld.test.tsx`
3. `src/services/rpg-entry/rpgProfileClient.test.ts`
4. `src/services/rpg-runtime/rpgSnapshotClient.test.ts`
目的:
1. 覆盖 `rpg-session` 新 hook 的远端鉴权守卫行为。
2. 覆盖自定义世界进入世界后的 bootstrap 初始化结果。
3. 覆盖迁入新域后的 browse history / save archive / snapshot 路由请求。
## 3. 本次没有做的事
以下内容仍保持原状,留给后续工作包:
1. 没有拆 `AdventurePanel.tsx``GameShellRuntime.tsx``GameShellMainContent.tsx` 的内部 UI 组织。
2. 没有拆 `useStoryGeneration.ts``npcEncounterActions.ts``runtimeStoryCoordinator.ts` 的 runtime story 内部职责。
3. 没有修改后端 `storyActionService.ts``runtimeSession.ts` 或任何路由组织。
4. 没有修改平台入口/世界详情/选角页面的视觉结构、按钮位置、tab 组织和独立面板交互方式。
## 4. 验证与检查
本轮应执行并记录:
1. `npm run check:encoding`
2. `npx vitest run src/hooks/runtimeAuthGuards.test.tsx src/hooks/useGameFlow.customWorld.test.tsx src/services/rpg-entry/rpgProfileClient.test.ts src/services/rpg-runtime/rpgSnapshotClient.test.ts src/services/storageService.test.ts`
重点核查点:
1. 主入口是否已经切到 `useRpgRuntimeSession`
2. `useGameFlow.ts``useGamePersistence.ts``useGameShellRuntime.ts` 是否只剩兼容导出。
3. `storageService.ts` 是否已不再承载 snapshot / save archive 的真实实现。
4. 是否没有改动任何前端界面结构与交互表现。
## 5. 对后续工作包的直接收益
1. 工作包 B / D 可以继续消费 `rpg-session` 新域入口,而不必再从旧 `useGame*` 文件接主逻辑。
2. 工作包 E 可以只关注 runtime story 链,不必再同时承担 session / persistence 主实现迁移。
3. 后续清理旧命名时,可以直接删除 `useGame*` 兼容层,而不会再触发大规模逻辑回迁。

View File

@@ -0,0 +1,130 @@
# RPG 进入游戏与运行时链路重构工作包 D 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 D前端运行态 shell 与面板拆分**,严格遵守以下边界:
1.`GameShellRuntime.tsx``GameShellMainContent.tsx``GameShellStoryPanels.tsx``AdventurePanel.tsx` 的真实实现迁入 `rpg-runtime-shell``rpg-runtime-panels`
2.`GameShell*``AdventurePanel` 保留兼容桥接,不在这一轮硬删旧路径。
3. 不改平台入口链、不改 runtime story 后端协议、不改任何前端交互界面设计。
4. 冒险主面板只做最小必要的 section 拆分,不额外扩散成更多与工作包 D 无关的重构。
## 2. 本次已落地内容
## 2.1 RPG 运行态 shell 已承接真实实现
以下文件已从 façade 升级为真实实现:
1. `src/components/rpg-runtime-shell/RpgRuntimeShell.tsx`
2. `src/components/rpg-runtime-shell/RpgRuntimeStageRouter.tsx`
3. `src/components/rpg-runtime-shell/RpgRuntimeOverlayHost.tsx`
4. `src/components/rpg-runtime-shell/useRpgRuntimeShellViewModel.ts`
5. `src/components/rpg-runtime-shell/useRpgRuntimeOverlayState.ts`
6. `src/components/rpg-runtime-shell/types.ts`
本轮完成的真实迁移包括:
1. `App.tsx` 主入口已经改为直接挂载 `RpgRuntimeShell`,不再把旧 `GameShellRuntime` 当作真实主入口。
2. `RpgRuntimeShell` 已承接运行态总外壳、画布舞台、主阶段路由、overlay host 装配。
3. `RpgRuntimeStageRouter` 已承接平台入口 / 角色选择 / 冒险运行态三阶段切换。
4. `RpgRuntimeOverlayHost` 已承接角色面板浮层、背包浮层、冒险实体详情、营地、地图、角色聊天与 NPC 交互弹层。
5. `useRpgRuntimeShellViewModel` 已承接运行态 overlay 状态、过场可见态、统计数据和 scene transition choice 包装。
6. `useRpgRuntimeOverlayState` 已把旧 `useGameShellViewModel` 的壳层状态迁入 RPG 域命名。
## 2.2 RPG 主面板路由与冒险主面板已承接真实实现
以下文件已从 façade 升级为真实实现:
1. `src/components/rpg-runtime-panels/RpgRuntimePanelRouter.tsx`
2. `src/components/rpg-runtime-panels/RpgAdventurePanel.tsx`
本轮完成的真实迁移包括:
1. `RpgRuntimePanelRouter` 已承接冒险 / 角色 / 背包三主标签切换。
2. `RpgAdventurePanel` 已成为真实冒险主面板实现,不再只是桥接旧 `AdventurePanel`
3. 冒险主面板按执行计划要求显式拆成了三个主 section
- story section剧情展示区与对话流
- choice section按钮区、快捷入口、NPC 输入框
- overlay section任务/设置/统计/奖励等独立面板挂载
4. 拆分后保持了原有 UI 结构、按钮位置、浮层方式和交互顺序不变。
5. 同时修正了冒险面板测试里要求隐藏的说明文本,避免迁移后把 `detailText` 重新暴露到 UI 中。
## 2.3 旧热点文件已降级为兼容桥接层
以下旧热点文件已不再承载真实实现,只保留兼容入口:
1. `src/components/game-shell/GameShellRuntime.tsx`
2. `src/components/game-shell/GameShellMainContent.tsx`
3. `src/components/game-shell/GameShellStoryPanels.tsx`
4. `src/components/game-shell/GameShellOverlays.tsx`
5. `src/components/AdventurePanel.tsx`
6. `src/components/game-shell/useGameShellRuntimeViewModel.ts`
7. `src/components/game-shell/useGameShellViewModel.ts`
8. `src/components/game-shell/types.ts`
当前策略:
1.`GameShell*` 路径继续可被现有调用与测试引用。
2. 旧 hook / type 文件继续对外提供兼容别名,避免并行工作流马上失效。
3. 真实运行态主链已经切到 `rpg-runtime-shell``rpg-runtime-panels`,旧热点不再继续扩大职责。
## 2.4 为兼容迁移补齐的新类型出口
为了避免本轮迁移造成旧桥接和 barrel 断裂,本轮额外补齐了以下类型出口:
1. `RpgRuntimeShellProps`
2. `RpgRuntimeShellViewModelResult`
3. `RpgEntryHomeViewProps`
4. `RpgEntryWorldDetailViewProps`
这些改动只用于保证工作包 D 新目录可以作为真实调用入口和兼容桥的稳定目标,不属于额外功能开发。
## 3. 本次刻意未做的事
以下内容明确保持到其他工作包,不在本轮越界处理:
1. 没有改平台入口链编排;`rpg-entry` 的真实拆分仍属于工作包 B。
2. 没有改 session/bootstrap/persistence 语义;这部分仍属于工作包 C。
3. 没有改 runtime story hooks、NPC 交互主链与后端协议;这部分仍属于工作包 E 及后续后端工作包。
4. 没有重做 AdventurePanel 内部更细粒度的卡片/子面板组件树,只做执行计划明确要求的三段 section 拆分。
5. 没有删除旧 `GameShell*` 文件,只把它们降级为兼容桥,避免影响并行工作流。
6. 没有调整任何用户可见的布局、按钮位置、tab 组织或弹窗/独立面板出现方式。
## 4. 验证结果
本轮已完成以下验证:
1. `npm run check:encoding`
2. `npx vitest run src/components/AdventurePanel.test.tsx src/components/AdventurePanel.npcChat.test.tsx src/components/game-shell/useGameShellRuntimeViewModel.test.ts src/components/CustomWorldEntityEditorModal.test.tsx`
3. 针对本工作包改动路径执行 `tsc` 定向筛查
验证结果:
1. 编码检查通过。
2. 上述 4 组定向测试全部通过。
3. 针对本轮改动路径的类型筛查无新增报错。
4. 全量 `tsc` 仍然存在其他工作流文件的并行类型噪音,但未命中本轮工作包 D 改动文件。
## 5. 对工作包 D 完成度的复核
对照执行计划中的工作包 D 目标,本轮已完成:
1. 运行态 shell 的真实迁移。
2. 主阶段路由器的真实迁移。
3. 主面板路由器的真实迁移。
4. 冒险主面板的真实迁移。
5. 冒险主面板按 section 显式分段。
6. 旧热点文件降级为兼容桥。
仍刻意保留、但不属于遗漏的部分:
1. 更细粒度的 AdventurePanel 子组件继续保留在单文件内部。
2. 旧文件兼容导出尚未清理。
3. 运行态 story hooks / gateway 仍在工作包 E 路径中继续演进。
4. 本轮复核已补齐一个收口遗漏:`rpg-runtime-shell` / `rpg-runtime-panels` 真实实现所依赖的 story UI 类型,现已直接从 `hooks/rpg-runtime-story` 导入,不再反向依赖旧 `useStoryGeneration.ts` 兼容入口。
结论:
**工作包 D 在“不改 UI、不改后端协议、禁止过度开发”的前提下已经完整落地当前没有遗漏的必做项。**

View File

@@ -0,0 +1,112 @@
# RPG 进入游戏与运行时链路重构工作包 E 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 E前端 runtime story 与 NPC 交互链拆分**,严格遵守以下边界:
1. 把前端 runtime story 主链真实迁到 `src/hooks/rpg-runtime-story/``src/services/rpg-runtime/`
2.`useStoryGeneration.ts``useStoryRuntimeController.ts``useStoryFlowCoordinator.ts``useStoryGoalSessionCoordinator.ts``useStoryInteractionCoordinator.ts``npcEncounterActions.ts``runtimeStoryCoordinator.ts``runtimeStoryService.ts` 的正式实现从旧命名入口迁出。
3. 保留旧 `story/*``runtimeStoryService.ts` 兼容导出,避免误伤其他并行工作包。
4. 不改任何前端交互界面设计,不改后端动作语义,不新增玩法。
## 2. 本次已落地内容
## 2.1 RPG runtime story 主链真实迁移
已把以下真实实现迁入 RPG 域目录:
1. `src/hooks/rpg-runtime-story/useRpgRuntimeStory.ts`
2. `src/hooks/rpg-runtime-story/useRpgRuntimeStoryController.ts`
3. `src/hooks/rpg-runtime-story/useRpgRuntimeStoryFlow.ts`
4. `src/hooks/rpg-runtime-story/useRpgRuntimeStoryState.ts`
5. `src/hooks/rpg-runtime-story/useRpgRuntimeInteractionFlow.ts`
6. `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts`
7. `src/hooks/rpg-runtime-story/rpgRuntimeStoryGateway.ts`
本轮完成后:
1. `useRpgRuntimeStory` 已成为前端 runtime story 顶层装配入口,不再只是 façade。
2. `useRpgRuntimeStoryController` 负责 story 状态、AI 请求与提交动作。
3. `useRpgRuntimeStoryFlow` 负责 option 展示、交互流和 story/session 状态流收口。
4. `useRpgRuntimeStoryState` 负责 reset、hydrate、地图跳转与 quest UI 收口。
5. `useRpgRuntimeInteractionFlow` 负责宝箱、背包、NPC、story choice 正式分发。
6. `useRpgRuntimeNpcInteraction` 负责 NPC 对话、待接委托、战斗后续对话重开与服务端 NPC 动作派发。
7. `rpgRuntimeStoryGateway` 负责 option catalog 拉取、继续游戏恢复与服务端 runtime choice 结算。
## 2.2 runtime story client 真实迁移
已把 `/api/runtime/story` 的真实 client 实现迁入:
1. `src/services/rpg-runtime/rpgRuntimeStoryClient.ts`
本轮完成后:
1. `rpgRuntimeStoryClient` 不再桥接旧 `runtimeStoryService.ts`,而是成为真实请求实现。
2. `runtimeStoryService.ts` 降级为兼容导出层。
3. `getRpgRuntimeStoryState``resolveRpgRuntimeStoryAction``resolveRpgRuntimeStoryMoment``isRpgRuntimeServerFunctionId``shouldUseRpgRuntimeServerOptions` 已成为新域主能力。
## 2.3 主调用链已接回 RPG 域
本轮已把以下主链与 helper 入口切到 RPG 域实现:
1. `src/hooks/rpg-session/useRpgRuntimeSession.ts` 改为直接使用 `useRpgRuntimeStory`
2. `src/hooks/story/storyRequestCoordinator.ts` 改为消费 `loadRpgRuntimeOptionCatalog``shouldUseRpgRuntimeServerOptions`
3. `src/hooks/story/choiceActions.ts` 改为消费 `isRpgRuntimeServerFunctionId`
4. `src/hooks/story/storyChoiceRuntime.ts` 改为消费 `resolveRpgRuntimeChoice`
5. `src/hooks/story/inventoryActions.ts` 改为消费 `resolveRpgRuntimeChoice`
6. `src/hooks/story/npcInteraction.ts` 改为消费 `resolveRpgRuntimeChoice`
7. `src/hooks/useTreasureFlow.ts` 改为消费 `resolveRpgRuntimeChoice`
这意味着工作包 E 范围内的前端 runtime story 正式结算主链,已经不再依赖旧 `runtimeStoryService.ts` 与旧 `runtimeStoryCoordinator.ts` 的真实实现。
## 2.4 兼容层处理
以下旧文件现已降级为兼容导出,不再承载正式实现:
1. `src/hooks/useStoryGeneration.ts`
2. `src/hooks/story/useStoryRuntimeController.ts`
3. `src/hooks/story/useStoryFlowCoordinator.ts`
4. `src/hooks/story/useStoryGoalSessionCoordinator.ts`
5. `src/hooks/story/useStoryInteractionCoordinator.ts`
6. `src/hooks/story/npcEncounterActions.ts`
7. `src/hooks/story/runtimeStoryCoordinator.ts`
8. `src/services/runtimeStoryService.ts`
保留这些兼容层的原因:
1. 避免一次性改动所有 UI 与测试引用,降低并行工作冲突。
2. 让工作包 D、C 仍能通过旧导入继续编译,再逐步切到新域命名。
3. 明确“主实现已迁移、旧入口只兼容”的工程状态,避免后续继续扩大旧热点文件。
4. 复核时已补齐一批主链收口点:`RpgRuntimeStageRouter``RpgRuntimeOverlayHost``RpgRuntimePanelRouter``RpgAdventurePanel` 及其直连组件的 story UI 类型导入,现已直接消费 `hooks/rpg-runtime-story` 新域出口,旧 `useStoryGeneration.ts` 仅保留兼容角色。
## 3. 本次没有做的事
以下内容仍保持原状,留给其他工作包:
1. 没有修改 `AdventurePanel.tsx``GameShellMainContent.tsx``GameShellStoryPanels.tsx` 的 UI 结构。
2. 没有重做任何对话区、奖励区、输入区、overlay 的交互形式。
3. 没有修改后端 runtime story route / service / compiler 语义。
4. 没有清理所有旧 UI 组件对 `useStoryGeneration.ts` 的类型导入,本轮只保证旧入口已退化为兼容层。
5. 没有删除旧兼容文件,避免误伤其他并行中的工作包。
## 4. 验证结果
本轮已执行:
1. `npm run check:encoding`
2. `npx vitest run src/services/runtimeStoryService.test.ts src/hooks/story/runtimeStoryCoordinator.test.ts src/hooks/story/storyRequestCoordinator.test.ts src/hooks/story/choiceActions.test.ts src/hooks/story/storyChoiceRuntime.test.ts src/hooks/story/npcEncounterActions.test.ts`
验证结果:
1. 编码检查通过。
2. 工作包 E 直接相关的 6 个测试文件、44 条测试全部通过。
3. 运行时 option catalog、runtime choice、NPC 交互、待接委托、背包动作与本地/服务端分流逻辑均完成回归。
4. 复核补跑 `src/services/rpg-entry/rpgProfileClient.test.ts``src/services/rpg-runtime/rpgSnapshotClient.test.ts``src/services/runtimeStoryService.test.ts``src/components/game-shell/PreGameSelectionFlow.agent.interaction.test.tsx` 共 33 条定向测试,全部通过。
## 5. 对后续工作包的直接收益
1. 工作包 D 后续如果继续拆 runtime panel 与 adventure panel可以直接消费 `useRpgRuntimeStory``rpg-runtime-story` 域类型,不必再穿旧 `story` 目录。
2. 工作包 C 与 session/persistence 链路已经可以直接对接 `rpgRuntimeStoryGateway` 的继续游戏恢复能力。
3. 后续如果要清理旧 `useStoryGeneration.ts``runtimeStoryService.ts` 等旧命名入口,已经具备“新主链真实可用”的前提,不再是 façade 空壳状态。

View File

@@ -0,0 +1,121 @@
# RPG 进入游戏与运行时链路重构工作包 F 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 F后端 route 边界拆分**,严格遵守以下边界:
1. 只调整后端路由组织、挂载边界与兼容 façade。
2. 不重写下游 `service` / `repository` 业务语义。
3. 不修改任何前端交互界面设计,不额外推进工作包 G/H。
## 2. 本次已落地内容
## 2.1 `app.ts` 显式挂载 RPG 域路由
已把 `server-node/src/app.ts` 中的 RPG 相关入口改成显式按域挂载:
1. `rpgProfile`:资料看板、浏览历史、设置
2. `rpgEntrySave`:快照读写、继续游戏归档列表与恢复
3. `rpgWorldLibrary`作品库、作品广场、works 列表
4. `rpgRuntimeStory``/api/runtime/story` 状态读取与动作结算
5. `rpgRuntimeAiAssist`runtime story 之外的 AI 辅助接口
当前策略:
1. 使用 `scopeToPrefixes(...)` 只匹配各自域前缀,避免新路由误拦截无关 `/api` 请求。
2. 所有线上接口路径保持不变,仍兼容现有前端与测试调用。
3. `routeVersion` 对新域挂载统一标记为 `2026-04-21`,与本轮路由重构窗口对齐。
## 2.2 新 `rpg-*` 路由从骨架升级为真实实现
已把以下骨架路由补成真实入口:
1. `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts`
2. `server-node/src/routes/rpg-entry/rpgEntrySaveRoutes.ts`
3. `server-node/src/routes/rpg-entry/rpgWorldLibraryRoutes.ts`
4. `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts`
5. `server-node/src/routes/rpg-runtime/rpgRuntimeAiAssistRoutes.ts`
本轮真实迁入的新边界包括:
1. `rpgProfileRoutes.ts`
- `/api/runtime/profile/dashboard`
- `/api/runtime/profile/wallet-ledger`
- `/api/runtime/profile/play-stats`
- `/api/runtime/profile/browse-history`
- 兼容路径 `/api/profile/...`
- `/api/runtime/settings`
2. `rpgEntrySaveRoutes.ts`
- `/api/runtime/save/snapshot`
- `/api/runtime/profile/save-archives`
- 兼容路径 `/api/profile/save-archives`
3. `rpgWorldLibraryRoutes.ts`
- `/api/runtime/custom-world-gallery`
- `/api/runtime/custom-world/works`
- `/api/runtime/custom-world-library`
- publish / unpublish / soft delete 等库操作
4. `rpgRuntimeStoryRoutes.ts`
- `/api/runtime/story/actions/resolve`
- `/api/runtime/story/state/:sessionId`
- `/api/runtime/story/state/resolve`
5. `rpgRuntimeAiAssistRoutes.ts`
- runtime story 之外的 LLM proxy、cover/scene 资产、custom world profile 生成、角色/NPC chat、runtime item、quest 生成、`/api/ws/health`
## 2.3 旧大路由退化为兼容 façade
已把旧入口降级为兼容层:
1. `server-node/src/modules/story/storyActionRoutes.ts`
- 不再自己承载 schema 与 handler
- 直接桥接到 `createRpgRuntimeStoryRoutes(context)`
2. `server-node/src/routes/runtimeRoutes.ts`
- 不再继续承载 profile / save / world library / runtime ai assist / runtime story
- 当前只保留旧 `customWorldAgent` 挂载与兼容 `ws/health`
这样处理后:
1. `runtimeRoutes.ts` 不再是 RPG 主链真实入口。
2. `storyActionRoutes.ts` 不再是 runtime story 主链真实实现。
3. 后续工作包 G/H 可以基于新 `rpg-*` 路由继续细化,而不用再穿透旧大文件。
## 3. 本次没有做的事
以下内容仍保持原状,不属于工作包 F 本轮范围:
1. 没有拆 `storyActionService.ts``runtimeSession.ts` 的内部职责。
2. 没有继续改 `npcInteractionService.ts``questStoryActionService.ts` 的业务实现。
3. 没有推进仓储、共享契约的进一步物理拆分。
4. 没有改变任何前端页面结构、交互路径、面板形式或 UI 文案。
## 4. 验证结果
本次已执行:
1. `npm run check:encoding`
2. `node --test --test-concurrency=1 --import tsx "src/routes/rpgRouteBoundaries.test.ts"`
验证结果:
1. 编码检查通过。
2. 新增定向测试 `server-node/src/routes/rpgRouteBoundaries.test.ts` 4 项全部通过。
3. 已覆盖以下工作包 F 验收重点:
- `rpgProfile` 新路径与 legacy 路径兼容
- `rpgEntrySave` 快照/归档路由可用
- `rpgWorldLibrary` / `gallery` / `works` 新边界可用
- `rpgRuntimeStory` 新边界与旧 façade 兼容可用
同时确认:
1. 全量 `npx tsc -p server-node/tsconfig.json --noEmit` 仍被仓库中既有跨模块类型问题阻塞。
2. 这些错误覆盖 auth、inventory、custom world、scene image 等多个既有热点,与本次工作包 F 新增路由边界并非同一问题面。
## 5. 对执行计划的对齐结论
对照执行计划中的工作包 F
1. `app.ts` 已能一眼看出 `rpgProfile``rpgEntry``rpgRuntimeStory``rpgRuntimeAiAssist` 的挂载边界。
2. `runtimeRoutes.ts` 已退化为兼容入口,不再承接 RPG 主链的大杂糅职责。
3. `storyActionRoutes.ts` 已退化为兼容 façade真实 runtime story 路由落点转入 `server-node/src/routes/rpg-runtime/rpgRuntimeStoryRoutes.ts`
4. 本轮没有越过工作包 F 去重写下游 service 语义,满足“禁止过度开发”的约束。

View File

@@ -0,0 +1,96 @@
# RPG 进入游戏与运行时链路重构工作包 G 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 G后端 runtime session / action service 拆分**,严格遵守以下边界:
1.`storyActionService.ts``runtimeSession.ts` 的真实实现迁入 `server-node/src/modules/rpg-runtime-story/` 新域目录。
2. 把 runtime action 主链依赖的 session 原语、option 编译、snapshot sync、story state 读取按职责落到新文件。
3.`server-node/src/modules/story/` 热点文件只保留兼容导出,不再承载真实实现。
4. 不改前端入口与 UI不改路由协议语义不做仓储拆分。
## 2. 本次已落地内容
## 2.1 `runtimeSession.ts` 真实实现已迁入 `rpg-runtime-story`
本轮已把旧 `server-node/src/modules/story/runtimeSession.ts` 的真实实现迁入:
1. `server-node/src/modules/rpg-runtime-story/RpgRuntimeSessionDomain.ts`
2. `server-node/src/modules/rpg-runtime-story/RpgRuntimeSessionLoader.ts`
3. `server-node/src/modules/rpg-runtime-story/RpgRuntimeSessionPrimitives.ts`
4. `server-node/src/modules/rpg-runtime-story/RpgRuntimeOptionCompiler.ts`
5. `server-node/src/modules/rpg-runtime-story/RpgRuntimeSnapshotSync.ts`
6. `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryPresentationCompiler.ts`
落地结果:
1. `loadRuntimeSession(...)``buildAvailableOptions(...)``buildRuntimeViewModel(...)``syncRawGameState(...)``replaceRuntimeSessionRawGameState(...)` 已有新域真实落点。
2. `appendStoryHistory(...)``getEncounterNpcState(...)``setEncounterNpcState(...)``MAX_TASK5_COMPANIONS``TASK6_DEFERRED_FUNCTION_IDS` 等运行时原语已通过 `RpgRuntimeSessionPrimitives.ts` 对外输出。
3.`server-node/src/modules/story/runtimeSession.ts` 已退化为兼容层,不再承载主实现。
## 2.2 `storyActionService.ts` 真实实现已迁入 `rpg-runtime-story`
本轮已把旧 `server-node/src/modules/story/storyActionService.ts` 的真实实现迁入:
1. `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts`
2. `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionService.ts`
3. `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryStateService.ts`
落地结果:
1. `resolveRuntimeStoryAction(...)` 已从新域动作服务入口导出。
2. `getRuntimeStoryState(...)` 已从新域状态服务入口导出。
3.`server-node/src/modules/story/storyActionService.ts` 已退化为兼容转发层。
## 2.3 runtime action 主链下游依赖已切到新域
本轮已把以下直接依赖 `runtimeSession.ts` 的后端模块切到 `rpg-runtime-story` 新域入口:
1. `server-node/src/modules/combat/combatResolutionService.ts`
2. `server-node/src/modules/npc/npcInteractionService.ts`
3. `server-node/src/modules/inventory/inventoryStoryActionService.ts`
4. `server-node/src/modules/inventory/npcInventoryStoryActionService.ts`
5. `server-node/src/modules/quest/questRuntimeSignalService.ts`
6. `server-node/src/modules/quest/questStoryActionService.ts`
7. `server-node/src/modules/runtime-item/treasureStoryActionService.ts`
8. `server-node/src/modules/story/storyActionRoutes.ts`
9. `server-node/src/modules/story/runtimeSession.test.ts`
当前状态:
1. runtime action 执行链已经直接消费 `rpg-runtime-story` 域入口,不再把旧 `modules/story/` 视为真实主实现。
2. `storyActionRoutes.ts` 已直接从新域读取动作服务与状态服务。
3. `runtimeSession.test.ts` 已直接验证新域编译链与 legacy currentStory 展示投影。
## 3. 本次没有做的事
以下内容仍保持原状,留给后续工作包或后续阶段:
1. 没有修改任何前端入口、前端运行态 UI、前端交互设计。
2. 没有拆 `server-node/src/routes/runtimeRoutes.ts``server-node/src/app.ts` 的路由挂载组织,这属于工作包 F。
3. 没有拆 `runtimeRepository.ts``runtimeSnapshotHydration.ts` 与 shared contract这属于工作包 H。
4. 没有把 `RpgRuntimeStoryActionDomain.ts` 再继续切成更细颗粒度的真实实现文件;本轮只做到工作包 G 要求的“目录化拆开 + 旧热点降级”。
5. 没有改动任何动作语义、返回协议或 LLM 编排行为。
## 4. 验证与检查
本轮已执行:
1. `npm run check:encoding`
2. `npx tsx --test server-node/src/modules/story/runtimeSession.test.ts server-node/src/modules/story/storyActionRoutes.test.ts`
3. `npm --prefix server-node run build`
验证结果:
1. 编码检查通过。
2. 与 runtime session / runtime story action 直接相关的后端定向测试 `20` 项全部通过。
3. `server-node` 构建通过。
4.`server-node/src/modules/story/runtimeSession.ts``server-node/src/modules/story/storyActionService.ts` 已只剩兼容导出,没有残留真实主实现。
## 5. 对后续工作的直接收益
1. 工作包 F 后续继续做 route 边界拆分时,可以直接把 runtime story 路由稳定挂到 `rpg-runtime-story` 新域入口。
2. 工作包 H 后续拆仓储、shared contract 和测试基建时,可以围绕 `rpg-runtime-story` 新目录继续收口,而不必再穿透旧热点文件。
3. 后续如果继续细拆 runtime story 主链,可以在新域内部继续物理拆分,而不会重新把真实实现塞回 `modules/story/`

View File

@@ -0,0 +1,90 @@
# RPG 进入游戏与运行时链路重构工作包 H 落地记录
更新时间:`2026-04-21`
## 1. 本次目标
本次只落实 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中的 **工作包 H仓储、契约与测试基建**,严格遵守以下边界:
1. 只补仓储、shared contract、fixture 与测试基建。
2. 不修改前端 UI不改页面交互设计。
3. 不重写 route 层与 runtime story 主流程逻辑。
## 2. 本次已落地内容
## 2.1 仓储按 RPG 域补齐独立入口
本次补齐并收口了工作包 H 目标中的按域仓储入口:
1. `server-node/src/repositories/rpg-runtime/RpgRuntimeSnapshotRepository.ts`
2. `server-node/src/repositories/rpg-entry/RpgSaveArchiveRepository.ts`
3. `server-node/src/repositories/rpg-entry/RpgWorldLibraryRepository.ts`
4. `server-node/src/repositories/rpg-profile/RpgProfileDashboardRepository.ts`
5. `server-node/src/repositories/rpg-profile/RpgBrowseHistoryRepository.ts`
本轮策略仍保持最小侵入:
1. 新仓储继续委托 `runtimeRepository.ts` 提供真实读写。
2. `RpgProfileDashboardRepository` 只保留资料看板、设置、钱包、游玩统计职责。
3. 浏览历史读写从资料仓储中抽离到 `RpgBrowseHistoryRepository`,与执行方案的目标拆分保持一致。
## 2.2 shared runtime contract 按领域拆分并保留兼容 façade
已把原 `packages/shared/src/contracts/story.ts` 中的 RPG runtime shared contract 拆分为:
1. `packages/shared/src/contracts/rpgRuntimeStoryAction.ts`
2. `packages/shared/src/contracts/rpgRuntimeStoryState.ts`
3. `packages/shared/src/contracts/rpgRuntimeChat.ts`
4. `packages/shared/src/contracts/rpgRuntimeQuestAssist.ts`
兼容策略:
1. `story.ts` 退化为 façade只做分文件 re-export。
2. 现有前后端调用方仍可继续从 `contracts/story` 取用类型与常量,不要求本轮同步迁移所有 import。
3. runtime story 主链契约与 chat / quest assist / runtime item 辅助契约已经具备独立演进落点。
## 2.3 测试基建补齐
本次补充并核对了工作包 H 范围内的测试:
1. `server-node/src/modules/runtime/runtimeSnapshotHydration.test.ts`
用于覆盖 snapshot 归一化、恢复继续游戏时的默认值修复与旧存档兼容。
2. `server-node/src/repositories/rpg-profile/RpgProfileRepositories.test.ts`
用于确认资料看板仓储与浏览历史仓储的职责边界已经分离。
3. `server-node/src/repositories/rpg-entry/RpgEntryRepositories.test.ts`
用于确认 continue game 归档仓储与作品库仓储已经独立命名并保持职责边界。
4. `server-node/src/repositories/rpg-runtime/RpgRuntimeSnapshotRepository.test.ts`
用于确认 snapshot 读写职责已经有独立仓储入口。
5. `packages/shared/src/contracts/rpgRuntimeContracts.test.ts`
用于确认 `story.ts` façade 在拆分后仍保持旧入口兼容。
## 3. 本次没有做的事
以下内容仍保持原状,留给其他工作包或第三批统一收口:
1. 没有把前后端所有 `contracts/story` import 全量改写到新分文件,避免与并行工作包产生无谓冲突。
2. 没有改 `server-node/src/routes/runtimeRoutes.ts``server-node/src/modules/story/storyActionService.ts``server-node/src/modules/story/runtimeSession.ts` 的真实逻辑。
3. 没有改前端 continue game、角色选择、冒险运行态的界面与交互。
## 4. 验证结果
本轮执行并通过:
1. `npm run check:encoding`
2. `npx vitest run packages/shared/src/contracts/rpgRuntimeContracts.test.ts`
3. `node --test --test-concurrency=1 --import tsx src/modules/runtime/runtimeSnapshotHydration.test.ts src/repositories/rpg-profile/RpgProfileRepositories.test.ts src/repositories/rpg-entry/RpgEntryRepositories.test.ts src/repositories/rpg-runtime/RpgRuntimeSnapshotRepository.test.ts`
说明:
1. 工作树当前存在其他并行修改与冲突文件,因此没有把与工作包 H 无关的全量问题一并处理。
2. 本轮验证只覆盖工作包 H 自身改动与其直接依赖,避免过度开发。
## 5. 与执行方案的对照结论
对照 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 中工作包 H 的目标,本轮已完成:
1. snapshot / profile / library / browse history 仓储独立命名入口补齐。
2. `story.ts` shared contract 已拆为 runtime story action、runtime story state、runtime chat、runtime quest assist 四个独立文件。
3. runtime story / snapshot / continue game 相关测试已补齐到可直接回归的最小闭环。
当前未额外扩张到主流程迁移、route 改写、UI 调整,符合“禁止过度开发”的执行要求。

View File

@@ -33,7 +33,6 @@
"test": "vitest run",
"test:watch": "vitest",
"check": "npm run lint && npm run test && npm run build && npm run check:content",
"generate:build-tags": "py -3 scripts/generate-build-tag-similarity.py",
"check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts",
"check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts",
"check:smoke": "node scripts/run-tsx.cjs scripts/smoke-content.ts",

View File

@@ -78,7 +78,8 @@ export type AuthPhoneChangeResponse = {
};
export type AuthRefreshResponse = {
token: string;
ok: true;
token?: string;
};
export type AuthSessionSummary = {

View File

@@ -1,533 +1,12 @@
export type CustomWorldWorkStatus = 'draft' | 'published';
export type CustomWorldWorkSource = 'agent_session' | 'published_profile';
/**
* 兼容出口:
* 当前仓库仍有大量旧 customWorld 命名导入,这个文件继续作为过渡层保留。
* 工作包 H 完成后,真实类型定义已经迁移到 rpg* 契约文件中;这里仅聚合旧命名分文件。
*/
export interface WorldPromiseValue {
hook: string;
differentiator: string;
desiredExperience: string;
}
export interface PlayerFantasyValue {
playerRole: string;
corePursuit: string;
fearOfLoss: string;
}
export interface ThemeBoundaryValue {
toneKeywords: string[];
aestheticDirectives: string[];
forbiddenDirectives: string[];
}
export interface PlayerEntryPointValue {
openingIdentity: string;
openingProblem: string;
entryMotivation: string;
}
export interface CoreConflictValue {
surfaceConflicts: string[];
hiddenCrisis: string;
firstTouchedConflict: string;
}
export interface KeyRelationshipValue {
pairs: string;
relationshipType: string;
secretOrCost: string;
}
export interface HiddenLineValue {
hiddenTruths: string[];
misdirectionHints: string[];
revealPacing: string;
}
export interface IconicElementValue {
iconicMotifs: string[];
institutionsOrArtifacts: string[];
hardRules: string[];
}
export interface EightAnchorContent {
worldPromise: WorldPromiseValue | null;
playerFantasy: PlayerFantasyValue | null;
themeBoundary: ThemeBoundaryValue | null;
playerEntryPoint: PlayerEntryPointValue | null;
coreConflict: CoreConflictValue | null;
keyRelationships: KeyRelationshipValue[];
hiddenLines: HiddenLineValue | null;
iconicElements: IconicElementValue | null;
}
export interface CustomWorldWorkSummary {
workId: string;
sourceType: CustomWorldWorkSource;
status: CustomWorldWorkStatus;
title: string;
subtitle: string;
summary: string;
coverImageSrc?: string | null;
coverRenderMode?: 'image' | 'scene_with_roles';
coverCharacterImageSrcs?: string[];
updatedAt: string;
publishedAt?: string | null;
stage?: string | null;
stageLabel?: string | null;
playableNpcCount: number;
landmarkCount: number;
roleVisualReadyCount?: number;
roleAnimationReadyCount?: number;
roleAssetSummaryLabel?: string | null;
sessionId?: string | null;
profileId?: string | null;
canResume: boolean;
canEnterWorld: boolean;
}
export interface CreatorIntentReadiness {
isReady: boolean;
completedKeys: string[];
missingKeys: string[];
}
export interface CustomWorldPendingClarification {
id: string;
label: string;
question: string;
targetKey:
| 'world_hook'
| 'player_premise'
| 'theme_and_tone'
| 'core_conflict'
| 'relationship_seed'
| 'iconic_element';
priority: number;
answer?: string;
}
export type CustomWorldAgentStage =
| 'collecting_intent'
| 'clarifying'
| 'foundation_review'
| 'object_refining'
| 'visual_refining'
| 'long_tail_review'
| 'ready_to_publish'
| 'published'
| 'error';
export type CustomWorldAgentMessageRole = 'user' | 'assistant' | 'system';
export type CustomWorldAgentMessageKind =
| 'chat'
| 'clarification'
| 'summary'
| 'checkpoint'
| 'warning'
| 'action_result';
export interface CustomWorldAgentMessage {
id: string;
role: CustomWorldAgentMessageRole;
kind: CustomWorldAgentMessageKind;
text: string;
createdAt: string;
relatedOperationId?: string | null;
}
export type CustomWorldDraftCardKind =
| 'world'
| 'camp'
| 'faction'
| 'character'
| 'landmark'
| 'thread'
| 'chapter'
| 'scene_chapter'
| 'carrier'
| 'sidequest_seed';
export type CustomWorldDraftCardStatus =
| 'suggested'
| 'confirmed'
| 'locked'
| 'warning';
export interface CustomWorldDraftCardSummary {
id: string;
kind: CustomWorldDraftCardKind;
title: string;
subtitle: string;
summary: string;
status: CustomWorldDraftCardStatus;
linkedIds: string[];
warningCount: number;
assetStatus?: CustomWorldRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface CustomWorldDraftCardDetailSection {
id: string;
label: string;
value: string;
}
export interface CustomWorldFoundationDraftFaction {
id: string;
name: string;
title?: string;
subtitle?: string;
publicGoal: string;
relatedConflict: string;
tension?: string;
playerRelation: string;
summary: string;
}
export interface CustomWorldFoundationDraftCharacter {
id: string;
name: string;
title: string;
role: string;
publicIdentity: string;
publicMask?: string;
currentPressure: string;
hiddenHook?: string;
relationToPlayer: string;
threadIds: string[];
summary: string;
skills?: Array<{
id: string;
name: string;
actionPreviewConfig?: Record<string, unknown> | null;
}>;
imageSrc?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
export interface CustomWorldFoundationDraftLandmark {
id: string;
name: string;
description?: string;
purpose: string;
mood: string;
importance: string;
secret?: string;
dangerLevel?: string;
imageSrc?: string | null;
characterIds: string[];
threadIds: string[];
summary: string;
}
export interface CustomWorldFoundationDraftThread {
id: string;
title: string;
type: 'main' | 'hidden';
conflictType?: string;
conflict: string;
stakes?: string;
characterIds: string[];
landmarkIds: string[];
summary: string;
}
export interface CustomWorldFoundationDraftChapter {
id: string;
title: string;
openingEvent: string;
playerGoal: string;
characterIds: string[];
landmarkIds: string[];
understandingShift: string;
summary: string;
}
export interface CustomWorldFoundationDraftCamp {
id: string;
name: string;
description: string;
mood: string;
dangerLevel?: string;
imageSrc?: string | null;
summary: string;
}
export type CustomWorldSceneActStage =
| 'opening'
| 'expansion'
| 'turning_point'
| 'climax'
| 'aftermath';
export type CustomWorldSceneActAdvanceRule =
| 'after_primary_contact'
| 'after_active_step_complete'
| 'after_chapter_resolution';
export interface CustomWorldFoundationDraftSceneAct {
id: string;
title: string;
summary: string;
stageCoverage: CustomWorldSceneActStage[];
backgroundImageSrc?: string | null;
backgroundAssetId?: string | null;
encounterNpcIds: string[];
primaryNpcId: string;
linkedThreadIds: string[];
actGoal: string;
transitionHook: string;
advanceRule: CustomWorldSceneActAdvanceRule;
}
export interface CustomWorldFoundationDraftSceneChapter {
id: string;
sceneId: string;
sceneName: string;
title: string;
summary: string;
linkedThreadIds: string[];
linkedLandmarkIds: string[];
acts: CustomWorldFoundationDraftSceneAct[];
}
export interface CustomWorldFoundationDraftProfile {
name: string;
subtitle: string;
summary: string;
tone: string;
playerGoal: string;
majorFactions: string[];
coreConflicts: string[];
playableNpcs: CustomWorldFoundationDraftCharacter[];
storyNpcs: CustomWorldFoundationDraftCharacter[];
landmarks: CustomWorldFoundationDraftLandmark[];
camp?: CustomWorldFoundationDraftCamp | null;
themePack?: Record<string, unknown> | null;
storyGraph?: Record<string, unknown> | null;
factions: CustomWorldFoundationDraftFaction[];
threads: CustomWorldFoundationDraftThread[];
chapters: CustomWorldFoundationDraftChapter[];
sceneChapters: CustomWorldFoundationDraftSceneChapter[];
worldHook: string;
playerPremise: string;
openingSituation: string;
iconicElements: string[];
sourceAnchorSummary: string;
}
export interface CustomWorldFoundationDraftResult {
draftProfile: CustomWorldFoundationDraftProfile;
draftCards: CustomWorldDraftCardSummary[];
}
export interface CustomWorldDraftCardDetail {
id: string;
kind: CustomWorldDraftCardKind;
title: string;
sections: CustomWorldDraftCardDetailSection[];
linkedIds: string[];
locked: false;
editable: boolean;
editableSectionIds: string[];
warningMessages: string[];
assetStatus?: CustomWorldRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface CustomWorldSuggestedAction {
id: string;
type:
| 'request_summary'
| 'draft_foundation'
| 'refine_focus_target'
| 'lock_current_target'
| 'generate_role_assets'
| 'generate_scene_assets'
| 'expand_long_tail'
| 'publish_world';
label: string;
targetId?: string | null;
}
export type CustomWorldAssetPriorityTier = 'hero' | 'featured' | 'supporting';
export type CustomWorldRoleAssetStatus =
| 'missing'
| 'visual_ready'
| 'animations_ready'
| 'complete';
export interface CustomWorldRoleAssetSummary {
roleId: string;
roleName: string;
roleKind: 'playable' | 'story';
priorityTier: CustomWorldAssetPriorityTier;
portraitPath?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
status: CustomWorldRoleAssetStatus;
missingAnimations: string[];
nextPointCost: number;
}
export interface CustomWorldSceneAssetSummary {
sceneId: string;
sceneName: string;
actId?: string | null;
actTitle?: string | null;
imageSrc?: string | null;
assetId?: string | null;
status: 'missing' | 'ready';
nextPointCost: number;
}
export interface CustomWorldAssetCoverageSummary {
roleAssets: CustomWorldRoleAssetSummary[];
sceneAssets: CustomWorldSceneAssetSummary[];
allRoleAssetsReady: boolean;
allSceneAssetsReady: boolean;
}
export interface CustomWorldAgentSessionSnapshot {
sessionId: string;
currentTurn: number;
anchorContent: EightAnchorContent;
progressPercent: number;
lastAssistantReply: string | null;
stage: CustomWorldAgentStage;
focusCardId: string | null;
creatorIntent: Record<string, unknown> | null;
creatorIntentReadiness: CreatorIntentReadiness;
anchorPack: Record<string, unknown> | null;
lockState: Record<string, unknown> | null;
draftProfile: Record<string, unknown> | null;
messages: CustomWorldAgentMessage[];
draftCards: CustomWorldDraftCardSummary[];
pendingClarifications: CustomWorldPendingClarification[];
suggestedActions: CustomWorldSuggestedAction[];
recommendedReplies: string[];
qualityFindings: {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}[];
assetCoverage: CustomWorldAssetCoverageSummary;
updatedAt: string;
}
export type CustomWorldAgentOperationType =
| 'process_message'
| 'lock_cards'
| 'unlock_cards'
| 'regenerate_scope'
| 'draft_foundation'
| 'update_draft_card'
| 'generate_characters'
| 'generate_landmarks'
| 'generate_role_assets'
| 'sync_role_assets'
| 'generate_scene_assets'
| 'sync_scene_assets'
| 'expand_long_tail'
| 'publish_world'
| 'revert_checkpoint';
export type CustomWorldAgentOperationStatus =
| 'queued'
| 'running'
| 'completed'
| 'failed';
export interface CustomWorldAgentOperationRecord {
operationId: string;
type: CustomWorldAgentOperationType;
status: CustomWorldAgentOperationStatus;
phaseLabel: string;
phaseDetail: string;
progress: number;
error?: string | null;
}
export interface CreateCustomWorldAgentSessionRequest {
seedText?: string;
}
export interface CreateCustomWorldAgentSessionResponse {
session: CustomWorldAgentSessionSnapshot;
}
export interface SendCustomWorldAgentMessageRequest {
clientMessageId: string;
text: string;
quickFillRequested?: boolean;
focusCardId?: string | null;
selectedCardIds?: string[];
}
export interface SendCustomWorldAgentMessageResponse {
operation: CustomWorldAgentOperationRecord;
}
export type CustomWorldAgentActionRequest =
| { action: 'lock_cards'; cardIds: string[] }
| { action: 'unlock_cards'; cardIds: string[] }
| {
action: 'regenerate_scope';
scope:
| 'focus_card'
| 'long_tail_npcs'
| 'long_tail_landmarks'
| 'sidequest_seeds'
| 'role_assets'
| 'scene_assets';
targetCardId?: string | null;
}
| { action: 'draft_foundation' }
| {
action: 'update_draft_card';
cardId: string;
sections: Array<{
sectionId: string;
value: string;
}>;
}
| {
action: 'generate_characters';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| {
action: 'generate_landmarks';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| { action: 'generate_role_assets'; roleIds: string[] }
| {
action: 'sync_role_assets';
roleId: string;
portraitPath: string;
generatedVisualAssetId: string;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
| { action: 'publish_world' };
export interface CustomWorldAgentActionResponse {
operation: CustomWorldAgentOperationRecord;
}
export interface GetCustomWorldAgentCardDetailResponse {
card: CustomWorldDraftCardDetail;
}
export interface ListCustomWorldWorksResponse {
items: CustomWorldWorkSummary[];
}
export type * from './customWorldAgentAnchors';
export type * from './customWorldAgentDraft';
export type * from './customWorldAgentActions';
export type * from './customWorldAgentSession';
export type * from './customWorldResultPreview';
export type * from './customWorldWorkSummary';

View File

@@ -0,0 +1,14 @@
/**
* 旧 custom world 动作契约兼容出口。
* 后续若逐步迁移旧代码,建议直接改用 rpgAgentActions.ts。
*/
export type {
RpgAgentActionRequest as CustomWorldAgentActionRequest,
RpgAgentActionResponse as CustomWorldAgentActionResponse,
RpgAgentOperationRecord as CustomWorldAgentOperationRecord,
RpgAgentOperationStatus as CustomWorldAgentOperationStatus,
RpgAgentOperationType as CustomWorldAgentOperationType,
RpgAgentSupportedAction as CustomWorldSupportedAction,
RpgAgentSuggestedAction as CustomWorldSuggestedAction,
} from './rpgAgentActions';

View File

@@ -0,0 +1,16 @@
/**
* 旧 custom world 八锚点兼容出口。
* 这里只保留旧命名到 RPG 创作域新契约的映射,便于旧导入渐进迁移。
*/
export type {
RpgCreationAnchorContent as EightAnchorContent,
RpgCreationCoreConflictValue as CoreConflictValue,
RpgCreationHiddenLineValue as HiddenLineValue,
RpgCreationIconicElementValue as IconicElementValue,
RpgCreationKeyRelationshipValue as KeyRelationshipValue,
RpgCreationPlayerEntryPointValue as PlayerEntryPointValue,
RpgCreationPlayerFantasyValue as PlayerFantasyValue,
RpgCreationThemeBoundaryValue as ThemeBoundaryValue,
RpgCreationWorldPromiseValue as WorldPromiseValue,
} from './rpgAgentAnchors';

View File

@@ -0,0 +1,29 @@
/**
* 旧 custom world 草稿契约兼容出口。
* 工作包 H 完成后,真实定义已经迁到 rpgAgentDraft.ts这里只负责旧命名映射。
*/
export type {
RpgAgentAssetCoverageSummary as CustomWorldAssetCoverageSummary,
RpgAgentAssetPriorityTier as CustomWorldAssetPriorityTier,
RpgAgentDraftCardDetail as CustomWorldDraftCardDetail,
RpgAgentDraftCardDetailSection as CustomWorldDraftCardDetailSection,
RpgAgentDraftCardKind as CustomWorldDraftCardKind,
RpgAgentDraftCardStatus as CustomWorldDraftCardStatus,
RpgAgentDraftCardSummary as CustomWorldDraftCardSummary,
RpgAgentFoundationDraftCamp as CustomWorldFoundationDraftCamp,
RpgAgentFoundationDraftChapter as CustomWorldFoundationDraftChapter,
RpgAgentFoundationDraftCharacter as CustomWorldFoundationDraftCharacter,
RpgAgentFoundationDraftFaction as CustomWorldFoundationDraftFaction,
RpgAgentFoundationDraftLandmark as CustomWorldFoundationDraftLandmark,
RpgAgentFoundationDraftProfile as CustomWorldFoundationDraftProfile,
RpgAgentFoundationDraftResult as CustomWorldFoundationDraftResult,
RpgAgentFoundationDraftSceneAct as CustomWorldFoundationDraftSceneAct,
RpgAgentFoundationDraftSceneChapter as CustomWorldFoundationDraftSceneChapter,
RpgAgentFoundationDraftThread as CustomWorldFoundationDraftThread,
RpgAgentRoleAssetStatus as CustomWorldRoleAssetStatus,
RpgAgentRoleAssetSummary as CustomWorldRoleAssetSummary,
RpgAgentSceneActAdvanceRule as CustomWorldSceneActAdvanceRule,
RpgAgentSceneActStage as CustomWorldSceneActStage,
RpgAgentSceneAssetSummary as CustomWorldSceneAssetSummary,
} from './rpgAgentDraft';

View File

@@ -0,0 +1,20 @@
/**
* 旧 custom world 会话契约兼容出口。
* 这一层只做命名映射,不再承担 session 真相源结构定义。
*/
export type {
CreateRpgAgentSessionRequest as CreateCustomWorldAgentSessionRequest,
CreateRpgAgentSessionResponse as CreateCustomWorldAgentSessionResponse,
GetRpgAgentCardDetailResponse as GetCustomWorldAgentCardDetailResponse,
RpgAgentMessage as CustomWorldAgentMessage,
RpgAgentMessageKind as CustomWorldAgentMessageKind,
RpgAgentMessageRole as CustomWorldAgentMessageRole,
RpgAgentPendingClarification as CustomWorldPendingClarification,
RpgAgentQualityFinding as CustomWorldAgentQualityFinding,
RpgAgentSessionSnapshot as CustomWorldAgentSessionSnapshot,
RpgAgentStage as CustomWorldAgentStage,
RpgCreationIntentReadiness as CreatorIntentReadiness,
SendRpgAgentMessageRequest as SendCustomWorldAgentMessageRequest,
SendRpgAgentMessageResponse as SendCustomWorldAgentMessageResponse,
} from './rpgAgentSession';

View File

@@ -0,0 +1,12 @@
/**
* 旧 custom world 结果页预览兼容出口。
* 额外单独拆一个 preview 兼容文件,避免预览别名继续堆回 customWorldAgent.ts 聚合层。
*/
export type {
RpgCreationPreview as CustomWorldResultPreview,
RpgCreationPreviewBlocker as CustomWorldResultPreviewBlocker,
RpgCreationPreviewEnvelope as CustomWorldResultPreviewEnvelope,
RpgCreationPreviewFinding as CustomWorldResultPreviewFinding,
RpgCreationPreviewSource as CustomWorldResultPreviewSource,
} from './rpgCreationPreview';

View File

@@ -0,0 +1,11 @@
/**
* 旧 custom world works 读模型兼容出口。
* 用于把旧作品列表命名平滑映射到新的 RPG 创作域 works 契约。
*/
export type {
ListRpgCreationWorksResponse as ListCustomWorldWorksResponse,
RpgCreationWorkSource as CustomWorldWorkSource,
RpgCreationWorkStatus as CustomWorldWorkStatus,
RpgCreationWorkSummary as CustomWorldWorkSummary,
} from './rpgCreationWorkSummary';

View File

@@ -0,0 +1,120 @@
/**
* RPG Agent 动作与异步操作契约。
* 这里显式区分“建议动作”和“真实可执行动作”,为后续后端 registry 收口预留接口。
*/
export type RpgAgentSuggestedActionType =
| 'request_summary'
| 'draft_foundation'
| 'refine_focus_target'
| 'lock_current_target'
| 'generate_role_assets'
| 'generate_scene_assets'
| 'expand_long_tail'
| 'publish_world';
export interface RpgAgentSuggestedAction {
id: string;
type: RpgAgentSuggestedActionType;
label: string;
targetId?: string | null;
}
export type RpgAgentActionType =
| 'draft_foundation'
| 'update_draft_card'
| 'sync_result_profile'
| 'generate_characters'
| 'generate_landmarks'
| 'generate_role_assets'
| 'sync_role_assets'
| 'generate_scene_assets'
| 'sync_scene_assets'
| 'expand_long_tail'
| 'publish_world'
| 'revert_checkpoint';
export type RpgAgentActionCapabilityKey =
| RpgAgentSuggestedActionType
| RpgAgentActionType;
/**
* 当前先把能力矩阵定义为可选契约。
* 等工作包 E 的 registry 落地后,后端可以把真实 supportedActions 填充到 session snapshot。
*/
export interface RpgAgentSupportedAction {
action: RpgAgentActionCapabilityKey;
enabled: boolean;
reason?: string | null;
}
export type RpgAgentOperationType = RpgAgentActionType | 'process_message';
export type RpgAgentOperationStatus =
| 'queued'
| 'running'
| 'completed'
| 'failed';
export interface RpgAgentOperationRecord {
operationId: string;
type: RpgAgentOperationType;
status: RpgAgentOperationStatus;
phaseLabel: string;
phaseDetail: string;
progress: number;
error?: string | null;
}
export type RpgAgentActionRequest =
| { action: 'draft_foundation' }
| {
action: 'update_draft_card';
cardId: string;
sections: Array<{
sectionId: string;
value: string;
}>;
}
| {
action: 'sync_result_profile';
profile: Record<string, unknown>;
}
| {
action: 'generate_characters';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| {
action: 'generate_landmarks';
count: number;
promptText?: string | null;
anchorCardIds?: string[];
}
| { action: 'generate_role_assets'; roleIds: string[] }
| {
action: 'sync_role_assets';
roleId: string;
portraitPath: string;
generatedVisualAssetId: string;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
| { action: 'generate_scene_assets'; sceneIds: string[] }
| {
action: 'sync_scene_assets';
sceneId: string;
sceneKind: 'camp' | 'landmark';
imageSrc: string;
generatedSceneAssetId: string;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
}
| { action: 'expand_long_tail' }
| { action: 'publish_world' }
| { action: 'revert_checkpoint'; checkpointId: string };
export interface RpgAgentActionResponse {
operation: RpgAgentOperationRecord;
}

View File

@@ -0,0 +1,63 @@
/**
* RPG 创作八锚点契约。
* 这一层只描述“创作意图采集态”的结构,不混入 session 或结果页字段。
*/
export interface RpgCreationWorldPromiseValue {
hook: string;
differentiator: string;
desiredExperience: string;
}
export interface RpgCreationPlayerFantasyValue {
playerRole: string;
corePursuit: string;
fearOfLoss: string;
}
export interface RpgCreationThemeBoundaryValue {
toneKeywords: string[];
aestheticDirectives: string[];
forbiddenDirectives: string[];
}
export interface RpgCreationPlayerEntryPointValue {
openingIdentity: string;
openingProblem: string;
entryMotivation: string;
}
export interface RpgCreationCoreConflictValue {
surfaceConflicts: string[];
hiddenCrisis: string;
firstTouchedConflict: string;
}
export interface RpgCreationKeyRelationshipValue {
pairs: string;
relationshipType: string;
secretOrCost: string;
}
export interface RpgCreationHiddenLineValue {
hiddenTruths: string[];
misdirectionHints: string[];
revealPacing: string;
}
export interface RpgCreationIconicElementValue {
iconicMotifs: string[];
institutionsOrArtifacts: string[];
hardRules: string[];
}
export interface RpgCreationAnchorContent {
worldPromise: RpgCreationWorldPromiseValue | null;
playerFantasy: RpgCreationPlayerFantasyValue | null;
themeBoundary: RpgCreationThemeBoundaryValue | null;
playerEntryPoint: RpgCreationPlayerEntryPointValue | null;
coreConflict: RpgCreationCoreConflictValue | null;
keyRelationships: RpgCreationKeyRelationshipValue[];
hiddenLines: RpgCreationHiddenLineValue | null;
iconicElements: RpgCreationIconicElementValue | null;
}

View File

@@ -0,0 +1,251 @@
/**
* RPG Agent 草稿与资产覆盖率契约。
* 这一层只描述 foundation draft、草稿卡片与资产状态不包含会话编排语义。
*/
export type RpgAgentDraftCardKind =
| 'world'
| 'camp'
| 'faction'
| 'character'
| 'landmark'
| 'thread'
| 'chapter'
| 'scene_chapter'
| 'carrier'
| 'sidequest_seed';
export type RpgAgentDraftCardStatus =
| 'suggested'
| 'confirmed'
| 'locked'
| 'warning';
export interface RpgAgentDraftCardSummary {
id: string;
kind: RpgAgentDraftCardKind;
title: string;
subtitle: string;
summary: string;
status: RpgAgentDraftCardStatus;
linkedIds: string[];
warningCount: number;
assetStatus?: RpgAgentRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export interface RpgAgentDraftCardDetailSection {
id: string;
label: string;
value: string;
}
export interface RpgAgentFoundationDraftFaction {
id: string;
name: string;
title?: string;
subtitle?: string;
publicGoal: string;
relatedConflict: string;
tension?: string;
playerRelation: string;
summary: string;
}
export interface RpgAgentFoundationDraftCharacter {
id: string;
name: string;
title: string;
role: string;
publicIdentity: string;
publicMask?: string;
currentPressure: string;
hiddenHook?: string;
relationToPlayer: string;
threadIds: string[];
summary: string;
skills?: Array<{
id: string;
name: string;
actionPreviewConfig?: Record<string, unknown> | null;
}>;
imageSrc?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
animationMap?: Record<string, unknown> | null;
}
export interface RpgAgentFoundationDraftLandmark {
id: string;
name: string;
description?: string;
purpose: string;
mood: string;
importance: string;
secret?: string;
dangerLevel?: string;
imageSrc?: string | null;
generatedSceneAssetId?: string | null;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
characterIds: string[];
threadIds: string[];
summary: string;
}
export interface RpgAgentFoundationDraftThread {
id: string;
title: string;
type: 'main' | 'hidden';
conflictType?: string;
conflict: string;
stakes?: string;
characterIds: string[];
landmarkIds: string[];
summary: string;
}
export interface RpgAgentFoundationDraftChapter {
id: string;
title: string;
openingEvent: string;
playerGoal: string;
characterIds: string[];
landmarkIds: string[];
understandingShift: string;
summary: string;
}
export interface RpgAgentFoundationDraftCamp {
id: string;
name: string;
description: string;
mood: string;
dangerLevel?: string;
imageSrc?: string | null;
generatedSceneAssetId?: string | null;
generatedScenePrompt?: string | null;
generatedSceneModel?: string | null;
summary: string;
}
export type RpgAgentSceneActStage =
| 'opening'
| 'expansion'
| 'turning_point'
| 'climax'
| 'aftermath';
export type RpgAgentSceneActAdvanceRule =
| 'after_primary_contact'
| 'after_active_step_complete'
| 'after_chapter_resolution';
export interface RpgAgentFoundationDraftSceneAct {
id: string;
title: string;
summary: string;
stageCoverage: RpgAgentSceneActStage[];
backgroundImageSrc?: string | null;
backgroundAssetId?: string | null;
encounterNpcIds: string[];
primaryNpcId: string;
linkedThreadIds: string[];
actGoal: string;
transitionHook: string;
advanceRule: RpgAgentSceneActAdvanceRule;
}
export interface RpgAgentFoundationDraftSceneChapter {
id: string;
sceneId: string;
sceneName: string;
title: string;
summary: string;
linkedThreadIds: string[];
linkedLandmarkIds: string[];
acts: RpgAgentFoundationDraftSceneAct[];
}
export interface RpgAgentFoundationDraftProfile {
name: string;
subtitle: string;
summary: string;
tone: string;
playerGoal: string;
majorFactions: string[];
coreConflicts: string[];
playableNpcs: RpgAgentFoundationDraftCharacter[];
storyNpcs: RpgAgentFoundationDraftCharacter[];
landmarks: RpgAgentFoundationDraftLandmark[];
camp?: RpgAgentFoundationDraftCamp | null;
themePack?: Record<string, unknown> | null;
storyGraph?: Record<string, unknown> | null;
factions: RpgAgentFoundationDraftFaction[];
threads: RpgAgentFoundationDraftThread[];
chapters: RpgAgentFoundationDraftChapter[];
sceneChapters: RpgAgentFoundationDraftSceneChapter[];
worldHook: string;
playerPremise: string;
openingSituation: string;
iconicElements: string[];
sourceAnchorSummary: string;
}
export interface RpgAgentFoundationDraftResult {
draftProfile: RpgAgentFoundationDraftProfile;
draftCards: RpgAgentDraftCardSummary[];
}
export interface RpgAgentDraftCardDetail {
id: string;
kind: RpgAgentDraftCardKind;
title: string;
sections: RpgAgentDraftCardDetailSection[];
linkedIds: string[];
locked: false;
editable: boolean;
editableSectionIds: string[];
warningMessages: string[];
assetStatus?: RpgAgentRoleAssetStatus | null;
assetStatusLabel?: string | null;
}
export type RpgAgentAssetPriorityTier = 'hero' | 'featured' | 'supporting';
export type RpgAgentRoleAssetStatus =
| 'missing'
| 'visual_ready'
| 'animations_ready'
| 'complete';
export interface RpgAgentRoleAssetSummary {
roleId: string;
roleName: string;
roleKind: 'playable' | 'story';
priorityTier: RpgAgentAssetPriorityTier;
portraitPath?: string | null;
generatedVisualAssetId?: string | null;
generatedAnimationSetId?: string | null;
status: RpgAgentRoleAssetStatus;
missingAnimations: string[];
nextPointCost: number;
}
export interface RpgAgentSceneAssetSummary {
sceneId: string;
sceneName: string;
actId?: string | null;
actTitle?: string | null;
imageSrc?: string | null;
assetId?: string | null;
status: 'missing' | 'ready';
nextPointCost: number;
}
export interface RpgAgentAssetCoverageSummary {
roleAssets: RpgAgentRoleAssetSummary[];
sceneAssets: RpgAgentSceneAssetSummary[];
allRoleAssetsReady: boolean;
allSceneAssetsReady: boolean;
}

View File

@@ -0,0 +1,134 @@
import type { RpgAgentActionResponse, RpgAgentOperationRecord, RpgAgentSupportedAction, RpgAgentSuggestedAction } from './rpgAgentActions';
import type { RpgCreationAnchorContent } from './rpgAgentAnchors';
import type { RpgAgentAssetCoverageSummary, RpgAgentDraftCardDetail, RpgAgentDraftCardSummary } from './rpgAgentDraft';
import type { RpgCreationPreviewEnvelope } from './rpgCreationPreview';
/**
* RPG Agent 会话层契约。
* 这里承载 session 真相源与会话编排元数据,同时预留 resultPreview 与 supportedActions 两个后续主链字段。
*/
export interface RpgCreationIntentReadiness {
isReady: boolean;
completedKeys: string[];
missingKeys: string[];
}
export interface RpgAgentPendingClarification {
id: string;
label: string;
question: string;
targetKey:
| 'world_hook'
| 'player_premise'
| 'theme_and_tone'
| 'core_conflict'
| 'relationship_seed'
| 'iconic_element';
priority: number;
answer?: string;
}
export type RpgAgentStage =
| 'collecting_intent'
| 'clarifying'
| 'foundation_review'
| 'object_refining'
| 'visual_refining'
| 'long_tail_review'
| 'ready_to_publish'
| 'published'
| 'error';
export type RpgAgentMessageRole = 'user' | 'assistant' | 'system';
export type RpgAgentMessageKind =
| 'chat'
| 'clarification'
| 'summary'
| 'checkpoint'
| 'warning'
| 'action_result';
export interface RpgAgentMessage {
id: string;
role: RpgAgentMessageRole;
kind: RpgAgentMessageKind;
text: string;
createdAt: string;
relatedOperationId?: string | null;
}
export interface RpgAgentQualityFinding {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}
export interface RpgAgentSessionSnapshot {
sessionId: string;
currentTurn: number;
anchorContent: RpgCreationAnchorContent;
progressPercent: number;
lastAssistantReply: string | null;
stage: RpgAgentStage;
focusCardId: string | null;
creatorIntent: Record<string, unknown> | null;
creatorIntentReadiness: RpgCreationIntentReadiness;
anchorPack: Record<string, unknown> | null;
lockState: Record<string, unknown> | null;
draftProfile: Record<string, unknown> | null;
messages: RpgAgentMessage[];
draftCards: RpgAgentDraftCardSummary[];
pendingClarifications: RpgAgentPendingClarification[];
suggestedActions: RpgAgentSuggestedAction[];
recommendedReplies: string[];
qualityFindings: RpgAgentQualityFinding[];
assetCoverage: RpgAgentAssetCoverageSummary;
/**
* checkpoint 元数据需要进入 session snapshot 主链,
* 这样前端后续才能拿到真实可回滚目标,而不是只能盲发 checkpointId。
*/
checkpoints?: Array<{
checkpointId: string;
createdAt: string;
label: string;
}>;
/**
* 后续由工作包 E 的 action registry 真实填充。
* 当前保持可选,确保主链迁移期间不影响旧 session snapshot。
*/
supportedActions?: RpgAgentSupportedAction[];
/**
* 后续由服务端 preview compiler 输出。
* 当前保持可选,允许前端兼容层继续走 legacy profile。
*/
resultPreview?: RpgCreationPreviewEnvelope | null;
updatedAt: string;
}
export interface CreateRpgAgentSessionRequest {
seedText?: string;
}
export interface CreateRpgAgentSessionResponse {
session: RpgAgentSessionSnapshot;
}
export interface SendRpgAgentMessageRequest {
clientMessageId: string;
text: string;
quickFillRequested?: boolean;
focusCardId?: string | null;
selectedCardIds?: string[];
}
export interface SendRpgAgentMessageResponse extends RpgAgentActionResponse {
operation: RpgAgentOperationRecord;
}
export interface GetRpgAgentCardDetailResponse {
card: RpgAgentDraftCardDetail;
}

View File

@@ -0,0 +1,146 @@
import { describe, expect, test } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from './customWorldAgentSession';
import type { CustomWorldResultPreviewEnvelope } from './customWorldResultPreview';
import type { CustomWorldWorkSummary } from './customWorldWorkSummary';
import {
createRpgAgentFoundationDraftProfileFixture,
createRpgAgentSupportedActionsFixture,
createRpgAgentSessionFixture,
createRpgCreationAnchorContentFixture,
createRpgCreationPreviewEnvelopeFixture,
createRpgCreationPublishedProfileFixture,
createRpgCreationWorksResponseFixture,
createRpgWorldLibraryEntryFixture,
} from './rpgCreationFixtures';
describe('RPG 创作共享契约 fixture', () => {
test('旧命名兼容分文件可以直接承接新 fixture 的类型消费', () => {
const legacySession: CustomWorldAgentSessionSnapshot =
createRpgAgentSessionFixture();
const legacyPreview: CustomWorldResultPreviewEnvelope =
createRpgCreationPreviewEnvelopeFixture();
const legacyWork: CustomWorldWorkSummary =
createRpgCreationWorksResponseFixture().items[0]!;
expect(legacySession.stage).toBe('ready_to_publish');
expect(legacySession.resultPreview?.source).toBe(legacyPreview.source);
expect(legacyWork.status).toBe('draft');
});
test('anchor fixture 与 foundation draft fixture 保持最小创作真相源对应关系', () => {
const anchors = createRpgCreationAnchorContentFixture();
const draftProfile = createRpgAgentFoundationDraftProfileFixture();
expect(anchors.worldPromise?.hook).toContain('旧航路群岛');
expect(draftProfile.worldHook).toContain('旧航路群岛');
expect(draftProfile.playableNpcs).toHaveLength(1);
expect(draftProfile.storyNpcs).toHaveLength(1);
expect(draftProfile.sceneChapters[0]?.acts[0]?.backgroundImageSrc).toContain(
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
);
});
test('session fixture 同时暴露 supportedActions 与 resultPreview', () => {
const session = createRpgAgentSessionFixture();
expect(session.sessionId).toBe('rpg-session-fixture');
expect(session.stage).toBe('ready_to_publish');
expect(session.checkpoints?.[0]?.checkpointId).toBe(
'checkpoint-foundation-v1',
);
expect(session.supportedActions?.map((entry) => entry.action)).toEqual(
expect.arrayContaining(['draft_foundation', 'generate_role_assets', 'publish_world']),
);
expect(session.resultPreview?.source).toBe('session_preview');
expect(session.resultPreview?.blockers).toEqual([]);
});
test('preview fixture 保持预览来源、质量结论与 profile 载体三层边界', () => {
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(preview.source).toBe('session_preview');
expect(preview.preview.previewId).toBe('preview-fixture-1');
expect(preview.preview.sessionId).toBe('rpg-session-fixture');
expect(preview.qualityFindings?.[0]).toMatchObject({
severity: 'info',
code: 'scene_asset_ready',
});
});
test('supported actions fixture 明确区分可执行能力矩阵,而不是让前端自行猜测按钮状态', () => {
const supportedActions = createRpgAgentSupportedActionsFixture();
expect(supportedActions).toEqual([
{ action: 'draft_foundation', enabled: true },
{ action: 'generate_role_assets', enabled: true },
{ action: 'publish_world', enabled: true },
]);
});
test('published profile fixture 能稳定承载作品库与结果页所需的封面、场景幕与角色资产字段', () => {
const profile = createRpgCreationPublishedProfileFixture();
expect(profile.id).toBe('rpg-profile-fixture');
expect(profile.playableNpcs).toHaveLength(1);
expect(profile.landmarks).toHaveLength(1);
expect(profile.sceneChapterBlueprints).toHaveLength(1);
expect(
(profile.sceneChapterBlueprints as Array<{ acts?: Array<{ backgroundImageSrc?: string }> }>)[0]
?.acts?.[0]?.backgroundImageSrc,
).toContain('/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png');
});
test('regression: session preview 与 published profile 需要共同保留角色动作资产和分幕背景字段', () => {
const session = createRpgAgentSessionFixture();
const publishedProfile = createRpgCreationPublishedProfileFixture();
const preview = createRpgCreationPreviewEnvelopeFixture();
expect(
((session.draftProfile as { playableNpcs?: Array<{ animationMap?: { run?: { basePath?: string } } }> })
.playableNpcs?.[0]?.animationMap?.run?.basePath ?? ''),
).toContain('/generated-characters/playable-1/animations/run');
expect(
((preview.preview.playableNpcs as Array<{ generatedAnimationSetId?: string }>)[0]
?.generatedAnimationSetId ?? ''),
).toBe('animation-set-playable-1');
expect(
((publishedProfile.sceneChapterBlueprints as Array<{
acts?: Array<{ backgroundAssetId?: string }>;
}>)[0]?.acts?.[0]?.backgroundAssetId ?? ''),
).toBe('scene-asset-runtime');
});
test('works fixture 与 library fixture 对齐同一 published profile', () => {
const works = createRpgCreationWorksResponseFixture();
const libraryEntry = createRpgWorldLibraryEntryFixture();
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(publishedWork?.profileId).toBe(libraryEntry.profileId);
expect(publishedWork?.title).toBe(libraryEntry.worldName);
expect(publishedWork?.canEnterWorld).toBe(true);
expect(libraryEntry.profile.id).toBe(libraryEntry.profileId);
});
test('regression: works fixture 需要稳定保留草稿与发布态的作品门槛字段', () => {
const works = createRpgCreationWorksResponseFixture();
const draftWork = works.items.find((entry) => entry.status === 'draft');
const publishedWork = works.items.find((entry) => entry.status === 'published');
expect(draftWork).toMatchObject({
stage: 'ready_to_publish',
stageLabel: '准备发布',
canResume: true,
canEnterWorld: false,
roleVisualReadyCount: 2,
roleAnimationReadyCount: 2,
});
expect(publishedWork).toMatchObject({
stage: 'published',
stageLabel: '已发布',
canResume: false,
canEnterWorld: true,
});
});
});

View File

@@ -0,0 +1,714 @@
import type {
CustomWorldLibraryEntry,
CustomWorldProfileRecord,
} from './runtime';
import type { RpgAgentSupportedAction } from './rpgAgentActions';
import type { RpgCreationAnchorContent } from './rpgAgentAnchors';
import type {
RpgAgentAssetCoverageSummary,
RpgAgentDraftCardSummary,
RpgAgentFoundationDraftProfile,
} from './rpgAgentDraft';
import type { RpgAgentSessionSnapshot } from './rpgAgentSession';
import type { RpgCreationPreviewEnvelope } from './rpgCreationPreview';
import type {
ListRpgCreationWorksResponse,
RpgCreationWorkSummary,
} from './rpgCreationWorkSummary';
const RPG_CREATION_FIXTURE_SESSION_ID = 'rpg-session-fixture';
const RPG_CREATION_FIXTURE_PROFILE_ID = 'rpg-profile-fixture';
const RPG_CREATION_FIXTURE_USER_ID = 'fixture-user';
const RPG_CREATION_FIXTURE_UPDATED_AT = '2026-04-21T09:30:00.000Z';
const RPG_CREATION_FIXTURE_PUBLISHED_AT = '2026-04-21T10:00:00.000Z';
function cloneFixture<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
/**
* 共享八锚点 fixture。
* 用于 contract test、session fixture 和 works 集成测试复用同一份创作意图样本。
*/
export function createRpgCreationAnchorContentFixture(): RpgCreationAnchorContent {
return cloneFixture({
worldPromise: {
hook: '被海雾吞没的旧航路群岛',
differentiator: '灯塔与禁航令共同决定谁能活着穿过去。',
desiredExperience: '压抑、悬疑、潮湿',
},
playerFantasy: {
playerRole: '玩家回到群岛调查沉船真相。',
corePursuit: '找出失控航路背后的真相。',
fearOfLoss: '失去最后一个还能对上旧案的人。',
},
themeBoundary: {
toneKeywords: ['压抑', '潮湿', '悬疑'],
aestheticDirectives: ['旧灯塔', '潮雾', '断裂航路'],
forbiddenDirectives: ['不要出现现代枪械'],
},
playerEntryPoint: {
openingIdentity: '被迫返乡的失职守灯人',
openingProblem: '首夜就有陌生船只闯入禁航区。',
entryMotivation: '查清沉船夜里被谁改动了灯册。',
},
coreConflict: {
surfaceConflicts: ['守灯会与航运公会争夺旧航路控制权'],
hiddenCrisis: '沉船夜的航灯与灯册被人动过手脚。',
firstTouchedConflict: '玩家开局就会撞上新的封航命令。',
},
keyRelationships: [
{
pairs: '玩家 / 沈砺',
relationshipType: '旧友兼潜在背叛者',
secretOrCost: '沈砺暗地里在替沉船商盟引路。',
},
],
hiddenLines: {
hiddenTruths: ['沉船夜的真实失误并不是单纯天灾。'],
misdirectionHints: ['所有人都会先把问题推给潮雾本身。'],
revealPacing: '第一章露出痕迹,第二章才让玩家摸到灯册线。',
},
iconicElements: {
iconicMotifs: ['会移动的海雾'],
institutionsOrArtifacts: ['回潮旧灯塔', '封灯令', '旧潮图'],
hardRules: ['禁航信号一旦点亮,任何船都必须退航。'],
},
} satisfies RpgCreationAnchorContent);
}
/**
* 共享 foundation draft fixture。
* 这份样本同时服务 session 草稿、preview 适配回归测试和 works 聚合测试。
*/
export function createRpgAgentFoundationDraftProfileFixture(): RpgAgentFoundationDraftProfile {
return cloneFixture({
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [
{
id: 'playable-1',
name: '沈砺',
title: '旧航路引路人',
role: '关键同行者',
publicIdentity: '最熟悉旧航路的人。',
publicMask: '看上去像可靠旧友。',
currentPressure: '他必须在两股势力间站队。',
hiddenHook: '暗中替沉船商盟引路。',
relationToPlayer: '旧友兼潜在背叛者',
threadIds: ['thread-1'],
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
skills: [
{
id: 'skill-playable-1',
name: '潮行引路',
actionPreviewConfig: {
basePath:
'/generated-characters/playable-1/animations/skills/skill-playable-1',
},
},
],
imageSrc:
'/generated-characters/playable-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-playable',
generatedAnimationSetId: 'animation-set-playable-1',
animationMap: {
idle: {
basePath: '/generated-characters/playable-1/animations/idle',
},
run: {
basePath: '/generated-characters/playable-1/animations/run',
},
attack: {
basePath: '/generated-characters/playable-1/animations/attack',
},
},
},
],
storyNpcs: [
{
id: 'story-1',
name: '顾潮音',
title: '守灯会值夜人',
role: '场景关键角色',
publicIdentity: '负责夜间巡灯与封锁。',
publicMask: '对外一直冷静克制。',
currentPressure: '她知道更多禁航区真相。',
hiddenHook: '曾亲眼见过失控海雾吞船。',
relationToPlayer: '最早愿意交换线索的人',
threadIds: ['thread-1'],
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
skills: [
{
id: 'skill-story-1',
name: '夜潮灯语',
actionPreviewConfig: {
basePath:
'/generated-characters/story-1/animations/skills/skill-story-1',
},
},
],
imageSrc:
'/generated-characters/story-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-story',
generatedAnimationSetId: 'animation-set-story-1',
animationMap: {
run: {
basePath: '/generated-characters/story-1/animations/run',
},
attack: {
basePath: '/generated-characters/story-1/animations/attack',
},
},
},
],
landmarks: [
{
id: 'landmark-1',
name: '回潮旧灯塔',
description: '被海雾啃旧的石塔仍在夜里维持着微弱灯火。',
purpose: '观察雾潮与往来船只',
mood: '潮湿、压抑、风声不止',
importance: '开局核心场景',
secret: '高处潮痕说明海面异常抬升过。',
dangerLevel: 'high',
imageSrc: '/generated-custom-world-scenes/landmark-1/latest-scene.png',
generatedSceneAssetId: 'scene-asset-landmark-1',
characterIds: ['story-1'],
threadIds: ['thread-1'],
summary: '旧灯塔是整片群岛最先看见异动的地方。',
},
],
camp: {
id: 'camp-1',
name: '回潮暂栖所',
description: '能暂时收拢队伍、整理灯册与潮路线索的落脚点。',
mood: '克制、紧绷,但还能暂时收拢局势',
dangerLevel: 'low',
imageSrc: '/custom/camp/huichao.png',
generatedSceneAssetId: 'scene-asset-camp-1',
summary: '玩家能在这里整理情报、回看旧灯册和沉船名单。',
},
themePack: {
id: 'theme-pack:tide',
displayName: '潮雾悬疑',
},
storyGraph: {
visibleThreads: [
{
id: 'thread-visible-1',
title: '封航争夺',
},
],
},
factions: [
{
id: 'faction-1',
name: '守灯会',
title: '守灯会',
subtitle: '把控禁航灯令的人',
publicGoal: '维持封航秩序并压住灯册流出。',
relatedConflict: '想把旧案继续压在禁航记录之下。',
tension: '他们越强调规矩,越像在遮掩灯册。',
playerRelation: '玩家迟早要与他们正面冲突。',
summary: '掌握灯塔与封航令的势力,也是最怕旧案被翻出来的一方。',
},
],
threads: [
{
id: 'thread-1',
title: '沉船旧案',
type: 'main',
conflictType: '真相遮蔽',
conflict: '沉船夜的航灯与灯册被人动过手脚。',
stakes: '真相一旦坐实,群岛秩序会先崩。',
characterIds: ['playable-1', 'story-1'],
landmarkIds: ['landmark-1'],
summary: '玩家会从灯塔高处潮痕一路追到沉船夜的真相。',
},
],
chapters: [
{
id: 'chapter-1',
title: '灯塔回潮',
openingEvent: '禁航区闯入了一艘不该出现的陌生船。',
playerGoal: '先稳住局势,再拿到第一份灯册线索。',
characterIds: ['playable-1', 'story-1'],
landmarkIds: ['landmark-1'],
understandingShift: '玩家会意识到沉船旧案至今仍在操控群岛秩序。',
summary: '第一章聚焦灯塔与封航令,给玩家一条可追的旧案线索。',
},
],
sceneChapters: [
{
id: 'scene-chapter-1',
sceneId: 'landmark-1',
sceneName: '回潮旧灯塔',
title: '灯塔初章',
summary: '围绕灯塔推进的首个场景章节。',
linkedThreadIds: ['thread-1'],
linkedLandmarkIds: ['landmark-1'],
acts: [
{
id: 'scene-act-1',
title: '第一幕',
summary: '先接住回潮灯塔的入口压力。',
stageCoverage: ['opening'],
backgroundImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
backgroundAssetId: 'scene-asset-runtime',
encounterNpcIds: ['story-1'],
primaryNpcId: 'story-1',
linkedThreadIds: ['thread-1'],
actGoal: '接住首幕入口',
transitionHook: '向第二幕推进。',
advanceRule: 'after_primary_contact',
},
],
},
],
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
openingSituation: '首夜就有陌生船只闯入禁航区。',
iconicElements: ['会移动的海雾', '回潮旧灯塔'],
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
} satisfies RpgAgentFoundationDraftProfile);
}
function createRpgAgentDraftCardsFixture(): RpgAgentDraftCardSummary[] {
return cloneFixture([
{
id: 'world-foundation',
kind: 'world',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
status: 'suggested',
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
warningCount: 0,
},
{
id: 'playable-1',
kind: 'character',
title: '沈砺',
subtitle: '旧航路引路人 / 动作已齐',
summary: '最熟悉旧航路的人,也可能是最危险的旧友。',
status: 'suggested',
linkedIds: ['thread-1', 'landmark-1'],
warningCount: 0,
assetStatus: 'complete',
assetStatusLabel: '动作已齐',
},
{
id: 'landmark-1',
kind: 'landmark',
title: '回潮旧灯塔',
subtitle: '观察雾潮与往来船只',
summary: '旧灯塔是整片群岛最先看见异动的地方。',
status: 'suggested',
linkedIds: ['story-1', 'thread-1'],
warningCount: 0,
},
] satisfies RpgAgentDraftCardSummary[]);
}
function createRpgAgentAssetCoverageFixture(): RpgAgentAssetCoverageSummary {
return cloneFixture({
roleAssets: [
{
roleId: 'playable-1',
roleName: '沈砺',
roleKind: 'playable',
priorityTier: 'hero',
portraitPath:
'/generated-characters/playable-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-playable',
generatedAnimationSetId: 'animation-set-playable-1',
status: 'complete',
missingAnimations: [],
nextPointCost: 0,
},
{
roleId: 'story-1',
roleName: '顾潮音',
roleKind: 'story',
priorityTier: 'featured',
portraitPath:
'/generated-characters/story-1/visual/asset-runtime/master.png',
generatedVisualAssetId: 'asset-runtime-story',
generatedAnimationSetId: 'animation-set-story-1',
status: 'complete',
missingAnimations: [],
nextPointCost: 0,
},
],
sceneAssets: [
{
sceneId: 'landmark-1',
sceneName: '回潮旧灯塔',
actId: 'scene-act-1',
actTitle: '第一幕',
imageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
assetId: 'scene-asset-runtime',
status: 'ready',
nextPointCost: 0,
},
],
allRoleAssetsReady: true,
allSceneAssetsReady: true,
} satisfies RpgAgentAssetCoverageSummary);
}
/**
* 已发布 profile fixture。
* 用于 preview compiler、works 聚合和 library 元数据解析测试。
*/
export function createRpgCreationPublishedProfileFixture(): CustomWorldProfileRecord {
const draft = createRpgAgentFoundationDraftProfileFixture();
return cloneFixture({
id: RPG_CREATION_FIXTURE_PROFILE_ID,
settingText: draft.worldHook,
name: draft.name,
subtitle: draft.subtitle,
summary: draft.summary,
tone: draft.tone,
playerGoal: draft.playerGoal,
templateWorldType: 'WUXIA',
compatibilityTemplateWorldType: 'WUXIA',
majorFactions: draft.majorFactions,
coreConflicts: draft.coreConflicts,
playableNpcs: draft.playableNpcs.map((role) => ({
id: role.id,
name: role.name,
title: role.title,
role: role.role,
description: role.publicIdentity,
backstory: role.hiddenHook || role.summary,
personality: role.publicMask || role.summary,
motivation: role.currentPressure,
combatStyle: '借地形和潮路换位,先拉扯再压近。',
initialAffinity: 18,
relationshipHooks: [role.relationToPlayer],
tags: ['潮路', '旧案'],
imageSrc: role.imageSrc,
generatedVisualAssetId: role.generatedVisualAssetId,
generatedAnimationSetId: role.generatedAnimationSetId,
animationMap: role.animationMap,
skills: [
{
id: 'skill-playable-1',
name: '潮行引路',
summary: '踩着旧潮阶切线前压,替队伍打开角度。',
style: '机动周旋',
},
],
templateCharacterId: 'archer-hero',
})),
storyNpcs: draft.storyNpcs.map((role) => ({
id: role.id,
name: role.name,
title: role.title,
role: role.role,
description: role.publicIdentity,
backstory: role.hiddenHook || role.summary,
personality: role.publicMask || role.summary,
motivation: role.currentPressure,
combatStyle: '借塔顶视角和风向压制,再用灯火错位扰乱。',
initialAffinity: 8,
relationshipHooks: [role.relationToPlayer],
tags: ['守灯会', '灯塔'],
imageSrc: role.imageSrc,
generatedVisualAssetId: role.generatedVisualAssetId,
skills: [
{
id: 'skill-story-1',
name: '夜潮灯语',
summary: '借灯语与潮声干扰对方判断。',
style: '起手压制',
},
],
})),
camp: {
name: draft.camp?.name,
description: draft.camp?.description,
dangerLevel: draft.camp?.dangerLevel,
imageSrc: draft.camp?.imageSrc,
},
landmarks: draft.landmarks.map((landmark) => ({
id: landmark.id,
name: landmark.name,
description: landmark.description,
dangerLevel: landmark.dangerLevel,
imageSrc: landmark.imageSrc,
sceneNpcIds: landmark.characterIds,
connections: [
{
targetLandmarkId: 'landmark-1',
relativePosition: 'forward',
summary: '沿着旧潮阶继续前压到雾栈尽头。',
},
],
})),
cover: {
sourceType: 'default',
characterRoleIds: ['playable-1'],
},
sceneChapterBlueprints: draft.sceneChapters.map((chapter) => ({
id: chapter.id,
sceneId: chapter.sceneId,
sceneName: chapter.sceneName,
title: chapter.title,
summary: chapter.summary,
acts: chapter.acts.map((act) => ({
id: act.id,
title: act.title,
summary: act.summary,
backgroundImageSrc: act.backgroundImageSrc,
backgroundAssetId: act.backgroundAssetId,
encounterNpcIds: act.encounterNpcIds,
primaryNpcId: act.primaryNpcId,
actGoal: act.actGoal,
transitionHook: act.transitionHook,
})),
})),
themePack: draft.themePack,
storyGraph: draft.storyGraph,
scenarioPackId: 'scenario-pack:tide',
campaignPackId: 'campaign-pack:tide',
generationMode: 'fast',
generationStatus: 'key_only',
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
} satisfies CustomWorldProfileRecord);
}
export function createRpgCreationPreviewEnvelopeFixture(): RpgCreationPreviewEnvelope {
return cloneFixture({
preview: {
...createRpgCreationPublishedProfileFixture(),
previewId: 'preview-fixture-1',
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
},
source: 'session_preview',
generatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
qualityFindings: [
{
id: 'finding-scene-asset-ready',
severity: 'info',
code: 'scene_asset_ready',
targetId: 'scene-act-1',
message: '首幕背景图已经就绪,可直接用于结果页预览。',
},
],
blockers: [],
publishReady: true,
canEnterWorld: false,
} satisfies RpgCreationPreviewEnvelope);
}
export function createRpgAgentSupportedActionsFixture(): RpgAgentSupportedAction[] {
return cloneFixture([
{
action: 'draft_foundation',
enabled: true,
},
{
action: 'generate_role_assets',
enabled: true,
},
{
action: 'publish_world',
enabled: true,
},
] satisfies RpgAgentSupportedAction[]);
}
/**
* 共享 session snapshot fixture。
* 默认模拟“底稿、预览、资产都已准备好”的 ready_to_publish 状态。
*/
export function createRpgAgentSessionFixture(): RpgAgentSessionSnapshot {
const draftProfile = createRpgAgentFoundationDraftProfileFixture();
return cloneFixture({
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
currentTurn: 6,
anchorContent: createRpgCreationAnchorContentFixture(),
progressPercent: 100,
lastAssistantReply: '八锚点与底稿都已经齐备,可以进入结果页收口。',
stage: 'ready_to_publish',
focusCardId: null,
creatorIntent: {
sourceMode: 'card',
rawSettingText: draftProfile.worldHook,
worldHook: draftProfile.worldHook,
playerPremise: draftProfile.playerPremise,
themeKeywords: ['海雾', '旧航路'],
toneDirectives: ['压抑', '悬疑'],
openingSituation: draftProfile.openingSituation,
coreConflicts: draftProfile.coreConflicts,
keyFactions: ['守灯会'],
keyCharacters: ['沈砺', '顾潮音'],
keyLandmarks: ['回潮旧灯塔'],
iconicElements: draftProfile.iconicElements,
forbiddenDirectives: ['不要出现现代枪械'],
},
creatorIntentReadiness: {
isReady: true,
completedKeys: [
'world_hook',
'player_premise',
'theme_and_tone',
'core_conflict',
'relationship_seed',
'iconic_element',
],
missingKeys: [],
},
anchorPack: {
summary: draftProfile.sourceAnchorSummary,
},
lockState: {
lockedCardIds: ['world-foundation'],
},
draftProfile,
messages: [
{
id: 'message-1',
role: 'assistant',
kind: 'summary',
text: '世界底稿已整理完成,建议进入结果页确认资产与发布门槛。',
createdAt: RPG_CREATION_FIXTURE_UPDATED_AT,
relatedOperationId: null,
},
],
draftCards: createRpgAgentDraftCardsFixture(),
pendingClarifications: [],
suggestedActions: [
{
id: 'action-publish',
type: 'publish_world',
label: '发布世界',
},
],
recommendedReplies: ['先看结果页', '继续精修角色关系'],
qualityFindings: [
{
id: 'finding-scene-asset-ready',
severity: 'info',
code: 'scene_asset_ready',
targetId: 'scene-act-1',
message: '首幕背景图已经就绪,可直接用于结果页预览。',
},
],
assetCoverage: createRpgAgentAssetCoverageFixture(),
checkpoints: [
{
checkpointId: 'checkpoint-foundation-v1',
createdAt: RPG_CREATION_FIXTURE_UPDATED_AT,
label: '世界底稿 V1',
},
],
supportedActions: createRpgAgentSupportedActionsFixture(),
resultPreview: createRpgCreationPreviewEnvelopeFixture(),
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
} satisfies RpgAgentSessionSnapshot);
}
export function createRpgWorldLibraryEntryFixture(): CustomWorldLibraryEntry<CustomWorldProfileRecord> {
const profile = createRpgCreationPublishedProfileFixture();
return cloneFixture({
ownerUserId: RPG_CREATION_FIXTURE_USER_ID,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
profile,
visibility: 'published',
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
authorDisplayName: '测试玩家',
worldName: String(profile.name ?? '潮雾列岛'),
subtitle: String(profile.subtitle ?? '旧灯塔与失控航路'),
summaryText: String(profile.summary ?? '第一版世界底稿已经整理完成。'),
coverImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
themeMode: 'tide',
playableNpcCount: Array.isArray(profile.playableNpcs)
? profile.playableNpcs.length
: 0,
landmarkCount: Array.isArray(profile.landmarks)
? profile.landmarks.length
: 0,
} satisfies CustomWorldLibraryEntry<CustomWorldProfileRecord>);
}
export function createRpgCreationWorksResponseFixture(): ListRpgCreationWorksResponse {
return cloneFixture({
items: [
{
workId: `draft:${RPG_CREATION_FIXTURE_SESSION_ID}`,
sourceType: 'agent_session',
status: 'draft',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
coverImageSrc: '/custom/camp/huichao.png',
coverRenderMode: 'scene_with_roles',
coverCharacterImageSrcs: [
'/generated-characters/playable-1/visual/asset-runtime/master.png',
],
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: null,
stage: 'ready_to_publish',
stageLabel: '准备发布',
playableNpcCount: 2,
landmarkCount: 1,
roleVisualReadyCount: 2,
roleAnimationReadyCount: 2,
roleAssetSummaryLabel: '沈砺 · 动作已就绪',
sessionId: RPG_CREATION_FIXTURE_SESSION_ID,
profileId: null,
canResume: true,
canEnterWorld: false,
blockerCount: 0,
publishReady: true,
},
{
workId: `published:${RPG_CREATION_FIXTURE_PROFILE_ID}`,
sourceType: 'published_profile',
status: 'published',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。',
coverImageSrc:
'/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png',
coverRenderMode: 'scene_with_roles',
coverCharacterImageSrcs: [
'/generated-characters/playable-1/visual/asset-runtime/master.png',
],
updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT,
publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT,
stage: 'published',
stageLabel: '已发布',
playableNpcCount: 1,
landmarkCount: 1,
roleVisualReadyCount: 1,
roleAnimationReadyCount: 1,
roleAssetSummaryLabel: '动作已就绪 1',
sessionId: null,
profileId: RPG_CREATION_FIXTURE_PROFILE_ID,
canResume: false,
canEnterWorld: true,
blockerCount: 0,
publishReady: true,
},
] satisfies RpgCreationWorkSummary[],
} satisfies ListRpgCreationWorksResponse);
}

View File

@@ -0,0 +1,40 @@
import type { CustomWorldProfileRecord } from './runtime';
/**
* 结果页预览契约。
* 当前 preview 仍以兼容 profile 作为承载体,但已经把来源、阻断项和质量结论从 session 草稿里显式剥离出来。
*/
export type RpgCreationPreviewSource =
| 'session_preview'
| 'published_profile';
export interface RpgCreationPreviewFinding {
id: string;
severity: 'info' | 'warning' | 'blocker';
code: string;
targetId?: string | null;
message: string;
}
export interface RpgCreationPreviewBlocker {
id: string;
code: string;
message: string;
}
export type RpgCreationPreview = CustomWorldProfileRecord & {
previewId?: string;
sessionId?: string | null;
profileId?: string | null;
};
export interface RpgCreationPreviewEnvelope {
preview: RpgCreationPreview;
source: RpgCreationPreviewSource;
generatedAt?: string;
qualityFindings?: RpgCreationPreviewFinding[];
blockers?: RpgCreationPreviewBlocker[];
publishReady?: boolean;
canEnterWorld?: boolean;
}

View File

@@ -0,0 +1,38 @@
/**
* RPG 创作作品卡读模型契约。
* works 列表只暴露继续创作与进入世界判断所需的稳定字段。
*/
export type RpgCreationWorkStatus = 'draft' | 'published';
export type RpgCreationWorkSource = 'agent_session' | 'published_profile';
export interface RpgCreationWorkSummary {
workId: string;
sourceType: RpgCreationWorkSource;
status: RpgCreationWorkStatus;
title: string;
subtitle: string;
summary: string;
coverImageSrc?: string | null;
coverRenderMode?: 'image' | 'scene_with_roles';
coverCharacterImageSrcs?: string[];
updatedAt: string;
publishedAt?: string | null;
stage?: string | null;
stageLabel?: string | null;
playableNpcCount: number;
landmarkCount: number;
roleVisualReadyCount?: number;
roleAnimationReadyCount?: number;
roleAssetSummaryLabel?: string | null;
sessionId?: string | null;
profileId?: string | null;
canResume: boolean;
canEnterWorld: boolean;
blockerCount?: number;
publishReady?: boolean;
}
export interface ListRpgCreationWorksResponse {
items: RpgCreationWorkSummary[];
}

View File

@@ -0,0 +1,184 @@
/**
* RPG 运行时聊天相关共享契约。
* 将角色聊天、NPC 对话与轻量 story 请求载荷从旧 story.ts 中独立出来。
*/
import type { JsonObject } from './common';
export type NpcChatTurnLimitReason = 'negative_affinity';
export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close';
export type NpcChatTurnDirective = {
sceneActId?: string | null;
turnLimit?: number | null;
remainingTurns?: number | null;
limitReason?: NpcChatTurnLimitReason | null;
closingMode?: NpcChatTurnClosingMode | null;
forceExitAfterTurn?: boolean;
};
export type NpcChatTurnCompletionDirective = {
turnLimit?: number | null;
remainingTurns?: number | null;
forceExit?: boolean;
closingMode?: NpcChatTurnClosingMode;
};
export type CharacterChatReplyRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
playerMessage: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSuggestionsRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSummaryRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
previousSummary: string;
targetStatus: TTargetStatus;
};
export type NpcChatDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
topic: string;
resultSummary: string;
npcInitiatesConversation?: boolean;
};
export type NpcChatTurnRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TCombatContext = unknown,
TNpcState = unknown,
TQuestOfferState = unknown,
TQuestOfferEncounter = unknown,
TChatDirective = NpcChatTurnDirective,
> = {
worldType: string;
character?: TCharacter;
player?: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
conversationHistory?: TConversationTurn[];
dialogue?: TConversationTurn[];
combatContext?: TCombatContext | null;
playerMessage: string;
npcState: TNpcState;
npcInitiatesConversation?: boolean;
questOfferContext?: {
state: TQuestOfferState;
encounter: TQuestOfferEncounter;
turnCount: number;
} | null;
chatDirective?: TChatDirective | null;
};
export type NpcChatPendingQuestOffer<TQuest = unknown> = {
quest: TQuest;
introText?: string;
};
export type NpcChatTurnResult<TQuest = unknown> = {
npcReply: string;
affinityDelta: number;
affinityText: string;
suggestions: string[];
pendingQuestOffer?: NpcChatPendingQuestOffer<TQuest> | null;
chatDirective?: NpcChatTurnCompletionDirective | null;
};
export type NpcRecruitDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
invitationText: string;
recruitSummary: string;
};
export type StoryRequestOptionsPayload = {
availableOptions?: JsonObject[];
optionCatalog?: JsonObject[];
};
export type StoryRequestPayload<TWorldType extends string = string> = {
worldType: TWorldType;
character: JsonObject;
monsters?: JsonObject[];
history?: JsonObject[];
choice?: string;
context: JsonObject;
requestOptions?: StoryRequestOptionsPayload;
};
export type PlainTextPromptRequest = {
systemPrompt: string;
userPrompt: string;
};
export type PlainTextResponse = {
text: string;
};

View File

@@ -0,0 +1,51 @@
import { describe, expect, test } from 'vitest';
import type { CharacterChatReplyRequest } from './rpgRuntimeChat';
import { QUEST_NARRATIVE_TYPES } from './rpgRuntimeQuestAssist';
import {
SERVER_RUNTIME_FUNCTION_IDS,
TASK5_RUNTIME_OPTION_SCOPES,
TASK6_RUNTIME_FUNCTION_IDS,
type RuntimeStoryActionRequest,
} from './rpgRuntimeStoryAction';
import type { RuntimeStoryStateRequest } from './rpgRuntimeStoryState';
describe('RPG runtime shared contracts', () => {
test('拆分后的 runtime story action 契约继续导出常量与类型', () => {
expect(SERVER_RUNTIME_FUNCTION_IDS).toContain('npc_chat');
expect(TASK6_RUNTIME_FUNCTION_IDS).toContain('npc_trade');
expect(TASK5_RUNTIME_OPTION_SCOPES).toEqual(['story', 'combat', 'npc']);
const request: RuntimeStoryActionRequest = {
sessionId: 'runtime-session-1',
action: {
type: 'story_choice',
functionId: 'npc_chat',
},
};
expect(request.action.functionId).toBe('npc_chat');
});
test('拆分后的 chat 与 quest assist 契约继续导出运行时类型', () => {
const payload: CharacterChatReplyRequest = {
worldType: 'WUXIA',
playerCharacter: {},
targetCharacter: {},
storyHistory: [],
context: {},
conversationHistory: [],
conversationSummary: '测试摘要',
playerMessage: '近况如何?',
targetStatus: {},
};
const stateRequest: RuntimeStoryStateRequest = {
sessionId: 'runtime-session-2',
};
expect(payload.playerMessage).toBe('近况如何?');
expect(stateRequest.sessionId).toBe('runtime-session-2');
expect(QUEST_NARRATIVE_TYPES).toContain('relationship');
});
});

View File

@@ -0,0 +1,83 @@
/**
* RPG 运行时任务辅助与道具意图共享契约。
* 该文件只承载 quest / runtime item 辅助类型,不混入 runtime story 主状态。
*/
import type { JsonObject } from './common';
export const QUEST_NARRATIVE_TYPES = [
'bounty',
'escort',
'investigation',
'retrieval',
'relationship',
'trial',
] as const;
export type SharedQuestNarrativeType = (typeof QUEST_NARRATIVE_TYPES)[number];
export const QUEST_OBJECTIVE_KINDS = [
'defeat_hostile_npc',
'inspect_treasure',
'spar_with_npc',
'talk_to_npc',
'reach_scene',
'deliver_item',
] as const;
export type SharedQuestObjectiveKind = (typeof QUEST_OBJECTIVE_KINDS)[number];
export const QUEST_URGENCY_LEVELS = ['low', 'medium', 'high'] as const;
export type SharedQuestUrgency = (typeof QUEST_URGENCY_LEVELS)[number];
export const QUEST_INTIMACY_LEVELS = [
'transactional',
'cooperative',
'trust_based',
] as const;
export type SharedQuestIntimacy = (typeof QUEST_INTIMACY_LEVELS)[number];
export const QUEST_REWARD_THEMES = [
'currency',
'resource',
'relationship',
'intel',
'rare_item',
] as const;
export type SharedQuestRewardTheme = (typeof QUEST_REWARD_THEMES)[number];
export const RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES = [
'heal',
'mana',
'cooldown',
'guard',
'damage',
] as const;
export type SharedRuntimeItemFunctionalBias =
(typeof RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES)[number];
export const RUNTIME_ITEM_TONE_VALUES = [
'grim',
'mysterious',
'martial',
'ritual',
'survival',
] as const;
export type SharedRuntimeItemTone = (typeof RUNTIME_ITEM_TONE_VALUES)[number];
export type RuntimeItemIntentRequest<
TContext = JsonObject,
TPlan = JsonObject,
> = {
context: TContext;
plans: TPlan[];
};
export type RuntimeItemIntentResponse<TIntent = JsonObject> = {
intents: TIntent[];
};
export type QuestGenerationRequest<
TState = JsonObject,
TEncounter = JsonObject,
> = {
state: TState;
encounter: TEncounter;
};

View File

@@ -0,0 +1,136 @@
/**
* RPG runtime story 动作层共享契约。
* 将 function id、动作请求与交互元数据从旧 story.ts 中单独收口。
*/
import type { JsonObject } from './common';
export type RuntimeAction<
TType extends string = string,
TPayload = JsonObject,
> = {
type: TType;
functionId?: string;
targetId?: string;
payload?: TPayload;
};
export type RuntimeActionRequest<
TAction extends RuntimeAction = RuntimeAction,
> = {
sessionId: string;
clientVersion?: number;
action: TAction;
};
export type RuntimeActionResponse<
TViewModel = JsonObject,
TPresentation = JsonObject,
TPatch = JsonObject,
> = {
sessionId: string;
serverVersion: number;
viewModel: TViewModel;
presentation: TPresentation;
patches: TPatch[];
};
export const TASK5_RUNTIME_FUNCTION_IDS = [
'story_continue_adventure',
'story_opening_camp_dialogue',
'camp_travel_home_scene',
'idle_call_out',
'idle_explore_forward',
'idle_observe_signs',
'idle_rest_focus',
'idle_travel_next_scene',
'battle_attack_basic',
'battle_use_skill',
'battle_all_in_crush',
'battle_escape_breakout',
'battle_feint_step',
'battle_finisher_window',
'battle_guard_break',
'battle_probe_pressure',
'battle_recover_breath',
'npc_chat',
'npc_fight',
'npc_help',
'npc_leave',
'npc_preview_talk',
'npc_recruit',
'npc_spar',
] as const;
export type Task5RuntimeFunctionId =
(typeof TASK5_RUNTIME_FUNCTION_IDS)[number];
export const TASK6_RUNTIME_FUNCTION_IDS = [
'equipment_equip',
'equipment_unequip',
'forge_craft',
'forge_dismantle',
'forge_reforge',
'inventory_use',
'npc_gift',
'npc_chat_quest_offer_abandon',
'npc_chat_quest_offer_replace',
'npc_chat_quest_offer_view',
'npc_quest_accept',
'npc_quest_turn_in',
'npc_trade',
'treasure_inspect',
'treasure_leave',
'treasure_secure',
] as const;
export type Task6RuntimeFunctionId =
(typeof TASK6_RUNTIME_FUNCTION_IDS)[number];
export const SERVER_RUNTIME_FUNCTION_IDS = [
...TASK5_RUNTIME_FUNCTION_IDS,
...TASK6_RUNTIME_FUNCTION_IDS,
] as const;
export type ServerRuntimeFunctionId =
(typeof SERVER_RUNTIME_FUNCTION_IDS)[number];
export const TASK5_RUNTIME_OPTION_SCOPES = ['story', 'combat', 'npc'] as const;
export type Task5RuntimeOptionScope =
(typeof TASK5_RUNTIME_OPTION_SCOPES)[number];
export type RuntimeStoryChoicePayload = JsonObject & {
optionText?: string;
note?: string;
releaseNpcId?: string;
preludeText?: string;
};
export type RuntimeStoryOptionInteraction =
| {
kind: 'npc';
npcId: string;
action:
| 'chat'
| 'help'
| 'fight'
| 'leave'
| 'quest_offer_abandon'
| 'quest_offer_replace'
| 'quest_offer_view'
| 'recruit'
| 'spar'
| 'trade'
| 'gift'
| 'quest_accept'
| 'quest_turn_in';
questId?: string;
}
| {
kind: 'treasure';
action: 'inspect' | 'leave' | 'secure';
};
export type RuntimeStoryChoiceAction = RuntimeAction<
'story_choice',
RuntimeStoryChoicePayload
> & {
functionId: string;
targetId?: string;
};

View File

@@ -0,0 +1,146 @@
/**
* RPG runtime story 状态与响应共享契约。
* 该文件只负责 view model、presentation、patch 与 snapshot 回包结构。
*/
import type { JsonObject } from './common';
import type { SavedGameSnapshot, SavedGameSnapshotInput } from './runtime';
import type {
RuntimeActionRequest,
RuntimeActionResponse,
RuntimeStoryChoiceAction,
RuntimeStoryChoicePayload,
RuntimeStoryOptionInteraction,
Task5RuntimeOptionScope,
} from './rpgRuntimeStoryAction';
export type RuntimeStoryOptionView = {
functionId: string;
actionText: string;
detailText?: string;
scope: Task5RuntimeOptionScope;
interaction?: RuntimeStoryOptionInteraction;
payload?: RuntimeStoryChoicePayload;
disabled?: boolean;
reason?: string;
};
export type RuntimeStoryPlayerViewModel = {
hp: number;
maxHp: number;
mana: number;
maxMana: number;
};
export type RuntimeStoryCompanionViewModel = {
npcId: string;
characterId?: string;
joinedAtAffinity: number;
};
export type RuntimeStoryEncounterViewModel = {
id: string;
kind: 'npc' | 'treasure';
npcName: string;
hostile: boolean;
affinity?: number;
recruited?: boolean;
interactionActive: boolean;
battleMode?: 'fight' | 'spar' | null;
};
export type RuntimeStoryStatusViewModel = {
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
};
export type RuntimeBattlePresentation = {
targetId?: string;
targetName?: string;
damageDealt?: number;
damageTaken?: number;
outcome?: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
};
export type RuntimeStoryViewModel = {
player: RuntimeStoryPlayerViewModel;
encounter: RuntimeStoryEncounterViewModel | null;
companions: RuntimeStoryCompanionViewModel[];
availableOptions: RuntimeStoryOptionView[];
status: RuntimeStoryStatusViewModel;
};
export type RuntimeStoryPresentation = {
actionText: string;
resultText: string;
storyText: string;
options: RuntimeStoryOptionView[];
toast?: string | null;
battle?: RuntimeBattlePresentation | null;
};
export type RuntimeStoryPatch =
| {
type: 'story_history_append';
actionText: string;
resultText: string;
}
| {
type: 'npc_affinity_changed';
npcId: string;
previousAffinity: number;
nextAffinity: number;
}
| {
type: 'battle_resolved';
functionId: string;
targetId?: string;
damageDealt?: number;
damageTaken?: number;
outcome: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
}
| {
type: 'status_changed';
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
}
| {
type: 'encounter_changed';
encounterId: string | null;
};
export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction> & {
snapshot?: SavedGameSnapshotInput;
};
export type RuntimeStoryStateRequest<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = {
sessionId: string;
clientVersion?: number;
snapshot?: SavedGameSnapshotInput<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};
export type RuntimeStoryActionResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = RuntimeActionResponse<
RuntimeStoryViewModel,
RuntimeStoryPresentation,
RuntimeStoryPatch
> & {
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};

View File

@@ -1,499 +0,0 @@
import type { JsonObject } from './common';
import type { SavedGameSnapshot } from './runtime';
export const QUEST_NARRATIVE_TYPES = [
'bounty',
'escort',
'investigation',
'retrieval',
'relationship',
'trial',
] as const;
export type SharedQuestNarrativeType = (typeof QUEST_NARRATIVE_TYPES)[number];
export const QUEST_OBJECTIVE_KINDS = [
'defeat_hostile_npc',
'inspect_treasure',
'spar_with_npc',
'talk_to_npc',
'reach_scene',
'deliver_item',
] as const;
export type SharedQuestObjectiveKind = (typeof QUEST_OBJECTIVE_KINDS)[number];
export const QUEST_URGENCY_LEVELS = ['low', 'medium', 'high'] as const;
export type SharedQuestUrgency = (typeof QUEST_URGENCY_LEVELS)[number];
export const QUEST_INTIMACY_LEVELS = [
'transactional',
'cooperative',
'trust_based',
] as const;
export type SharedQuestIntimacy = (typeof QUEST_INTIMACY_LEVELS)[number];
export const QUEST_REWARD_THEMES = [
'currency',
'resource',
'relationship',
'intel',
'rare_item',
] as const;
export type SharedQuestRewardTheme = (typeof QUEST_REWARD_THEMES)[number];
export const RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES = [
'heal',
'mana',
'cooldown',
'guard',
'damage',
] as const;
export type SharedRuntimeItemFunctionalBias =
(typeof RUNTIME_ITEM_FUNCTIONAL_BIAS_VALUES)[number];
export const RUNTIME_ITEM_TONE_VALUES = [
'grim',
'mysterious',
'martial',
'ritual',
'survival',
] as const;
export type SharedRuntimeItemTone = (typeof RUNTIME_ITEM_TONE_VALUES)[number];
export type StoryRequestOptionsPayload = {
availableOptions?: JsonObject[];
optionCatalog?: JsonObject[];
};
export type StoryRequestPayload<TWorldType extends string = string> = {
worldType: TWorldType;
character: JsonObject;
monsters?: JsonObject[];
history?: JsonObject[];
choice?: string;
context: JsonObject;
requestOptions?: StoryRequestOptionsPayload;
};
export type PlainTextPromptRequest = {
systemPrompt: string;
userPrompt: string;
};
export type PlainTextResponse = {
text: string;
};
export type NpcChatTurnLimitReason = 'negative_affinity';
export type NpcChatTurnClosingMode = 'free' | 'foreshadow_close';
export type NpcChatTurnDirective = {
sceneActId?: string | null;
turnLimit?: number | null;
remainingTurns?: number | null;
limitReason?: NpcChatTurnLimitReason | null;
closingMode?: NpcChatTurnClosingMode | null;
forceExitAfterTurn?: boolean;
};
export type NpcChatTurnCompletionDirective = {
turnLimit?: number | null;
remainingTurns?: number | null;
forceExit?: boolean;
closingMode?: NpcChatTurnClosingMode;
};
export type CharacterChatReplyRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
playerMessage: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSuggestionsRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
conversationSummary: string;
targetStatus: TTargetStatus;
};
export type CharacterChatSummaryRequest<
TCharacter = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TTargetStatus = unknown,
> = {
worldType: string;
playerCharacter: TCharacter;
targetCharacter: TCharacter;
storyHistory: TStoryMoment[];
context: TContext;
conversationHistory: TConversationTurn[];
previousSummary: string;
targetStatus: TTargetStatus;
};
export type NpcChatDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
topic: string;
resultSummary: string;
npcInitiatesConversation?: boolean;
};
export type NpcChatTurnRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
TConversationTurn = unknown,
TCombatContext = unknown,
TNpcState = unknown,
TQuestOfferState = unknown,
TQuestOfferEncounter = unknown,
TChatDirective = NpcChatTurnDirective,
> = {
worldType: string;
character?: TCharacter;
player?: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
conversationHistory?: TConversationTurn[];
dialogue?: TConversationTurn[];
combatContext?: TCombatContext | null;
playerMessage: string;
npcState: TNpcState;
npcInitiatesConversation?: boolean;
questOfferContext?: {
state: TQuestOfferState;
encounter: TQuestOfferEncounter;
turnCount: number;
} | null;
chatDirective?: TChatDirective | null;
};
export type NpcChatPendingQuestOffer<TQuest = unknown> = {
quest: TQuest;
introText?: string;
};
export type NpcChatTurnResult<TQuest = unknown> = {
npcReply: string;
affinityDelta: number;
affinityText: string;
suggestions: string[];
pendingQuestOffer?: NpcChatPendingQuestOffer<TQuest> | null;
chatDirective?: NpcChatTurnCompletionDirective | null;
};
export type NpcRecruitDialogueRequest<
TCharacter = unknown,
TEncounter = unknown,
TMonster = unknown,
TStoryMoment = unknown,
TContext = unknown,
> = {
worldType: string;
character: TCharacter;
encounter: TEncounter;
monsters: TMonster[];
history: TStoryMoment[];
context: TContext;
invitationText: string;
recruitSummary: string;
};
export type RuntimeItemIntentRequest<
TContext = JsonObject,
TPlan = JsonObject,
> = {
context: TContext;
plans: TPlan[];
};
export type RuntimeItemIntentResponse<TIntent = JsonObject> = {
intents: TIntent[];
};
export type QuestGenerationRequest<
TState = JsonObject,
TEncounter = JsonObject,
> = {
state: TState;
encounter: TEncounter;
};
export type RuntimeAction<
TType extends string = string,
TPayload = JsonObject,
> = {
type: TType;
functionId?: string;
targetId?: string;
payload?: TPayload;
};
export type RuntimeActionRequest<
TAction extends RuntimeAction = RuntimeAction,
> = {
sessionId: string;
clientVersion?: number;
action: TAction;
};
export type RuntimeActionResponse<
TViewModel = JsonObject,
TPresentation = JsonObject,
TPatch = JsonObject,
> = {
sessionId: string;
serverVersion: number;
viewModel: TViewModel;
presentation: TPresentation;
patches: TPatch[];
};
export const TASK5_RUNTIME_FUNCTION_IDS = [
'story_continue_adventure',
'story_opening_camp_dialogue',
'camp_travel_home_scene',
'idle_call_out',
'idle_explore_forward',
'idle_observe_signs',
'idle_rest_focus',
'idle_travel_next_scene',
'battle_attack_basic',
'battle_use_skill',
'battle_all_in_crush',
'battle_escape_breakout',
'battle_feint_step',
'battle_finisher_window',
'battle_guard_break',
'battle_probe_pressure',
'battle_recover_breath',
'npc_chat',
'npc_fight',
'npc_help',
'npc_leave',
'npc_preview_talk',
'npc_recruit',
'npc_spar',
] as const;
export type Task5RuntimeFunctionId =
(typeof TASK5_RUNTIME_FUNCTION_IDS)[number];
export const TASK6_RUNTIME_FUNCTION_IDS = [
'equipment_equip',
'equipment_unequip',
'forge_craft',
'forge_dismantle',
'forge_reforge',
'inventory_use',
'npc_gift',
'npc_quest_accept',
'npc_quest_turn_in',
'npc_trade',
'treasure_inspect',
'treasure_leave',
'treasure_secure',
] as const;
export type Task6RuntimeFunctionId =
(typeof TASK6_RUNTIME_FUNCTION_IDS)[number];
export const SERVER_RUNTIME_FUNCTION_IDS = [
...TASK5_RUNTIME_FUNCTION_IDS,
...TASK6_RUNTIME_FUNCTION_IDS,
] as const;
export type ServerRuntimeFunctionId =
(typeof SERVER_RUNTIME_FUNCTION_IDS)[number];
export const TASK5_RUNTIME_OPTION_SCOPES = ['story', 'combat', 'npc'] as const;
export type Task5RuntimeOptionScope =
(typeof TASK5_RUNTIME_OPTION_SCOPES)[number];
export type RuntimeStoryChoicePayload = JsonObject & {
optionText?: string;
note?: string;
};
export type RuntimeStoryOptionInteraction =
| {
kind: 'npc';
npcId: string;
action:
| 'chat'
| 'help'
| 'fight'
| 'leave'
| 'recruit'
| 'spar'
| 'trade'
| 'gift'
| 'quest_accept'
| 'quest_turn_in';
questId?: string;
}
| {
kind: 'treasure';
action: 'inspect' | 'leave' | 'secure';
};
export type RuntimeStoryChoiceAction = RuntimeAction<
'story_choice',
RuntimeStoryChoicePayload
> & {
functionId: string;
targetId?: string;
};
export type RuntimeStoryOptionView = {
functionId: string;
actionText: string;
detailText?: string;
scope: Task5RuntimeOptionScope;
interaction?: RuntimeStoryOptionInteraction;
payload?: RuntimeStoryChoicePayload;
disabled?: boolean;
reason?: string;
};
export type RuntimeStoryPlayerViewModel = {
hp: number;
maxHp: number;
mana: number;
maxMana: number;
};
export type RuntimeStoryCompanionViewModel = {
npcId: string;
characterId?: string;
joinedAtAffinity: number;
};
export type RuntimeStoryEncounterViewModel = {
id: string;
kind: 'npc' | 'treasure';
npcName: string;
hostile: boolean;
affinity?: number;
recruited?: boolean;
interactionActive: boolean;
battleMode?: 'fight' | 'spar' | null;
};
export type RuntimeStoryStatusViewModel = {
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
};
export type RuntimeBattlePresentation = {
targetId?: string;
targetName?: string;
damageDealt?: number;
damageTaken?: number;
outcome?: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
};
export type RuntimeStoryViewModel = {
player: RuntimeStoryPlayerViewModel;
encounter: RuntimeStoryEncounterViewModel | null;
companions: RuntimeStoryCompanionViewModel[];
availableOptions: RuntimeStoryOptionView[];
status: RuntimeStoryStatusViewModel;
};
export type RuntimeStoryPresentation = {
actionText: string;
resultText: string;
storyText: string;
options: RuntimeStoryOptionView[];
toast?: string | null;
battle?: RuntimeBattlePresentation | null;
};
export type RuntimeStoryPatch =
| {
type: 'story_history_append';
actionText: string;
resultText: string;
}
| {
type: 'npc_affinity_changed';
npcId: string;
previousAffinity: number;
nextAffinity: number;
}
| {
type: 'battle_resolved';
functionId: string;
targetId?: string;
damageDealt?: number;
damageTaken?: number;
outcome: 'ongoing' | 'victory' | 'spar_complete' | 'escaped';
}
| {
type: 'status_changed';
inBattle: boolean;
npcInteractionActive: boolean;
currentNpcBattleMode: 'fight' | 'spar' | null;
currentNpcBattleOutcome: 'fight_victory' | 'spar_complete' | null;
}
| {
type: 'encounter_changed';
encounterId: string | null;
};
export type RuntimeStoryActionRequest =
RuntimeActionRequest<RuntimeStoryChoiceAction>;
export type RuntimeStoryActionResponse<
TSnapshotGameState = JsonObject,
TSnapshotCurrentStory = JsonObject,
> = RuntimeActionResponse<
RuntimeStoryViewModel,
RuntimeStoryPresentation,
RuntimeStoryPatch
> & {
snapshot: SavedGameSnapshot<
TSnapshotGameState,
string,
TSnapshotCurrentStory
>;
};

View File

@@ -1,8 +1,19 @@
export * from './assets/qwenSprite';
export * from './contracts/auth';
export * from './contracts/common';
export type * from './contracts/customWorldAgent';
export * from './contracts/rpgAgentActions';
export * from './contracts/rpgAgentAnchors';
export * from './contracts/rpgAgentDraft';
export * from './contracts/rpgAgentSession';
export * from './contracts/rpgCreationFixtures';
export * from './contracts/rpgCreationPreview';
export * from './contracts/rpgCreationWorkSummary';
export * from './contracts/rpgRuntimeChat';
export * from './contracts/rpgRuntimeQuestAssist';
export * from './contracts/rpgRuntimeStoryAction';
export * from './contracts/rpgRuntimeStoryState';
export * from './contracts/runtime';
export * from './contracts/story';
export * from './http';
export * from './llm/narrativeLanguage';
export * from './llm/parsers';

View File

@@ -1,357 +0,0 @@
import json
import os
from pathlib import Path
import numpy as np
try:
from vikingdb import VikingDB, IAM, EmbeddingClient
from vikingdb.vector import EmbeddingData, EmbeddingModelOpt, EmbeddingRequest
except ImportError as exc: # pragma: no cover
raise SystemExit(
"Missing dependency: vikingdb-python-sdk.\n"
"Install it with: py -3 -m pip install vikingdb-python-sdk"
) from exc
def zh(value: str) -> str:
return value.encode("utf-8").decode("unicode_escape")
BUILD_TAGS = [
{
"label": zh(r"\u5feb\u5251"),
"aliases": ["duelist", "swift blade", "swiftblade", zh(r"\u5251\u5feb"), zh(r"\u5feb\u5203")],
"description": zh(r"\u4ee5\u9ad8\u901f\u8f7b\u5175\u5668\u3001\u8fde\u7eed\u51fa\u624b\u548c\u8d34\u8eab\u538b\u8feb\u4e3a\u6838\u5fc3\u7684\u8fd1\u6218\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u8fde\u6bb5"),
"aliases": ["combo", "chain", zh(r"\u8fde\u51fb")],
"description": zh(r"\u4f9d\u8d56\u8fde\u7eed\u547d\u4e2d\u4e0e\u591a\u6bb5\u8282\u594f\u538b\u5236\u7684\u8f93\u51fa\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7a81\u8fdb"),
"aliases": ["dash", "lunge", "mobility engage"],
"description": zh(r"\u5f3a\u8c03\u5feb\u901f\u8d34\u8fd1\u76ee\u6807\u3001\u62a2\u5360\u8eab\u4f4d\u548c\u5148\u624b\u5207\u5165\u7684\u6218\u6597\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u8ffd\u51fb"),
"aliases": ["chase", "follow-up", "finisher chase"],
"description": zh(r"\u64c5\u957f\u5728\u5bf9\u624b\u5931\u8861\u6216\u88ab\u51fb\u9000\u540e\u7ee7\u7eed\u8ffd\u6253\u7684\u6218\u6597\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5feb\u88ad"),
"aliases": ["assassin", "rogue", "ambush", zh(r"\u523a\u51fb")],
"description": zh(r"\u5f3a\u8c03\u77ed\u65f6\u5207\u5165\u3001\u70b9\u6740\u5f31\u70b9\u548c\u8fc5\u901f\u8131\u79bb\u7684\u523a\u51fb\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u8fdc\u5c04"),
"aliases": ["projectile", "ranged", "arrow", zh(r"\u5c04\u51fb")],
"description": zh(r"\u4ee5\u6295\u5c04\u7269\u3001\u4e2d\u8fdc\u8ddd\u79bb\u7275\u5236\u548c\u5b89\u5168\u8f93\u51fa\u4e3a\u6838\u5fc3\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u6e38\u51fb"),
"aliases": ["scout", "skirmish", "harass", "fieldcraft"],
"description": zh(r"\u5f3a\u8c03\u8fb9\u79fb\u52a8\u8fb9\u8f93\u51fa\u3001\u8bd5\u63a2\u62c9\u626f\u548c\u62e9\u673a\u518d\u5165\u573a\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u673a\u52a8"),
"aliases": ["mobility", "nimble", "agile"],
"description": zh(r"\u4ee3\u8868\u9ad8\u4f4d\u79fb\u3001\u9ad8\u8eab\u6cd5\u548c\u5feb\u901f\u6362\u4f4d\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u98ce\u884c"),
"aliases": ["wind", "gust", "speed", zh(r"\u75be\u884c")],
"description": zh(r"\u5f3a\u8c03\u8f7b\u7075\u6b65\u6cd5\u3001\u79fb\u901f\u4f18\u52bf\u548c\u8fc5\u901f\u8c03\u4f4d\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u91cd\u51fb"),
"aliases": ["heavy", "slam", "mighty", "crush"],
"description": zh(r"\u5f3a\u8c03\u539a\u91cd\u6253\u51fb\u3001\u5355\u6b21\u9ad8\u538b\u8f93\u51fa\u548c\u6b63\u9762\u7838\u7a7f\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7206\u53d1"),
"aliases": ["burst", "nova", "sudden damage"],
"description": zh(r"\u4ee3\u8868\u77ed\u7a97\u53e3\u5185\u8fc5\u901f\u62ac\u9ad8\u4f24\u5bb3\u5cf0\u503c\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7834\u7532"),
"aliases": ["breaker", "armor break", "shatter"],
"description": zh(r"\u64c5\u957f\u6495\u5f00\u9632\u5fa1\u3001\u6253\u65ad\u5b88\u52bf\u548c\u9488\u5bf9\u786c\u76ee\u6807\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u538b\u5236"),
"aliases": ["tempo", "pressure", "control offense"],
"description": zh(r"\u901a\u8fc7\u6301\u7eed\u4e3b\u52a8\u8fdb\u653b\u4e0e\u8282\u594f\u5360\u4f18\u8feb\u4f7f\u5bf9\u624b\u5931\u8bef\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u538b\u8840"),
"aliases": ["low hp", "berserk", "risk damage"],
"description": zh(r"\u4ee5\u5192\u9669\u538b\u4f4e\u8840\u7ebf\u6362\u53d6\u66f4\u5f3a\u653b\u51fb\u6027\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5b88\u5fa1"),
"aliases": ["ward", "guard", "protector", "defense"],
"description": zh(r"\u5f3a\u8c03\u51cf\u4f24\u3001\u7a33\u5b88\u548c\u9876\u4f4f\u6b63\u9762\u4f24\u5bb3\u7684\u9632\u5fa1\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u62a4\u4f53"),
"aliases": ["barrier", "shielding", "spirit guard", "spirit"],
"description": zh(r"\u504f\u5411\u62a4\u7f69\u3001\u62a4\u8eab\u6c14\u52b2\u548c\u72b6\u6001\u6297\u538b\u7684\u9632\u5fa1\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u91cd\u7532"),
"aliases": ["tank", "heavy armor", "iron wall"],
"description": zh(r"\u4ee3\u8868\u9ad8\u786c\u5ea6\u62a4\u7532\u3001\u6b63\u9762\u627f\u4f24\u4e0e\u7a33\u5b9a\u7ad9\u573a\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u53cd\u51fb"),
"aliases": ["counter", "riposte", "retaliate"],
"description": zh(r"\u901a\u8fc7\u683c\u6321\u3001\u7ad9\u6869\u4e0e\u540e\u624b\u60e9\u7f5a\u5f62\u6210\u6536\u76ca\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u9547\u90aa"),
"aliases": ["banish", "holy ward", "warding seal"],
"description": zh(r"\u64c5\u957f\u538b\u5236\u90aa\u795f\u3001\u5492\u715e\u548c\u5f02\u7c7b\u80fd\u91cf\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u6cd5\u4fee"),
"aliases": ["caster", "mage", "arcane", "spell"],
"description": zh(r"\u4ee5\u6cd5\u672f\u9a71\u52a8\u8f93\u51fa\u3001\u63a7\u5236\u548c\u8d44\u6e90\u8fd0\u8f6c\u7684\u6838\u5fc3\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u6cd5\u529b"),
"aliases": ["mana", "magic", "essence", "spirit power"],
"description": zh(r"\u56f4\u7ed5\u6cd5\u529b\u4e0a\u9650\u3001\u6cd5\u672f\u6d88\u8017\u4e0e\u6cd5\u80fd\u5faa\u73af\u6784\u7b51\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u96f7\u6cd5"),
"aliases": ["lightning", "thunder", "storm"],
"description": zh(r"\u4ee3\u8868\u9ad8\u538b\u96f7\u7cfb\u672f\u6cd5\u3001\u77ac\u65f6\u9707\u8361\u548c\u9ebb\u75f9\u538b\u5236\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7b26\u9635"),
"aliases": ["sigil", "formation", "seal", "rune"],
"description": zh(r"\u901a\u8fc7\u7b26\u7bb4\u3001\u6cd5\u9635\u548c\u9884\u5e03\u7f6e\u6548\u679c\u6539\u53d8\u6218\u573a\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u63a7\u573a"),
"aliases": ["control", "crowd control", "lockdown"],
"description": zh(r"\u4ee5\u9650\u5236\u884c\u52a8\u3001\u5c01\u9501\u7a7a\u95f4\u548c\u538b\u7f29\u9009\u62e9\u4e3a\u6838\u5fc3\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u8fc7\u8f7d"),
"aliases": ["overload", "surge", "power spike"],
"description": zh(r"\u5728\u77ed\u65f6\u95f4\u5185\u63a8\u52a8\u9ad8\u6cd5\u8017\u4e0e\u9ad8\u5f3a\u5ea6\u91ca\u653e\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u56de\u590d"),
"aliases": ["heal", "healing", "recovery", "restore"],
"description": zh(r"\u5f3a\u8c03\u5373\u65f6\u6062\u590d\u4e0e\u6218\u540e\u7eed\u63a5\u80fd\u529b\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u62a4\u6301"),
"aliases": ["support", "aid", "blessing"],
"description": zh(r"\u901a\u8fc7\u589e\u76ca\u3001\u62ac\u7a33\u6001\u548c\u4fdd\u62a4\u961f\u53cb\u6765\u5efa\u7acb\u4f18\u52bf\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7eed\u6218"),
"aliases": ["sustain", "endurance", "long fight"],
"description": zh(r"\u9762\u5411\u957f\u7ebf\u6218\u6597\u3001\u8d44\u6e90\u6301\u7eed\u4e0e\u5bb9\u9519\u63d0\u5347\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u547d\u7eb9"),
"aliases": ["fate", "omen", "destiny"],
"description": zh(r"\u56f4\u7ed5\u547d\u8fd0\u3001\u5370\u8bb0\u4e0e\u89e6\u53d1\u5f0f\u8fde\u9501\u6536\u76ca\u6784\u7b51\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u673a\u7f18"),
"aliases": ["fortune", "luck", "opportunity"],
"description": zh(r"\u4f9d\u8d56\u65f6\u673a\u3001\u8fd0\u52bf\u548c\u989d\u5916\u6536\u76ca\u89e6\u53d1\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u51b7\u5374"),
"aliases": ["cooldown", "cdr", "recharge"],
"description": zh(r"\u901a\u8fc7\u66f4\u5feb\u5468\u8f6c\u6280\u80fd\u4e0e\u9053\u5177\u6765\u6eda\u52a8\u4f18\u52bf\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u7edf\u5fa1"),
"aliases": ["commander", "command", "leader"],
"description": zh(r"\u5f3a\u8c03\u6574\u4f53\u534f\u8c03\u3001\u56e2\u961f\u6536\u76ca\u548c\u7efc\u5408\u8c03\u5ea6\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5747\u8861"),
"aliases": ["balanced", "adaptable", "all-round"],
"description": zh(r"\u6ca1\u6709\u660e\u663e\u77ed\u677f\uff0c\u504f\u91cd\u4e2d\u540e\u671f\u7a33\u5b9a\u6210\u578b\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5de5\u5de7"),
"aliases": ["craft", "artisan", "utility", "socket"],
"description": zh(r"\u504f\u5411\u5de5\u827a\u3001\u5668\u68b0\u3001\u9576\u5d4c\u548c\u8f85\u52a9\u6784\u7b51\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u70bc\u836f"),
"aliases": ["alchemy", "potion", "tonic"],
"description": zh(r"\u56f4\u7ed5\u836f\u5242\u3001\u4e34\u65f6\u5f3a\u5316\u548c\u6218\u4e2d\u8865\u7ed9\u7684\u5de5\u827a\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5148\u950b"),
"aliases": ["vanguard", "frontline"],
"description": zh(r"\u4ee3\u8868\u961f\u4f0d\u4e2d\u7684\u6b63\u9762\u5f00\u8def\u3001\u5403\u7ebf\u4e0e\u538b\u524d\u6392\u804c\u8d23\u3002"),
},
{
"label": zh(r"\u72c2\u6218"),
"aliases": ["berserker", "rage"],
"description": zh(r"\u4ee5\u8840\u91cf\u4ea4\u6362\u3001\u731b\u653b\u548c\u9ad8\u98ce\u9669\u9ad8\u56de\u62a5\u4e3a\u7279\u8272\u7684\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u6cd5\u5251"),
"aliases": ["spellblade", "bladecaster"],
"description": zh(r"\u878d\u5408\u5175\u5203\u4e0e\u672f\u6cd5\uff0c\u64c5\u957f\u4e2d\u8ddd\u79bb\u538b\u8feb\u7684\u6df7\u5408\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5723\u4f51"),
"aliases": ["paladin", "holy guard"],
"description": zh(r"\u517c\u5177\u9632\u62a4\u3001\u56de\u590d\u548c\u60e9\u6212\u80fd\u529b\u7684\u795d\u798f\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u5821\u5792"),
"aliases": ["fortress", "bulwark"],
"description": zh(r"\u4ee5\u7a33\u5b9a\u7ad9\u573a\u3001\u786c\u6297\u4e0e\u53cd\u6253\u4e3a\u6838\u5fc3\u7684\u91cd\u9632\u5fa1\u6807\u7b7e\u3002"),
},
{
"label": zh(r"\u8d77\u624b"),
"aliases": ["starter", "legacy"],
"description": zh(r"\u504f\u8fc7\u6e21\u4e0e\u8d77\u6b65\u7528\u9014\u7684\u65e9\u671f\u6784\u7b51\u6807\u7b7e\u3002"),
},
]
def build_prompt(definition: dict) -> str:
aliases = "\u3001".join(definition["aliases"])
return f"{definition['label']}{definition['description']} 别名:{aliases}"
def load_env_file(path: Path, protected_keys: set[str]) -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
if not key or key in protected_keys:
continue
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
os.environ[key] = value
def load_local_env() -> None:
root_dir = Path(__file__).resolve().parents[1]
protected_keys = set(os.environ)
load_env_file(root_dir / ".env", protected_keys)
load_env_file(root_dir / ".env.local", protected_keys)
def create_embedding_client() -> EmbeddingClient:
access_key = os.getenv("VOLCENGINE_ACCESS_KEY_ID") or os.getenv("VIKINGDB_ACCESS_KEY_ID")
secret_key = os.getenv("VOLCENGINE_SECRET_ACCESS_KEY") or os.getenv("VIKINGDB_SECRET_ACCESS_KEY")
host = os.getenv("VIKINGDB_HOST", "api-vikingdb.vikingdb.cn-beijing.volces.com")
region = os.getenv("VIKINGDB_REGION", "cn-beijing")
if not access_key or not secret_key:
raise SystemExit(
"Missing VikingDB credentials.\n"
"Required:\n"
" VOLCENGINE_ACCESS_KEY_ID\n"
" VOLCENGINE_SECRET_ACCESS_KEY\n"
"Optional:\n"
" VIKINGDB_HOST (default: api-vikingdb.vikingdb.cn-beijing.volces.com)\n"
" VIKINGDB_REGION (default: cn-beijing)\n"
)
service = VikingDB(
host=host,
region=region,
auth=IAM(ak=access_key, sk=secret_key),
)
return EmbeddingClient(service)
def encode_texts(client: EmbeddingClient, texts: list[str]) -> np.ndarray:
request = EmbeddingRequest(
data=[EmbeddingData(text=text) for text in texts],
dense_model=EmbeddingModelOpt(name="bge-large-zh"),
)
response = client.embedding(request)
result = getattr(response, "result", None)
data = getattr(result, "data", None) if result is not None else None
if data is None and isinstance(result, dict):
data = result.get("data")
if data is None:
data = getattr(response, "data", None)
if data is None:
raise ValueError("Embedding response did not include any data entries.")
embeddings: list[list[float]] = []
for item in data:
dense = getattr(item, "dense", None)
if dense is None and isinstance(item, dict):
dense = item.get("dense")
if dense is None:
dense = getattr(item, "embedding", None)
if dense is None and isinstance(item, dict):
dense = item.get("embedding")
if dense is None:
raise ValueError("Embedding response item did not include a dense vector.")
embeddings.append(dense)
matrix = np.array(embeddings, dtype=np.float32)
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
norms[norms == 0] = 1.0
return matrix / norms
def main():
load_local_env()
client = create_embedding_client()
prompts = [build_prompt(definition) for definition in BUILD_TAGS]
embeddings = encode_texts(client, prompts)
threshold = 0.35
pairs: list[tuple[str, str, float]] = []
for index, left in enumerate(BUILD_TAGS):
for other_index in range(index + 1, len(BUILD_TAGS)):
right = BUILD_TAGS[other_index]
similarity = float(np.dot(embeddings[index], embeddings[other_index]))
if similarity < threshold:
continue
pairs.append((left["label"], right["label"], round(similarity, 4)))
output_path = Path(__file__).resolve().parents[1] / "src" / "data" / "buildTagSimilarity.generated.ts"
lines = [
"export const BUILD_TAG_SIMILARITY_PAIRS: Array<readonly [string, string, number]> = ["
]
for left, right, similarity in pairs:
lines.append(f" ['{left}', '{right}', {similarity}],")
lines.append("] as const;")
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(json.dumps({
"output": str(output_path),
"pair_count": len(pairs),
"model": "bge-large-zh",
}, ensure_ascii=False))
if __name__ == "__main__":
main()

View File

@@ -25,8 +25,8 @@ import {
} from '../src/data/questFlow.ts';
import { createInitialGameRuntimeStats } from '../src/data/runtimeStats.ts';
import { createSceneCallOutEncounter, createSceneEncounterPreview, ensureSceneEncounterPreview } from '../src/data/sceneEncounterPreviews.ts';
import { buildSceneObserveSignsStoryText } from '../src/data/sceneObservation.ts';
import { getSceneHostileNpcPresetIds, getScenePresetsByWorld } from '../src/data/scenePresets.ts';
import { resolveFunctionOption } from '../src/data/stateFunctions.ts';
import { buildTreasureEncounterStoryMoment, buildTreasureResultText, resolveTreasureReward } from '../src/data/treasureInteractions.ts';
import { AnimationState, GameState, WorldType } from '../src/types.ts';
@@ -209,8 +209,24 @@ function smokeObserveAndCallOut() {
assert(callOutResult.currentEncounter?.kind !== 'treasure', `[idle] treasure call_out should be disabled for ${worldType}`);
assert(callOutResult.currentEncounter || callOutResult.sceneHostileNpcs.length > 0 || getSceneHostileNpcPresetIds(scene).length === 0, `[idle] call_out failed for ${scene.id}`);
const observeText = buildSceneObserveSignsStoryText(worldType, scene.id);
assert(observeText.length > 12, `[idle] observe_signs text too short for ${scene.id}`);
const observeOption = resolveFunctionOption(
'idle_observe_signs',
{
worldType,
playerCharacter: baseState.playerCharacter,
inBattle: false,
currentSceneId: scene.id,
currentSceneName: scene.name,
monsters: [],
playerHp: baseState.playerHp,
playerMaxHp: baseState.playerMaxHp,
playerMana: baseState.playerMana,
playerMaxMana: baseState.playerMaxMana,
},
'观察周围动静',
);
assert(observeOption?.functionId === 'idle_observe_signs', `[idle] observe_signs option missing for ${scene.id}`);
assert(Boolean(observeOption?.detailText?.trim()), `[idle] observe_signs detail missing for ${scene.id}`);
}
}

View File

@@ -122,6 +122,11 @@ function createTestConfig(
mockAvatarUrl: '',
},
authSession: {
accessCookieName: 'genarrative_access_session',
accessCookieTtlSeconds: 7200,
accessCookieSecure: false,
accessCookieSameSite: 'Lax',
accessCookiePath: '/',
refreshCookieName: 'genarrative_refresh_session',
refreshSessionTtlDays: 30,
refreshCookieSecure: false,
@@ -203,7 +208,10 @@ async function authEntry(baseUrl: string, username: string, password: string) {
username: string;
};
};
const refreshCookie = response.headers.get('set-cookie');
const refreshCookie = buildCookieHeader(
response.headers.get('set-cookie'),
'genarrative_refresh_session',
);
assert.equal(response.status, 200);
assert.ok(payload.token);
@@ -258,7 +266,10 @@ async function phoneLogin(baseUrl: string, phone: string, code = '123456') {
wechatBound: boolean;
};
};
const refreshCookie = response.headers.get('set-cookie');
const refreshCookie = buildCookieHeader(
response.headers.get('set-cookie'),
'genarrative_refresh_session',
);
assert.equal(response.status, 200);
assert.ok(payload.token);
@@ -444,6 +455,191 @@ async function createObjectRefiningCustomWorldAgentSession(params: {
return session;
}
async function markAgentSessionPublishReady(params: {
context: TestAppContext;
userId: string;
sessionId: string;
}) {
const snapshot = await params.context.customWorldAgentOrchestrator.getSessionSnapshot(
params.userId,
params.sessionId,
);
const draftProfile = snapshot?.draftProfile as Record<string, unknown> | null;
const playableNpcs = Array.isArray(draftProfile?.playableNpcs)
? (draftProfile?.playableNpcs as Array<Record<string, unknown>>)
: [];
const storyNpcs = Array.isArray(draftProfile?.storyNpcs)
? (draftProfile?.storyNpcs as Array<Record<string, unknown>>)
: [];
const landmarks = Array.isArray(draftProfile?.landmarks)
? (draftProfile?.landmarks as Array<Record<string, unknown>>)
: [];
const sceneChapters = Array.isArray(draftProfile?.sceneChapters)
? (draftProfile?.sceneChapters as Array<Record<string, unknown>>)
: [];
const camp =
draftProfile?.camp && typeof draftProfile.camp === 'object'
? (draftProfile.camp as Record<string, unknown>)
: null;
const firstPlayableRoleId =
typeof playableNpcs[0]?.id === 'string' && playableNpcs[0]?.id.trim()
? playableNpcs[0].id.trim()
: null;
const firstStoryRoleId =
typeof storyNpcs[0]?.id === 'string' && storyNpcs[0]?.id.trim()
? storyNpcs[0].id.trim()
: firstPlayableRoleId;
assert.ok(snapshot);
assert.ok(draftProfile);
assert.ok(playableNpcs.length > 0);
assert.ok(storyNpcs.length > 0);
assert.ok(landmarks.length > 0);
assert.ok(sceneChapters.length > 0);
assert.ok(firstStoryRoleId);
await params.context.customWorldAgentSessions.replaceDerivedState(
params.userId,
params.sessionId,
{
stage: 'ready_to_publish',
qualityFindings: [],
draftProfile: {
...draftProfile,
chapters:
Array.isArray(draftProfile.chapters) && draftProfile.chapters.length > 0
? draftProfile.chapters
: [{ id: 'chapter-main-1', title: '主线第一章' }],
camp: {
...(camp ?? {}),
id:
typeof camp?.id === 'string' && camp.id.trim()
? camp.id.trim()
: 'camp-home',
name:
typeof camp?.name === 'string' && camp.name.trim()
? camp.name.trim()
: '归潮营地',
description:
typeof camp?.description === 'string' && camp.description.trim()
? camp.description.trim()
: '可供玩家整理线索的临时据点。',
imageSrc:
typeof camp?.imageSrc === 'string' && camp.imageSrc.trim()
? camp.imageSrc.trim()
: '/generated/camp/publish-ready.png',
generatedSceneAssetId:
typeof camp?.generatedSceneAssetId === 'string' &&
camp.generatedSceneAssetId.trim()
? camp.generatedSceneAssetId.trim()
: 'scene-camp-publish-ready',
generatedScenePrompt:
typeof camp?.generatedScenePrompt === 'string' &&
camp.generatedScenePrompt.trim()
? camp.generatedScenePrompt.trim()
: '潮雾营地发布正式图',
generatedSceneModel:
typeof camp?.generatedSceneModel === 'string' &&
camp.generatedSceneModel.trim()
? camp.generatedSceneModel.trim()
: 'test-scene-model',
},
playableNpcs: playableNpcs.map((entry, index) => ({
...entry,
imageSrc:
typeof entry.imageSrc === 'string' && entry.imageSrc.trim()
? entry.imageSrc.trim()
: `/generated/playable/publish-ready-${index + 1}.png`,
generatedVisualAssetId:
typeof entry.generatedVisualAssetId === 'string' &&
entry.generatedVisualAssetId.trim()
? entry.generatedVisualAssetId.trim()
: `visual-playable-publish-${index + 1}`,
generatedAnimationSetId:
typeof entry.generatedAnimationSetId === 'string' &&
entry.generatedAnimationSetId.trim()
? entry.generatedAnimationSetId.trim()
: `anim-playable-publish-${index + 1}`,
})),
storyNpcs: storyNpcs.map((entry, index) => ({
...entry,
imageSrc:
typeof entry.imageSrc === 'string' && entry.imageSrc.trim()
? entry.imageSrc.trim()
: `/generated/story/publish-ready-${index + 1}.png`,
generatedVisualAssetId:
typeof entry.generatedVisualAssetId === 'string' &&
entry.generatedVisualAssetId.trim()
? entry.generatedVisualAssetId.trim()
: `visual-story-publish-${index + 1}`,
generatedAnimationSetId:
typeof entry.generatedAnimationSetId === 'string' &&
entry.generatedAnimationSetId.trim()
? entry.generatedAnimationSetId.trim()
: `anim-story-publish-${index + 1}`,
})),
landmarks: landmarks.map((entry, index) => ({
...entry,
imageSrc:
typeof entry.imageSrc === 'string' && entry.imageSrc.trim()
? entry.imageSrc.trim()
: `/generated/landmark/publish-ready-${index + 1}.png`,
generatedSceneAssetId:
typeof entry.generatedSceneAssetId === 'string' &&
entry.generatedSceneAssetId.trim()
? entry.generatedSceneAssetId.trim()
: `scene-landmark-publish-${index + 1}`,
generatedScenePrompt:
typeof entry.generatedScenePrompt === 'string' &&
entry.generatedScenePrompt.trim()
? entry.generatedScenePrompt.trim()
: `地点 ${typeof entry.name === 'string' ? entry.name : index + 1} 的正式场景图`,
generatedSceneModel:
typeof entry.generatedSceneModel === 'string' &&
entry.generatedSceneModel.trim()
? entry.generatedSceneModel.trim()
: 'test-scene-model',
})),
sceneChapters: sceneChapters.map((chapter, chapterIndex) => {
const acts = Array.isArray(chapter.acts)
? (chapter.acts as Array<Record<string, unknown>>)
: [];
return {
...chapter,
linkedThreadIds:
Array.isArray(chapter.linkedThreadIds) &&
chapter.linkedThreadIds.length > 0
? chapter.linkedThreadIds
: ['thread-publish-ready'],
acts: acts.map((act, actIndex) => ({
...act,
encounterNpcIds:
Array.isArray(act.encounterNpcIds) && act.encounterNpcIds.length > 0
? act.encounterNpcIds
: [firstStoryRoleId],
primaryNpcId:
typeof act.primaryNpcId === 'string' && act.primaryNpcId.trim()
? act.primaryNpcId.trim()
: firstStoryRoleId,
backgroundImageSrc:
typeof act.backgroundImageSrc === 'string' &&
act.backgroundImageSrc.trim()
? act.backgroundImageSrc.trim()
: `/generated/scene/publish-ready-${chapterIndex + 1}-${actIndex + 1}.png`,
backgroundAssetId:
typeof act.backgroundAssetId === 'string' &&
act.backgroundAssetId.trim()
? act.backgroundAssetId.trim()
: `scene-act-publish-${chapterIndex + 1}-${actIndex + 1}`,
})),
};
}),
},
},
);
}
function parseRedirectHash(location: string) {
const url = new URL(location, 'http://127.0.0.1');
return new URLSearchParams(
@@ -451,6 +647,18 @@ function parseRedirectHash(location: string) {
);
}
function readCookieValue(cookieHeader: string, cookieName: string) {
const match = cookieHeader.match(
new RegExp(`${cookieName}=([^;,\r\n]+)`, 'u'),
);
return match?.[1] ? decodeURIComponent(match[1]) : '';
}
function buildCookieHeader(cookieHeader: string | null | undefined, cookieName: string) {
const value = readCookieValue(cookieHeader || '', cookieName);
return value ? `${cookieName}=${encodeURIComponent(value)}` : '';
}
async function startWechatMockFlow(baseUrl: string, redirectPath = '/') {
const startResponse = await httpRequest(
`${baseUrl}/api/auth/wechat/start?redirectPath=${encodeURIComponent(redirectPath)}`,
@@ -467,8 +675,7 @@ async function startWechatMockFlow(baseUrl: string, redirectPath = '/') {
const location = callbackResponse.headers.get('location') || '';
assert.ok(location);
const hash = parseRedirectHash(location);
const token = hash.get('auth_token') || '';
const token = hash.get('auth_token')?.trim() || '';
assert.ok(token);
return {
@@ -1536,7 +1743,10 @@ test('logout-all revokes all refresh sessions and invalidates existing access to
assert.equal(refreshResponse.status, 200);
const entryB = {
token: refreshPayload.token,
refreshCookie: refreshResponse.headers.get('set-cookie') || '',
refreshCookie: buildCookieHeader(
refreshResponse.headers.get('set-cookie'),
'genarrative_refresh_session',
),
};
const logoutAllResponse = await httpRequest(
@@ -2503,6 +2713,34 @@ test('custom world works endpoint returns draft sessions and published worlds to
assert.equal(publishResponse.status, 200);
const publishMutationResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library/world-published/publish`,
withBearer(entry.token, {
method: 'POST',
}),
);
assert.equal(publishMutationResponse.status, 200);
const draftOnlyResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library/world-draft-only`,
withBearer(entry.token, {
method: 'PUT',
body: JSON.stringify({
profile: {
id: 'world-draft-only',
name: '旧兼容草稿',
subtitle: '仍保留在作品库,但不再进入创作中心',
summary: '这个条目用来验证阶段三不会继续把 library draft 当主草稿展示。',
playableNpcs: [{ id: 'hero-draft', name: '旧草稿角色' }],
landmarks: [{ id: 'port-draft', name: '旧草稿地点' }],
},
}),
}),
);
assert.equal(draftOnlyResponse.status, 200);
const worksResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/works`,
{
@@ -2542,6 +2780,10 @@ test('custom world works endpoint returns draft sessions and published worlds to
item.canEnterWorld === true,
),
);
assert.equal(
worksPayload.items.some((item) => item.profileId === 'world-draft-only'),
false,
);
});
});
@@ -2847,6 +3089,98 @@ test('custom world agent draft_foundation action generates draft cards and card
);
});
test('custom world agent stream message returns enriched session payload over sse', async () => {
await withTestServer(
'custom-world-agent-stream-session',
async ({ baseUrl, context }) => {
installTestCustomWorldAgentSingleTurnLlm(context);
const entry = await authEntry(baseUrl, 'cw_agent_stream', 'secret123');
const readySession = await createReadyCustomWorldAgentSession({
baseUrl,
token: entry.token,
});
const foundationResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/agent/sessions/${encodeURIComponent(readySession.sessionId)}/actions`,
withBearer(entry.token, {
method: 'POST',
body: JSON.stringify({
action: 'draft_foundation',
}),
}),
);
const foundationPayload = (await foundationResponse.json()) as {
operation: {
operationId: string;
};
};
assert.equal(foundationResponse.status, 200);
await waitForCustomWorldAgentOperation({
baseUrl,
token: entry.token,
sessionId: readySession.sessionId,
operationId: foundationPayload.operation.operationId,
expectedStatus: 'completed',
});
const streamResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/agent/sessions/${encodeURIComponent(readySession.sessionId)}/messages/stream`,
withBearer(entry.token, {
method: 'POST',
headers: {
Accept: 'text/event-stream',
},
body: JSON.stringify({
clientMessageId: 'stream-client-1',
text: '把守灯会的压力再收紧一些,玩家与沈砺的旧案关系也再明显一点。',
focusCardId: null,
selectedCardIds: [],
}),
}),
);
const streamText = await streamResponse.text();
const sessionEventMatch = streamText.match(/event: session\s+data: (\{.*\})/u);
assert.equal(streamResponse.status, 200);
assert.match(
streamResponse.headers.get('content-type') ?? '',
/text\/event-stream/u,
);
assert.match(streamText, /event: reply_delta/u);
assert.match(streamText, /event: session/u);
assert.match(streamText, /event: done/u);
assert.ok(sessionEventMatch?.[1]);
const sessionEvent = JSON.parse(sessionEventMatch![1]) as {
session: {
stage: string;
supportedActions?: Array<{ action: string; enabled: boolean }>;
resultPreview?: {
source: string;
preview: { name?: string };
} | null;
};
};
assert.equal(sessionEvent.session.stage, 'object_refining');
assert.equal(
sessionEvent.session.supportedActions?.some(
(entry) =>
entry.action === 'update_draft_card' && entry.enabled === true,
),
true,
);
assert.equal(
sessionEvent.session.resultPreview?.source,
'session_preview',
);
assert.ok(sessionEvent.session.resultPreview?.preview?.name);
},
);
});
test('custom world agent draft_foundation action rejects not-ready sessions over http', async () => {
await withTestServer(
'custom-world-agent-phase3-http-not-ready',
@@ -3038,6 +3372,240 @@ test('custom world agent update_draft_card action updates draft profile and card
);
});
test('custom world agent sync_result_profile action writes result snapshot back over http', async () => {
await withTestServer(
'custom-world-agent-sync-result-profile-http',
async ({ baseUrl, context }) => {
installTestCustomWorldAgentSingleTurnLlm(context);
const entry = await authEntry(
baseUrl,
'cw_agent_sync_result',
'secret123',
);
const session = await createObjectRefiningCustomWorldAgentSession({
baseUrl,
token: entry.token,
});
const actionResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/agent/sessions/${encodeURIComponent(session.sessionId)}/actions`,
withBearer(entry.token, {
method: 'POST',
body: JSON.stringify({
action: 'sync_result_profile',
profile: {
id: `agent-draft-${session.sessionId}`,
settingText: '被海雾吞没的旧航路群岛',
name: '潮雾列岛·结果页回写版',
subtitle: '旧灯塔与失控航路',
summary: '结果页里的最新世界概述已经回写到当前草稿。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船夜与假航灯背后的操盘链。',
templateWorldType: 'WUXIA',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
attributeSchema: {
id: 'schema:test',
worldId: 'CUSTOM',
schemaVersion: 1,
schemaName: '测试',
generatedFrom: {
worldType: 'CUSTOM',
worldName: '潮雾列岛·结果页回写版',
settingSummary: '测试',
tone: '测试',
conflictCore: '测试',
},
slots: [],
},
playableNpcs: [],
storyNpcs: [],
items: [],
landmarks: [],
generationMode: 'full',
generationStatus: 'complete',
},
}),
}),
);
const actionPayload = (await actionResponse.json()) as {
operation: {
operationId: string;
status: string;
};
};
assert.equal(actionResponse.status, 200);
assert.equal(actionPayload.operation.status, 'queued');
await waitForCustomWorldAgentOperation({
baseUrl,
token: entry.token,
sessionId: session.sessionId,
operationId: actionPayload.operation.operationId,
expectedStatus: 'completed',
});
const sessionResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world/agent/sessions/${encodeURIComponent(session.sessionId)}`,
{
headers: {
Authorization: `Bearer ${entry.token}`,
},
},
);
const sessionPayload = (await sessionResponse.json()) as {
draftProfile: {
name?: string;
summary?: string;
legacyResultProfile?: {
name?: string;
playerGoal?: string;
};
} | null;
};
assert.equal(sessionResponse.status, 200);
assert.equal(sessionPayload.draftProfile?.name, '潮雾列岛·结果页回写版');
assert.equal(
sessionPayload.draftProfile?.summary,
'结果页里的最新世界概述已经回写到当前草稿。',
);
assert.equal(
sessionPayload.draftProfile?.legacyResultProfile?.name,
'潮雾列岛·结果页回写版',
);
assert.equal(
sessionPayload.draftProfile?.legacyResultProfile?.playerGoal,
'查清沉船夜与假航灯背后的操盘链。',
);
},
);
});
test('library publish for agent-backed draft returns explicit blocker message when Phase4 gate is not ready', async () => {
await withTestServer(
'custom-world-library-agent-publish-blocked',
async ({ baseUrl, context }) => {
installTestCustomWorldAgentSingleTurnLlm(context);
const entry = await authEntry(
baseUrl,
'cw_library_agent_blocked',
'secret123',
);
const session = await createObjectRefiningCustomWorldAgentSession({
baseUrl,
token: entry.token,
});
const profileId = `agent-draft-${session.sessionId}`;
const publishResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library/${encodeURIComponent(profileId)}/publish`,
withBearer(entry.token, {
method: 'POST',
}),
);
const publishPayload = (await publishResponse.json()) as {
error: {
code: string;
message: string;
};
};
const sessionAfterPublishAttempt =
await context.customWorldAgentOrchestrator.getSessionSnapshot(
entry.user.id,
session.sessionId,
);
assert.equal(publishResponse.status, 409);
assert.equal(publishPayload.error.code, 'CONFLICT');
assert.match(
publishPayload.error.message,
/ \d+ blocker/u,
);
assert.match(
publishPayload.error.message,
/||线/u,
);
assert.notEqual(sessionAfterPublishAttempt?.stage, 'published');
},
);
});
test('library publish for agent-backed draft reuses Phase4 gate and syncs session into published stage', async () => {
await withTestServer(
'custom-world-library-agent-publish-success',
async ({ baseUrl, context }) => {
installTestCustomWorldAgentSingleTurnLlm(context);
const entry = await authEntry(
baseUrl,
'cw_library_agent_success',
'secret123',
);
const session = await createObjectRefiningCustomWorldAgentSession({
baseUrl,
token: entry.token,
});
const profileId = `agent-draft-${session.sessionId}`;
await markAgentSessionPublishReady({
context,
userId: entry.user.id,
sessionId: session.sessionId,
});
const publishResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library/${encodeURIComponent(profileId)}/publish`,
withBearer(entry.token, {
method: 'POST',
}),
);
const publishPayload = (await publishResponse.json()) as {
entry: {
profileId: string;
visibility: 'draft' | 'published';
};
};
const libraryResponse = await httpRequest(
`${baseUrl}/api/runtime/custom-world-library`,
withBearer(entry.token),
);
const libraryPayload = (await libraryResponse.json()) as {
entries: Array<{
profileId: string;
visibility: 'draft' | 'published';
}>;
};
const sessionAfterPublish =
await context.customWorldAgentOrchestrator.getSessionSnapshot(
entry.user.id,
session.sessionId,
);
assert.equal(publishResponse.status, 200);
assert.equal(publishPayload.entry.profileId, profileId);
assert.equal(publishPayload.entry.visibility, 'published');
assert.equal(libraryResponse.status, 200);
assert.equal(
libraryPayload.entries.find((item) => item.profileId === profileId)
?.visibility,
'published',
);
assert.equal(sessionAfterPublish?.stage, 'published');
assert.equal(sessionAfterPublish?.resultPreview?.publishReady, true);
assert.equal(sessionAfterPublish?.resultPreview?.canEnterWorld, true);
assert.deepEqual(sessionAfterPublish?.resultPreview?.blockers ?? [], []);
assert.ok(
sessionAfterPublish?.messages.some(
(message) =>
message.kind === 'action_result' &&
message.text.includes('已正式发布'),
),
);
},
);
});
test('custom world agent generate_characters action appends character cards over http', async () => {
await withTestServer(
'custom-world-agent-phase4-generate-characters-http',

View File

@@ -8,11 +8,14 @@ import { errorHandler } from './middleware/errorHandler.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import { responseEnvelopeMiddleware } from './middleware/responseEnvelope.js';
import { createCharacterAssetRoutes } from './modules/assets/characterAssetRoutes.js';
import { createQwenSpriteRoutes } from './modules/assets/qwenSpriteRoutes.js';
import { createEditorRoutes } from './modules/editor/editorRoutes.js';
import { createStoryActionRoutes } from './modules/story/storyActionRoutes.js';
import { createAuthRoutes } from './routes/authRoutes.js';
import { createRuntimeRoutes } from './routes/runtimeRoutes.js';
import { createRpgEntrySaveRoutes } from './routes/rpg-entry/rpgEntrySaveRoutes.js';
import { createRpgWorldLibraryRoutes } from './routes/rpg-entry/rpgWorldLibraryRoutes.js';
import { createRpgProfileRoutes } from './routes/rpg-profile/rpgProfileRoutes.js';
import { createRpgRuntimeAiAssistRoutes } from './routes/rpg-runtime/rpgRuntimeAiAssistRoutes.js';
import { createRpgRuntimeStoryRoutes } from './routes/rpg-runtime/rpgRuntimeStoryRoutes.js';
import { createCustomWorldAgentRoutes } from './routes/customWorldAgent.js';
function matchesRoutePrefix(
request: express.Request,
@@ -120,16 +123,31 @@ export function createApp(context: AppContext) {
),
);
app.use(
'/api',
scopeToPrefixes(
['/api/assets/qwen-sprite'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'assets.qwen' }),
['/runtime/profile', '/profile', '/runtime/settings'],
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.profile.api' }),
),
createRpgProfileRoutes(context),
);
app.use(
'/api',
scopeToPrefixes(
['/api/assets/qwen-sprite'],
createQwenSpriteRoutes(context.config),
['/runtime/save', '/runtime/profile/save-archives', '/profile/save-archives'],
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.entry.save.api' }),
),
createRpgEntrySaveRoutes(context),
);
app.use(
'/api',
scopeToPrefixes(
['/runtime/custom-world-gallery', '/runtime/custom-world/works', '/runtime/custom-world-library'],
withRouteMeta({
routeVersion: '2026-04-21',
operation: 'rpg.entry.worldLibrary.api',
}),
),
createRpgWorldLibraryRoutes(context),
);
app.use(
'/api/auth',
@@ -138,13 +156,61 @@ export function createApp(context: AppContext) {
);
app.use(
'/api/runtime/story',
withRouteMeta({ routeVersion: '2026-04-08' }),
createStoryActionRoutes(context),
withRouteMeta({ routeVersion: '2026-04-21' }),
createRpgRuntimeStoryRoutes(context),
);
app.use(
scopeToPrefixes(
[
'/llm/chat/completions',
'/custom-world/cover-image',
'/custom-world/cover-upload',
'/custom-world/scene-image',
'/custom-world/entity',
'/custom-world/scene-npc',
'/runtime/custom-world/entity',
'/runtime/custom-world/scene-npc',
'/runtime/custom-world/profile',
'/runtime/story/initial',
'/runtime/story/continue',
'/runtime/chat',
'/runtime/items',
'/runtime/quests',
'/ws/health',
],
withRouteMeta({
routeVersion: '2026-04-21',
operation: 'rpg.runtime.aiAssist.api',
}),
),
);
app.use(
'/api',
withRouteMeta({ routeVersion: '2026-04-08' }),
createRuntimeRoutes(context),
scopeToPrefixes(
[
'/llm/chat/completions',
'/custom-world/cover-image',
'/custom-world/cover-upload',
'/custom-world/scene-image',
'/custom-world/entity',
'/custom-world/scene-npc',
'/runtime/custom-world/entity',
'/runtime/custom-world/scene-npc',
'/runtime/custom-world/profile',
'/runtime/story/initial',
'/runtime/story/continue',
'/runtime/chat',
'/runtime/items',
'/runtime/quests',
'/ws/health',
],
createRpgRuntimeAiAssistRoutes(context),
),
);
app.use(
'/api/runtime/custom-world/agent',
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.creation.agent.api' }),
createCustomWorldAgentRoutes(context),
);
app.use(
express.static(context.config.publicDir, {

View File

@@ -32,6 +32,21 @@ function buildCookieParts(
return parts.join('; ');
}
function appendSetCookieHeader(response: Response, cookieValue: string) {
const currentHeader = response.getHeader('Set-Cookie');
if (!currentHeader) {
response.setHeader('Set-Cookie', cookieValue);
return;
}
if (Array.isArray(currentHeader)) {
response.setHeader('Set-Cookie', [...currentHeader, cookieValue]);
return;
}
response.setHeader('Set-Cookie', [String(currentHeader), cookieValue]);
}
export function hashRefreshSessionToken(token: string) {
return crypto.createHash('sha256').update(token).digest('hex');
}
@@ -46,8 +61,8 @@ export function setRefreshSessionCookie(
token: string,
maxAgeSeconds: number,
) {
response.setHeader(
'Set-Cookie',
appendSetCookieHeader(
response,
buildCookieParts(config, token, {
maxAgeSeconds,
}),
@@ -55,8 +70,8 @@ export function setRefreshSessionCookie(
}
export function clearRefreshSessionCookie(response: Response, config: AppConfig) {
response.setHeader(
'Set-Cookie',
appendSetCookieHeader(
response,
buildCookieParts(config, '', {
maxAgeSeconds: 0,
}),

View File

@@ -1,6 +0,0 @@
// Temporary bridge for legacy pure build calculation logic from src/**.
export { getEquipmentBonuses } from '../modules/runtime/runtimeEquipmentModule.js';
export {
getPlayerBuildDamageBreakdown,
resolvePlayerOutgoingDamageResult,
} from '../modules/runtime/runtimeBuildModule.js';

View File

@@ -1,8 +0,0 @@
// Temporary bridge for legacy pure runtime item resolution logic from src/**.
export {
buildLooseRuntimeItemGenerationContext,
buildQuestRuntimeItemGenerationContext,
buildDirectedRuntimeReward,
buildRuntimeInventoryStock,
flattenDirectedRuntimeRewardItems,
} from '../modules/runtime-item/runtimeItemModule.js';

View File

@@ -74,6 +74,11 @@ export type AppConfig = {
mockAvatarUrl: string;
};
authSession: {
accessCookieName: string;
accessCookieTtlSeconds: number;
accessCookieSecure: boolean;
accessCookieSameSite: 'Lax' | 'Strict' | 'None';
accessCookiePath: string;
refreshCookieName: string;
refreshSessionTtlDays: number;
refreshCookieSecure: boolean;
@@ -274,6 +279,11 @@ export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
'AUTH_REFRESH_COOKIE_SAME_SITE',
'Lax',
);
const accessSameSite = readString(
env,
'AUTH_ACCESS_COOKIE_SAME_SITE',
'Lax',
);
return {
nodeEnv,
@@ -484,6 +494,30 @@ export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
mockAvatarUrl: readString(env, 'WECHAT_MOCK_AVATAR_URL', ''),
},
authSession: {
accessCookieName: readString(
env,
'AUTH_ACCESS_COOKIE_NAME',
'genarrative_access_session',
),
accessCookieTtlSeconds: readPositiveInt(
env,
'AUTH_ACCESS_COOKIE_TTL_SECONDS',
7200,
),
accessCookieSecure: readBoolean(
env,
'AUTH_ACCESS_COOKIE_SECURE',
readString(env, 'NODE_ENV', 'development') === 'production',
),
accessCookieSameSite:
accessSameSite === 'None' || accessSameSite === 'Strict'
? (accessSameSite as AppConfig['authSession']['accessCookieSameSite'])
: 'Lax',
accessCookiePath: readString(
env,
'AUTH_ACCESS_COOKIE_PATH',
'/',
),
refreshCookieName: readString(
env,
'AUTH_REFRESH_COOKIE_NAME',

View File

@@ -5,6 +5,13 @@ import type { AppDatabase } from './db.js';
import { AuthAuditLogRepository } from './repositories/authAuditLogRepository.js';
import { AuthIdentityRepository } from './repositories/authIdentityRepository.js';
import { AuthRiskBlockRepository } from './repositories/authRiskBlockRepository.js';
import type { RpgAgentSessionRepository } from './repositories/RpgAgentSessionRepository.js';
import type { RpgSaveArchiveRepository } from './repositories/rpg-entry/RpgSaveArchiveRepository.js';
import type { RpgWorldLibraryRepository } from './repositories/rpg-entry/RpgWorldLibraryRepository.js';
import type { RpgBrowseHistoryRepository } from './repositories/rpg-profile/RpgBrowseHistoryRepository.js';
import type { RpgProfileDashboardRepository } from './repositories/rpg-profile/RpgProfileDashboardRepository.js';
import type { RpgRuntimeSnapshotRepository } from './repositories/rpg-runtime/RpgRuntimeSnapshotRepository.js';
import type { RpgWorldProfileRepository } from './repositories/RpgWorldProfileRepository.js';
import { RuntimeRepository } from './repositories/runtimeRepository.js';
import { SmsAuthEventRepository } from './repositories/smsAuthEventRepository.js';
import { UserRepository } from './repositories/userRepository.js';
@@ -12,8 +19,8 @@ import { UserSessionRepository } from './repositories/userSessionRepository.js';
import { CaptchaChallengeStore } from './services/captchaChallengeStore.js';
import { CustomWorldAgentOrchestrator } from './services/customWorldAgentOrchestrator.js';
import { CustomWorldAgentSessionStore } from './services/customWorldAgentSessionStore.js';
import { CustomWorldSessionStore } from './services/customWorldSessionStore.js';
import { UpstreamLlmClient } from './services/llmClient.js';
import type { RpgWorldWorkSummaryService } from './services/RpgWorldWorkSummaryService.js';
import type { SmsVerificationService } from './services/smsVerificationService.js';
import type { WechatAuthService } from './services/wechatAuthService.js';
import { WechatAuthStateStore } from './services/wechatAuthStateStore.js';
@@ -28,11 +35,18 @@ export type AppContext = {
authRiskBlockRepository: AuthRiskBlockRepository;
smsAuthEventRepository: SmsAuthEventRepository;
userSessionRepository: UserSessionRepository;
rpgAgentSessionRepository: RpgAgentSessionRepository;
rpgWorldProfileRepository: RpgWorldProfileRepository;
rpgProfileDashboardRepository: RpgProfileDashboardRepository;
rpgBrowseHistoryRepository: RpgBrowseHistoryRepository;
rpgSaveArchiveRepository: RpgSaveArchiveRepository;
rpgWorldLibraryRepository: RpgWorldLibraryRepository;
rpgRuntimeSnapshotRepository: RpgRuntimeSnapshotRepository;
runtimeRepository: RuntimeRepository;
llmClient: UpstreamLlmClient;
customWorldSessions: CustomWorldSessionStore;
customWorldAgentSessions: CustomWorldAgentSessionStore;
customWorldAgentOrchestrator: CustomWorldAgentOrchestrator;
rpgWorldWorkSummaryService: RpgWorldWorkSummaryService;
smsVerificationService: SmsVerificationService;
wechatAuthService: WechatAuthService;
wechatAuthStates: WechatAuthStateStore;

View File

@@ -81,6 +81,11 @@ function createTestConfig(databaseUrl: string): AppConfig {
mockAvatarUrl: '',
},
authSession: {
accessCookieName: 'genarrative_access_session',
accessCookieTtlSeconds: 7200,
accessCookieSecure: false,
accessCookieSameSite: 'Lax',
accessCookiePath: '/',
refreshCookieName: 'genarrative_refresh_session',
refreshSessionTtlDays: 30,
refreshCookieSecure: false,

View File

@@ -9,7 +9,7 @@ import type {
type NpcChatTurnCompletionDirective,
NpcChatTurnRequest,
NpcRecruitDialogueRequest,
} from '../../../../packages/shared/src/contracts/story.js';
} from '../../../../packages/shared/src/contracts/rpgRuntimeChat.js';
import { parseLineListContent } from '../../../../packages/shared/src/llm/parsers.js';
import { prepareEventStreamResponse } from '../../http.js';
import type { UpstreamLlmClient } from '../../services/llmClient.js';

View File

@@ -4,7 +4,7 @@ import test from 'node:test';
import type {
CharacterChatSuggestionsRequest,
NpcChatTurnRequest,
} from '../../../../packages/shared/src/contracts/story.js';
} from '../../../../packages/shared/src/contracts/rpgRuntimeChat.js';
import { createTestPlayerCharacter } from '../../testFixtures/runtimeCharacter.js';
import {
generateCharacterChatSuggestionsFromOrchestrator,

View File

@@ -1,912 +0,0 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import http, {
type IncomingMessage,
type RequestOptions,
type ServerResponse,
} from 'node:http';
import https from 'node:https';
import path from 'node:path';
import { Router, type NextFunction, type Request, type Response } from 'express';
import type { AppConfig } from '../../config.js';
import { routeMeta } from '../../middleware/routeMeta.js';
const QWEN_SPRITE_MASTER_GENERATE_PATH = '/api/assets/qwen-sprite/master';
const QWEN_SPRITE_SHEET_GENERATE_PATH = '/api/assets/qwen-sprite/sheet';
const QWEN_SPRITE_FRAME_REPAIR_PATH = '/api/assets/qwen-sprite/frame-repair';
const QWEN_SPRITE_SAVE_PATH = '/api/assets/qwen-sprite/save';
const DEFAULT_DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/api/v1';
const DEFAULT_QWEN_IMAGE_MODEL = 'qwen-image-2.0';
function readJsonBody(req: IncomingMessage & { body?: unknown }) {
const parsedBody = req.body;
if (parsedBody && typeof parsedBody === 'object' && !Array.isArray(parsedBody)) {
return Promise.resolve(parsedBody as Record<string, unknown>);
}
return new Promise<Record<string, unknown>>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
req.on('end', () => {
try {
const raw =
Buffer.concat(chunks)
.toString('utf8')
.replace(/^\uFEFF/u, '') || '{}';
resolve(JSON.parse(raw));
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
}
function sendJson(res: ServerResponse, statusCode: number, payload: unknown) {
res.statusCode = statusCode;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(payload));
}
function isRecordValue(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every((item) => typeof item === 'string' && item.trim().length > 0)
);
}
function resolveRuntimeEnv(config: AppConfig) {
return config.rawEnv;
}
function normalizeDashScopeBaseUrl(value: string) {
return value.replace(/\/$/u, '');
}
function extractApiErrorMessage(responseText: string, fallbackMessage: string) {
if (!responseText.trim()) {
return fallbackMessage;
}
try {
const parsed = JSON.parse(responseText) as {
code?: string;
message?: string;
error?: { message?: string };
};
if (
typeof parsed.error?.message === 'string' &&
parsed.error.message.trim()
) {
return parsed.error.message;
}
if (typeof parsed.message === 'string' && parsed.message.trim()) {
return parsed.message;
}
if (typeof parsed.code === 'string' && parsed.code.trim()) {
return `${fallbackMessage} (${parsed.code})`;
}
} catch {
// Fall through to raw text.
}
return responseText;
}
function sanitizePathSegment(value: string) {
const normalized = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9-_]+/gu, '-')
.replace(/-+/gu, '-')
.replace(/^-|-$/gu, '');
return normalized || 'asset';
}
function createTimestampId(prefix: string) {
return `${prefix}-${Date.now()}`;
}
function requestTextResponse(
urlString: string,
options: {
method?: string;
headers?: Record<string, string>;
bodyText?: string;
} = {},
) {
return new Promise<{
statusCode: number;
headers: Record<string, string | string[] | undefined>;
bodyText: string;
}>((resolve, reject) => {
const url = new URL(urlString);
const transport = url.protocol === 'https:' ? https : http;
const payload = options.bodyText;
const requestOptions: RequestOptions = {
protocol: url.protocol,
hostname: url.hostname,
port: url.port ? Number(url.port) : undefined,
path: `${url.pathname}${url.search}`,
method: options.method ?? 'GET',
headers: {
...(options.headers ?? {}),
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
},
};
const request = transport.request(requestOptions, (upstreamRes) => {
const chunks: Buffer[] = [];
upstreamRes.on('data', (chunk) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
upstreamRes.on('end', () => {
resolve({
statusCode: upstreamRes.statusCode ?? 502,
headers: upstreamRes.headers,
bodyText: Buffer.concat(chunks).toString('utf8'),
});
});
upstreamRes.on('error', reject);
});
request.on('error', reject);
if (payload) {
request.write(payload);
}
request.end();
});
}
function requestBinaryResponse(
urlString: string,
options: {
method?: string;
headers?: Record<string, string>;
} = {},
) {
return new Promise<{
statusCode: number;
headers: Record<string, string | string[] | undefined>;
body: Buffer;
}>((resolve, reject) => {
const url = new URL(urlString);
const transport = url.protocol === 'https:' ? https : http;
const requestOptions: RequestOptions = {
protocol: url.protocol,
hostname: url.hostname,
port: url.port ? Number(url.port) : undefined,
path: `${url.pathname}${url.search}`,
method: options.method ?? 'GET',
headers: options.headers ?? {},
};
const request = transport.request(requestOptions, (upstreamRes) => {
const chunks: Buffer[] = [];
upstreamRes.on('data', (chunk) => {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
});
upstreamRes.on('end', () => {
resolve({
statusCode: upstreamRes.statusCode ?? 502,
headers: upstreamRes.headers,
body: Buffer.concat(chunks),
});
});
upstreamRes.on('error', reject);
});
request.on('error', reject);
request.end();
});
}
function proxyJsonRequest(
urlString: string,
apiKey: string,
body: Record<string, unknown>,
) {
return requestTextResponse(urlString, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
bodyText: JSON.stringify(body),
});
}
function collectStringsByKey(
value: unknown,
targetKey: string,
results: string[],
) {
if (Array.isArray(value)) {
value.forEach((item) => collectStringsByKey(item, targetKey, results));
return;
}
if (!isRecordValue(value)) {
return;
}
const directValue = value[targetKey];
if (typeof directValue === 'string' && directValue.trim()) {
results.push(directValue.trim());
}
Object.values(value).forEach((nestedValue) =>
collectStringsByKey(nestedValue, targetKey, results),
);
}
function extractImageUrls(payload: Record<string, unknown>) {
const results: string[] = [];
collectStringsByKey(payload.output, 'image', results);
collectStringsByKey(payload.output, 'url', results);
return [...new Set(results)];
}
function parseDataUrl(source: string) {
const matched = /^data:(image\/[^;]+);base64,(.+)$/u.exec(source);
if (!matched) {
return null;
}
const mimeType = matched[1];
const base64Payload = matched[2];
const extension = (() => {
switch (mimeType) {
case 'image/jpeg':
return 'jpg';
case 'image/webp':
return 'webp';
default:
return 'png';
}
})();
return {
buffer: Buffer.from(base64Payload, 'base64'),
extension,
};
}
async function resolveImageSourcePayload(rootDir: string, source: string) {
const parsedDataUrl = parseDataUrl(source);
if (parsedDataUrl) {
return parsedDataUrl;
}
if (!source.startsWith('/')) {
throw new Error('图像来源必须是 Data URL 或 public 目录 URL。');
}
const normalizedSource = path.posix.normalize(source).replace(/^\/+/u, '');
const absolutePath = path.resolve(
rootDir,
'public',
...normalizedSource.split('/'),
);
const publicRoot = path.resolve(rootDir, 'public');
if (!absolutePath.startsWith(publicRoot)) {
throw new Error('图像来源路径越界。');
}
const buffer = await readFile(absolutePath);
const extension = path.extname(absolutePath).replace(/^\./u, '') || 'png';
return {
buffer,
extension,
};
}
async function resolveImageSourceAsDataUrl(rootDir: string, source: string) {
if (/^data:image\/[^;]+;base64,/u.test(source)) {
return source;
}
const payload = await resolveImageSourcePayload(rootDir, source);
const mimeType = (() => {
switch (payload.extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'webp':
return 'image/webp';
default:
return 'image/png';
}
})();
return `data:${mimeType};base64,${payload.buffer.toString('base64')}`;
}
async function writeDraftImageFile(
rootDir: string,
relativePath: string,
buffer: Buffer,
) {
const absolutePath = path.resolve(rootDir, 'public', ...relativePath.split('/'));
await mkdir(path.dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, buffer);
return `/${relativePath}`;
}
async function generateQwenImages(
config: AppConfig,
input: {
kind: 'master' | 'sheet' | 'repair';
promptText: string;
negativePrompt: string;
model: string;
size: string;
promptExtend: boolean;
seed?: number;
candidateCount: number;
referenceImages: string[];
},
) {
const rootDir = config.projectRoot;
const runtimeEnv = resolveRuntimeEnv(config);
const baseUrl = normalizeDashScopeBaseUrl(
runtimeEnv.DASHSCOPE_BASE_URL || DEFAULT_DASHSCOPE_BASE_URL,
);
const apiKey = runtimeEnv.DASHSCOPE_API_KEY || '';
if (!apiKey) {
throw new Error('服务端缺少 DASHSCOPE_API_KEY无法调用 Qwen-Image。');
}
const content = [
...(await Promise.all(
input.referenceImages
.slice(0, 3)
.map(async (image) => ({ image: await resolveImageSourceAsDataUrl(rootDir, image) })),
)),
{ text: input.promptText },
];
const requestPayload: Record<string, unknown> = {
model: input.model || DEFAULT_QWEN_IMAGE_MODEL,
input: {
messages: [
{
role: 'user',
content,
},
],
},
parameters: {
n: Math.max(1, Math.min(6, input.candidateCount)),
negative_prompt: input.negativePrompt,
prompt_extend: input.promptExtend,
watermark: false,
size: input.size,
...(typeof input.seed === 'number' && Number.isFinite(input.seed)
? { seed: input.seed }
: {}),
},
};
const response = await proxyJsonRequest(
`${baseUrl}/services/aigc/multimodal-generation/generation`,
apiKey,
requestPayload,
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw new Error(
extractApiErrorMessage(response.bodyText, 'Qwen-Image 生成失败。'),
);
}
const parsed = JSON.parse(response.bodyText) as Record<string, unknown>;
const imageUrls = extractImageUrls(parsed);
if (imageUrls.length === 0) {
throw new Error('Qwen-Image 未返回可下载的图片结果。');
}
const draftId = createTimestampId(`qwen-${input.kind}`);
const relativeDir = path.posix.join(
'generated-qwen-sprites',
'_drafts',
input.kind,
draftId,
);
const drafts = await Promise.all(
imageUrls.map(async (imageUrl, index) => {
const binaryResponse = await requestBinaryResponse(imageUrl);
if (
binaryResponse.statusCode < 200 ||
binaryResponse.statusCode >= 300
) {
throw new Error(`下载生成图片失败(${binaryResponse.statusCode})。`);
}
const imageSrc = await writeDraftImageFile(
rootDir,
path.posix.join(relativeDir, `candidate-${String(index + 1).padStart(2, '0')}.png`),
binaryResponse.body,
);
return {
id: `${draftId}-${index + 1}`,
label: `${input.kind === 'master' ? '主图' : input.kind === 'sheet' ? '精灵表' : '修帧'} ${index + 1}`,
imageSrc,
remoteUrl: imageUrl,
};
}),
);
await writeFile(
path.resolve(rootDir, 'public', ...relativeDir.split('/'), 'job.json'),
JSON.stringify(
{
draftId,
kind: input.kind,
model: input.model,
size: input.size,
promptText: input.promptText,
negativePrompt: input.negativePrompt,
promptExtend: input.promptExtend,
seed: input.seed,
candidateCount: input.candidateCount,
referenceImageCount: input.referenceImages.length,
drafts,
createdAt: new Date().toISOString(),
},
null,
2,
) + '\n',
'utf8',
);
return {
draftId,
drafts,
model: input.model,
size: input.size,
promptText: input.promptText,
negativePrompt: input.negativePrompt,
};
}
async function handleGenerateMaster(
config: AppConfig,
req: IncomingMessage & { body?: unknown },
res: ServerResponse,
) {
if (req.method !== 'POST') {
sendJson(res, 405, { error: { message: 'Method Not Allowed' } });
return;
}
let body: Record<string, unknown>;
try {
body = await readJsonBody(req);
} catch {
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
return;
}
const promptText =
typeof body.promptText === 'string' ? body.promptText.trim() : '';
const negativePrompt =
typeof body.negativePrompt === 'string' ? body.negativePrompt.trim() : '';
const model =
typeof body.model === 'string' && body.model.trim()
? body.model.trim()
: DEFAULT_QWEN_IMAGE_MODEL;
const size =
typeof body.size === 'string' && body.size.trim()
? body.size.trim()
: '1024*1024';
const promptExtend = body.promptExtend !== false;
const candidateCount =
typeof body.candidateCount === 'number' && Number.isFinite(body.candidateCount)
? body.candidateCount
: 1;
const seed =
typeof body.seed === 'number' && Number.isFinite(body.seed)
? body.seed
: undefined;
const referenceImages = isStringArray(body.referenceImages)
? body.referenceImages
: [];
if (!promptText) {
sendJson(res, 400, { error: { message: 'promptText is required.' } });
return;
}
try {
const result = await generateQwenImages(config, {
kind: 'master',
promptText,
negativePrompt,
model,
size,
promptExtend,
seed,
candidateCount,
referenceImages,
});
sendJson(res, 200, {
ok: true,
...result,
});
} catch (error) {
sendJson(res, 500, {
error: {
message: error instanceof Error ? error.message : '生成主图失败。',
},
});
}
}
async function handleGenerateSheet(
config: AppConfig,
req: IncomingMessage & { body?: unknown },
res: ServerResponse,
) {
if (req.method !== 'POST') {
sendJson(res, 405, { error: { message: 'Method Not Allowed' } });
return;
}
let body: Record<string, unknown>;
try {
body = await readJsonBody(req);
} catch {
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
return;
}
const promptText =
typeof body.promptText === 'string' ? body.promptText.trim() : '';
const negativePrompt =
typeof body.negativePrompt === 'string' ? body.negativePrompt.trim() : '';
const model =
typeof body.model === 'string' && body.model.trim()
? body.model.trim()
: DEFAULT_QWEN_IMAGE_MODEL;
const size =
typeof body.size === 'string' && body.size.trim()
? body.size.trim()
: '1024*1024';
const promptExtend = body.promptExtend !== false;
const candidateCount =
typeof body.candidateCount === 'number' && Number.isFinite(body.candidateCount)
? body.candidateCount
: 1;
const seed =
typeof body.seed === 'number' && Number.isFinite(body.seed)
? body.seed
: undefined;
const referenceImages = isStringArray(body.referenceImages)
? body.referenceImages
: [];
if (!promptText) {
sendJson(res, 400, { error: { message: 'promptText is required.' } });
return;
}
try {
const result = await generateQwenImages(config, {
kind: 'sheet',
promptText,
negativePrompt,
model,
size,
promptExtend,
seed,
candidateCount,
referenceImages,
});
sendJson(res, 200, {
ok: true,
...result,
});
} catch (error) {
sendJson(res, 500, {
error: {
message: error instanceof Error ? error.message : '生成精灵表失败。',
},
});
}
}
async function handleRepairFrame(
config: AppConfig,
req: IncomingMessage & { body?: unknown },
res: ServerResponse,
) {
if (req.method !== 'POST') {
sendJson(res, 405, { error: { message: 'Method Not Allowed' } });
return;
}
let body: Record<string, unknown>;
try {
body = await readJsonBody(req);
} catch {
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
return;
}
const promptText =
typeof body.promptText === 'string' ? body.promptText.trim() : '';
const negativePrompt =
typeof body.negativePrompt === 'string' ? body.negativePrompt.trim() : '';
const model =
typeof body.model === 'string' && body.model.trim()
? body.model.trim()
: DEFAULT_QWEN_IMAGE_MODEL;
const size =
typeof body.size === 'string' && body.size.trim()
? body.size.trim()
: '512*512';
const promptExtend = body.promptExtend !== false;
const seed =
typeof body.seed === 'number' && Number.isFinite(body.seed)
? body.seed
: undefined;
const referenceImages = isStringArray(body.referenceImages)
? body.referenceImages
: [];
if (!promptText) {
sendJson(res, 400, { error: { message: 'promptText is required.' } });
return;
}
if (referenceImages.length === 0) {
sendJson(res, 400, {
error: { message: '至少需要一张参考图来修复帧。' },
});
return;
}
try {
const result = await generateQwenImages(config, {
kind: 'repair',
promptText,
negativePrompt,
model,
size,
promptExtend,
seed,
candidateCount: 1,
referenceImages,
});
sendJson(res, 200, {
ok: true,
...result,
repairedFrame: result.drafts[0] ?? null,
});
} catch (error) {
sendJson(res, 500, {
error: {
message: error instanceof Error ? error.message : '修帧失败。',
},
});
}
}
async function handleSaveAsset(
rootDir: string,
req: IncomingMessage & { body?: unknown },
res: ServerResponse,
) {
if (req.method !== 'POST') {
sendJson(res, 405, { error: { message: 'Method Not Allowed' } });
return;
}
let body: Record<string, unknown>;
try {
body = await readJsonBody(req);
} catch {
sendJson(res, 400, { error: { message: 'Invalid JSON body' } });
return;
}
const assetKey =
typeof body.assetKey === 'string' ? sanitizePathSegment(body.assetKey) : '';
const actionKey =
typeof body.actionKey === 'string' ? sanitizePathSegment(body.actionKey) : '';
const masterSource =
typeof body.masterSource === 'string' ? body.masterSource.trim() : '';
const sheetSource =
typeof body.sheetSource === 'string' ? body.sheetSource.trim() : '';
const framesDataUrls = isStringArray(body.framesDataUrls)
? body.framesDataUrls
: [];
const metadata = isRecordValue(body.metadata) ? body.metadata : {};
const prompts = isRecordValue(body.prompts) ? body.prompts : {};
if (!assetKey) {
sendJson(res, 400, { error: { message: 'assetKey is required.' } });
return;
}
if (!actionKey) {
sendJson(res, 400, { error: { message: 'actionKey is required.' } });
return;
}
if (!sheetSource) {
sendJson(res, 400, { error: { message: 'sheetSource is required.' } });
return;
}
try {
const assetId = createTimestampId('qwen-sprite');
const relativeDir = path.posix.join(
'generated-qwen-sprites',
assetKey,
actionKey,
assetId,
);
const absoluteDir = path.resolve(rootDir, 'public', ...relativeDir.split('/'));
await mkdir(path.join(absoluteDir, 'frames'), { recursive: true });
let masterImagePath: string | null = null;
if (masterSource) {
const payload = await resolveImageSourcePayload(rootDir, masterSource);
masterImagePath = await writeDraftImageFile(
rootDir,
path.posix.join(relativeDir, `master.${payload.extension}`),
payload.buffer,
);
}
const sheetPayload = await resolveImageSourcePayload(rootDir, sheetSource);
const sheetImagePath = await writeDraftImageFile(
rootDir,
path.posix.join(relativeDir, `sheet.${sheetPayload.extension}`),
sheetPayload.buffer,
);
const framePaths: string[] = [];
for (let index = 0; index < framesDataUrls.length; index += 1) {
const framePayload = await resolveImageSourcePayload(
rootDir,
framesDataUrls[index] ?? '',
);
const framePath = await writeDraftImageFile(
rootDir,
path.posix.join(
relativeDir,
'frames',
`frame-${String(index + 1).padStart(2, '0')}.${framePayload.extension}`,
),
framePayload.buffer,
);
framePaths.push(framePath);
}
await writeFile(
path.join(absoluteDir, 'metadata.json'),
JSON.stringify(
{
assetId,
assetKey,
actionKey,
masterImagePath,
sheetImagePath,
framePaths,
metadata,
prompts,
createdAt: new Date().toISOString(),
},
null,
2,
) + '\n',
'utf8',
);
sendJson(res, 200, {
ok: true,
assetId,
assetDir: `/${relativeDir}`,
masterImagePath,
sheetImagePath,
framePaths,
saveMessage: '已保存到 public/generated-qwen-sprites。',
});
} catch (error) {
sendJson(res, 500, {
error: {
message: error instanceof Error ? error.message : '保存精灵表资产失败。',
},
});
}
}
function toExpressHandler(
handler: (
request: IncomingMessage & { body?: unknown },
response: ServerResponse,
) => Promise<void> | void,
) {
return (request: Request, response: Response, next: NextFunction) => {
Promise.resolve(
handler(
request as Request & IncomingMessage & { body?: unknown },
response as Response & ServerResponse,
),
).catch(next);
};
}
export function createQwenSpriteRoutes(config: AppConfig) {
const router = Router();
router.use((request, response, next) => {
if (
request.path !== '/api/assets' &&
!request.path.startsWith('/api/assets/')
) {
next();
return;
}
if (!config.assetsApiEnabled) {
response.status(403).json({
error: {
message: '资产工具接口当前未启用。',
},
});
return;
}
next();
});
router.post(
QWEN_SPRITE_MASTER_GENERATE_PATH,
routeMeta({ operation: 'assets.qwenSprite.master.generate' }),
toExpressHandler((request, response) =>
handleGenerateMaster(config, request, response),
),
);
router.post(
QWEN_SPRITE_SHEET_GENERATE_PATH,
routeMeta({ operation: 'assets.qwenSprite.sheet.generate' }),
toExpressHandler((request, response) =>
handleGenerateSheet(config, request, response),
),
);
router.post(
QWEN_SPRITE_FRAME_REPAIR_PATH,
routeMeta({ operation: 'assets.qwenSprite.frameRepair.generate' }),
toExpressHandler((request, response) =>
handleRepairFrame(config, request, response),
),
);
router.post(
QWEN_SPRITE_SAVE_PATH,
routeMeta({ operation: 'assets.qwenSprite.asset.save' }),
toExpressHandler((request, response) =>
handleSaveAsset(config.projectRoot, request, response),
),
);
return router;
}

View File

@@ -1,8 +1,10 @@
import type {
RuntimeBattlePresentation,
RuntimeStoryChoicePayload,
RuntimeStoryPatch,
} from '../../../../packages/shared/src/contracts/story.js';
} from '../../../../packages/shared/src/contracts/rpgRuntimeStoryState.js';
import type {
RuntimeStoryChoicePayload,
} from '../../../../packages/shared/src/contracts/rpgRuntimeStoryAction.js';
import {
buildInventoryUseResultText,
incrementGameRuntimeStats,
@@ -26,7 +28,7 @@ import {
getPlayerSkillCooldowns,
setEncounterNpcState,
type RuntimeSession,
} from '../story/runtimeSession.js';
} from '../rpg-runtime-story/RpgRuntimeSessionPrimitives.js';
type CombatActionConfig = {
actionText: string;

View File

@@ -0,0 +1,365 @@
import type {
AttributeVector,
CustomWorldNpc,
CustomWorldPlayableNpc,
RoleAttributeProfile,
WorldAttributeSchema,
WorldAttributeSlot,
WorldType,
} from '../runtimeTypes.js';
import { inferWorldTypeFromSetting } from './creatorIntentBridge.js';
import { slugify } from './normalizeShared.js';
/**
* 工作包 G
* 把 attribute schema 构建和角色属性画像编译从主 runtime compiler 中抽离,
* 让结果预览编译、世界基础 profile 归一和角色属性推导有清晰边界。
*/
const WORLD_ATTRIBUTE_SLOT_IDS = [
'axis_a',
'axis_b',
'axis_c',
'axis_d',
'axis_e',
'axis_f',
] as const;
const AXIS_KEYWORD_RULES: Array<{
slotId: string;
patterns: RegExp[];
weight: number;
}> = [
{ slotId: 'axis_a', patterns: [/||||||||||/u], weight: 16 },
{ slotId: 'axis_b', patterns: [/|||||||||/u], weight: 16 },
{ slotId: 'axis_c', patterns: [/|||||||||/u], weight: 16 },
{ slotId: 'axis_d', patterns: [/|||||||||/u], weight: 16 },
{ slotId: 'axis_e', patterns: [/|||||||||/u], weight: 16 },
{ slotId: 'axis_f', patterns: [/|||||||||/u], weight: 16 },
];
export function buildTemplateWorldAttributeSchema(
worldType: Exclude<WorldType, 'CUSTOM'>,
) {
const common = {
schemaVersion: 1,
generatedFrom:
worldType === 'XIANXIA'
? {
worldType: 'XIANXIA' as const,
worldName: '仙侠',
settingSummary: '灵潮、宗门、禁制、秘境与道途交织。',
tone: '空灵、危险、带着灾变与大道压迫。',
conflictCore: '在裂变与因果之间稳住自我与道途。',
}
: {
worldType: 'WUXIA' as const,
worldName: '武侠',
settingSummary: '江湖、门派、旧案与人情纠葛并存。',
tone: '克制、紧张、讲究局势与心气。',
conflictCore: '在人情、威压与旧案之间立住自身。',
},
};
if (worldType === 'XIANXIA') {
return {
id: 'schema:xianxia:v1',
worldId: 'XIANXIA',
schemaName: '灵界六轴',
...common,
slots: [
{
slotId: 'axis_a',
name: '道骨',
definition: '承载道压与高强度冲击的底子。',
positiveSignals: ['承压', '根基稳', '扛得住'],
negativeSignals: ['根基浅', '易溃', '承载不足'],
combatUseText: '扛住灵压、正面承受高强度对撞。',
socialUseText: '让人感到根基扎实,值得托付重事。',
explorationUseText: '承受秘境、禁制与裂隙带来的压迫。',
},
{
slotId: 'axis_b',
name: '灵行',
definition: '位移、御空、转场、抢占天时地利的能力。',
positiveSignals: ['位移', '御空', '机动'],
negativeSignals: ['迟滞', '失位', '转场慢'],
combatUseText: '抢位、御空、快速重整战场位置。',
socialUseText: '反应轻快,擅长顺势接住局面的变化。',
explorationUseText: '穿越高危地形、裂隙、云海与复杂禁区。',
},
{
slotId: 'axis_c',
name: '识海',
definition: '解析禁制、洞察因果、识破虚实的能力。',
positiveSignals: ['洞察', '解构', '看破'],
negativeSignals: ['迷失', '误判', '看不清'],
combatUseText: '识破术理、找出因果节点与破绽。',
socialUseText: '更容易辨认真话、虚言与隐藏动机。',
explorationUseText: '解读阵纹、禁制、旧史与环境异象。',
},
{
slotId: 'axis_d',
name: '劫纹',
definition: '在高危变化中强行推进、改写局势的能力。',
positiveSignals: ['强推', '决断', '逆转'],
negativeSignals: ['畏缩', '迟疑', '不敢碰变局'],
combatUseText: '在高压窗口里压上去,逼出变化与突破。',
socialUseText: '在关键谈判中拍板,推动他人表态。',
explorationUseText: '面对异变与风险时敢于推进关键节点。',
},
{
slotId: 'axis_e',
name: '心契',
definition: '与他者、器物、灵兽、誓约建立共鸣的能力。',
positiveSignals: ['共鸣', '结契', '安抚'],
negativeSignals: ['隔阂', '生硬', '难以共振'],
combatUseText: '与器物、灵兽、同伴形成协同与共鸣。',
socialUseText: '建立信任、誓约与更深层的关系连结。',
explorationUseText: '借由共鸣打开封印、回应遗物或安抚异兽。',
},
{
slotId: 'axis_f',
name: '玄息',
definition: '循环灵息、稳住心神、让自身持续在线的能力。',
positiveSignals: ['稳态', '回转', '续航'],
negativeSignals: ['紊乱', '枯竭', '失衡'],
combatUseText: '维持灵息循环、拖住长线压力与消耗。',
socialUseText: '气息沉稳,不轻易乱阵脚或露出破绽。',
explorationUseText: '在漫长探索与灵潮侵蚀中维持可行动状态。',
},
] satisfies WorldAttributeSlot[],
} satisfies WorldAttributeSchema;
}
return {
id: 'schema:wuxia:v1',
worldId: 'WUXIA',
schemaVersion: 1,
schemaName: '江湖六脉',
generatedFrom: common.generatedFrom,
slots: [
{
slotId: 'axis_a',
name: '骨势',
definition: '扛压、顶冲、硬吃风险也不退的势头。',
positiveSignals: ['扛压', '硬桥硬马', '稳住正面'],
negativeSignals: ['虚浮', '怯退', '一碰就散'],
combatUseText: '顶住正面压力、换伤不退、撑住阵线。',
socialUseText: '在强压场面里不露怯,给人可靠或强硬之感。',
explorationUseText: '穿越险路、硬顶机关、承受高压环境。',
},
{
slotId: 'axis_b',
name: '身法',
definition: '腾挪、抢位、换线、把握出手节奏的能力。',
positiveSignals: ['快', '轻灵', '抢位'],
negativeSignals: ['迟缓', '失位', '笨重'],
combatUseText: '切线换位、闪转腾挪、争夺先手。',
socialUseText: '应变快,擅长观察气口并顺势接话。',
explorationUseText: '攀越、潜入、追踪与复杂地形穿行。',
},
{
slotId: 'axis_c',
name: '眼脉',
definition: '看破破绽、拆招、识局、看穿人心的能力。',
positiveSignals: ['识局', '洞察', '拆招'],
negativeSignals: ['迟钝', '误判', '看不透'],
combatUseText: '抓破绽、拆套路、找出最该切入的位置。',
socialUseText: '判断弦外之音、试探真假、识别来意。',
explorationUseText: '识破机关、辨认痕迹、看懂异状。',
},
{
slotId: 'axis_d',
name: '心焰',
definition: '决断、压迫、胆气、在局面中立住自身意志的能力。',
positiveSignals: ['胆气', '决断', '压迫'],
negativeSignals: ['犹疑', '软弱', '易被动摇'],
combatUseText: '逼迫对手、强行推进、在关键时刻拍板。',
socialUseText: '立威、定调、在谈判里压住场子。',
explorationUseText: '在未知风险前保持决断,不被局势拖死。',
},
{
slotId: 'axis_e',
name: '尘缘',
definition: '与人事、情面、承诺、牵引关系打交道的能力。',
positiveSignals: ['通人情', '会安抚', '懂交换'],
negativeSignals: ['生硬', '失礼', '不近人情'],
combatUseText: '借势协同、读懂同伴与对手的关系脉络。',
socialUseText: '安抚、求助、结盟、维系承诺与信任。',
explorationUseText: '从传闻、人脉和地方关系里打开线索。',
},
{
slotId: 'axis_f',
name: '玄息',
definition: '调息、稳态、久战、把自身维持在可用状态的能力。',
positiveSignals: ['稳', '续战', '调息'],
negativeSignals: ['紊乱', '易崩', '续不上'],
combatUseText: '续战、回气、稳住节奏与状态。',
socialUseText: '遇事不乱,语气和姿态都更沉稳可信。',
explorationUseText: '长线跋涉、在恶劣环境下维持专注与状态。',
},
] satisfies WorldAttributeSlot[],
} satisfies WorldAttributeSchema;
}
export function generateWorldAttributeSchema(input: {
worldName: string;
settingText: string;
summary: string;
tone: string;
playerGoal: string;
}) {
const inferredWorldType = inferWorldTypeFromSetting(input.settingText);
const template = buildTemplateWorldAttributeSchema(
inferredWorldType === 'XIANXIA' ? 'XIANXIA' : 'WUXIA',
);
return {
...template,
id: `schema:custom:${slugify(input.worldName)}`,
worldId: `custom:${input.worldName}`,
generatedFrom: {
worldType: 'CUSTOM',
worldName: input.worldName,
settingSummary: input.summary,
tone: input.tone,
conflictCore: input.playerGoal,
},
} satisfies WorldAttributeSchema;
}
function normalizeAttributeValues(
values: AttributeVector,
slotIds: readonly string[],
targetTotal = 360,
) {
const positiveValues = slotIds.map((slotId) => Math.max(0, values[slotId] ?? 0));
const rawTotal = positiveValues.reduce((sum, value) => sum + value, 0);
const normalized =
rawTotal > 0
? positiveValues.map((value) => (value / rawTotal) * targetTotal)
: slotIds.map(() => targetTotal / Math.max(slotIds.length, 1));
const rounded = normalized.map((value) => Math.max(0, Math.min(100, Math.round(value))));
return Object.fromEntries(
slotIds.map((slotId, index) => [slotId, rounded[index] ?? 0]),
) as AttributeVector;
}
function ensureRoleAttributeProfile(
profile: Partial<RoleAttributeProfile> | null | undefined,
schema: WorldAttributeSchema,
fallbackValues: AttributeVector,
): RoleAttributeProfile {
const slotIds = schema.slots.map((slot) => slot.slotId);
const values = normalizeAttributeValues(
{
...fallbackValues,
...(profile?.values ?? {}),
},
slotIds,
);
const sortedSlots = [...schema.slots]
.map((slot) => ({
slot,
value: values[slot.slotId] ?? 0,
}))
.sort((left, right) => right.value - left.value);
return {
schemaId: profile?.schemaId ?? schema.id,
values,
topTraits: sortedSlots.slice(0, 2).map((entry) => entry.slot.name),
hiddenTraits: profile?.hiddenTraits ? [...profile.hiddenTraits] : undefined,
evidence:
profile?.evidence?.length
? [...profile.evidence]
: sortedSlots.slice(0, 3).map((entry) => ({
slotId: entry.slot.slotId,
reason: `${entry.slot.name}在当前画像中最突出。`,
})),
};
}
function buildDefaultAxisVector(
overrides: Partial<Record<(typeof WORLD_ATTRIBUTE_SLOT_IDS)[number], number>>,
) {
return WORLD_ATTRIBUTE_SLOT_IDS.reduce<AttributeVector>((result, slotId) => {
result[slotId] = overrides[slotId] ?? 0;
return result;
}, {});
}
function buildRoleAttributeProfileFromTexts(params: {
schema: WorldAttributeSchema;
textBlocks: Array<string | null | undefined>;
}) {
const sourceText = params.textBlocks.filter(Boolean).join(' ');
const seed = buildDefaultAxisVector({
axis_a: 58,
axis_b: 58,
axis_c: 58,
axis_d: 58,
axis_e: 58,
axis_f: 58,
});
AXIS_KEYWORD_RULES.forEach((rule) => {
const matches = rule.patterns.reduce(
(count, pattern) => count + (pattern.test(sourceText) ? 1 : 0),
0,
);
if (matches <= 0) {
return;
}
seed[rule.slotId] = (seed[rule.slotId] ?? 0) + rule.weight * matches;
});
return ensureRoleAttributeProfile(
{
schemaId: params.schema.id,
},
params.schema,
seed,
);
}
export function buildCustomWorldPlayableNpcAttributeProfile(
npc: CustomWorldPlayableNpc,
schema: WorldAttributeSchema,
) {
return buildRoleAttributeProfileFromTexts({
schema,
textBlocks: [
npc.title,
npc.role,
npc.description,
npc.backstory,
npc.personality,
npc.motivation,
npc.combatStyle,
...(npc.relationshipHooks ?? []),
...(npc.tags ?? []),
],
});
}
export function buildCustomWorldStoryNpcAttributeProfile(
npc: CustomWorldNpc,
schema: WorldAttributeSchema,
) {
return buildRoleAttributeProfileFromTexts({
schema,
textBlocks: [
npc.title,
npc.role,
npc.description,
npc.backstory,
npc.personality,
npc.motivation,
npc.combatStyle,
...(npc.relationshipHooks ?? []),
...(npc.tags ?? []),
],
});
}

Some files were not shown because too many files have changed in this diff Show More