diff --git a/AGENTS.md b/AGENTS.md index cf5aef51..994679a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,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/ diff --git a/README.md b/README.md index 98cd591f..c9b219e4 100644 --- a/README.md +++ b/README.md @@ -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) 核心数据: diff --git a/docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md b/docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md index d6aa8108..303fdc11 100644 --- a/docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md +++ b/docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md @@ -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:等主链收口后再清桥接字段 diff --git a/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md new file mode 100644 index 00000000..26b2b81a --- /dev/null +++ b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md @@ -0,0 +1,141 @@ +# 工程死分支清理执行记录 A(2026-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` + +一句话总结本批次: + +**先把最确定的死分支和占位壳子清掉,让主工程少一些假入口、假主源、假能力,再进入更重的主链收口。** diff --git a/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md new file mode 100644 index 00000000..f4bb8093 --- /dev/null +++ b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md @@ -0,0 +1,145 @@ +# 工程死分支清理执行记录 B(2026-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 路牌从主工程里真正拔掉,让现行架构不再和历史壳子并排站着。** diff --git a/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md new file mode 100644 index 00000000..9e9f7a80 --- /dev/null +++ b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md @@ -0,0 +1,177 @@ +# 工程死分支清理执行记录 C(2026-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 门槛 + +这属于仓库当前既有工程问题,不是本批次引入的新断裂。 + +--- + +## 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 约束,不能在服务端未先补齐前硬砍。** diff --git a/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md new file mode 100644 index 00000000..89315af5 --- /dev/null +++ b/docs/audits/engineering/ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md @@ -0,0 +1,56 @@ +# 工程死分支清理执行记录 D(2026-04-21) + +更新时间:`2026-04-21` + +## 0. 本批次目标 + +本批次继续清理上一轮复核后剩余的低风险数据产物与测试占位: + +1. 未接入业务的生成产物 +2. 只为测试替换真实实现的空 stub +3. 支撑这些残留的配置与脚本 + +--- + +## 1. 已删除对象 + +| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 | +| --- | --- | --- | --- | +| `src/data/buildTagSimilarity.generated.ts` | 未接入业务的生成产物 | 运行时代码不 import;Build 相似度当前由 `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. 大型主流程组件继续拆分,而不是直接删除 diff --git a/docs/audits/engineering/FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md b/docs/audits/engineering/FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md new file mode 100644 index 00000000..66b66ef5 --- /dev/null +++ b/docs/audits/engineering/FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md @@ -0,0 +1,503 @@ +# 前端应迁后端逻辑审计(2026-04-21) + +更新时间:`2026-04-21` + +## 0. 审计目标 + +这份文档只回答一个问题: + +**当前前端代码里,哪些逻辑已经明显越过“前端只做表现,Express 后端负责逻辑、数据与存储”的边界,应该继续迁到后端。** + +本轮不改业务代码,只做: + +1. 基于当前仓库状态给出高置信度候选点 +2. 标明代码证据 +3. 给出迁移优先级 +4. 说明迁移后前端应该保留什么、移走什么 + +--- + +## 1. 结论先行 + +结合当前代码与已有边界文档,前端里仍有 6 类逻辑应该继续后移: + +1. **运行时快照前置写入与本地镜像解释** +2. **鉴权 token 的浏览器本地真相** +3. **平台浏览历史的本地真相与迁移状态** +4. **NPC 待接委托“换单”仍由前端直接触发正式生成** +5. **quest/runtime item 的双环境混合编排** +6. **浏览器侧大型 AI orchestration 与 prompt/repair/fallback 主链** + +一句话判断: + +**当前前端已经不是最早那种“大量主算”的状态,但仍然保留了运行时镜像、生成编排和部分正式真相。后端边界还需要再收一轮,前端才算真正退回表现层。** + +--- + +## 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.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` + +--- + +## 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。** diff --git a/docs/audits/engineering/README.md b/docs/audits/engineering/README.md index e3ae7bcf..94efb4c8 100644 --- a/docs/audits/engineering/README.md +++ b/docs/audits/engineering/README.md @@ -4,21 +4,36 @@ ## 当前推荐入口 -1. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md) +1. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md) + 这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 Express 后端的运行时、鉴权、生成编排与本地真相残留。 +2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md) + 这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。 +3. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md) + 这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。 +4. [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 的正式出清。 +5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md) + 这一版是第一批落地记录,聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。 +6. [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) +7. [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) +8. [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) +9. [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) +10. [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) +11. [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 与对应脚本/配置残留,主工程里的“假数据主源”进一步减少。 - 当前仓库已经完成“旧 dev 插件链路删除、根目录噪音清理、`server-node -> src/**` 反向依赖切断”这批第一阶段任务。 - 当前如果想直接判断“今天先优化什么”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`。 - 当前的新重点已经进一步收敛到三类:未接线孤岛模块、前端残留的运行时/鉴权真相、热点向 prompt/runtime profile/平台入口壳层迁移。 diff --git a/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md b/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md index 99166c90..5a15a1ab 100644 --- a/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md +++ b/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md @@ -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 联动 - 保留视觉覆盖保存与全局布局保存能力 diff --git a/docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md b/docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md index 68fe5eff..16424b14 100644 --- a/docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md +++ b/docs/planning/CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md @@ -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. 当前版本规划只保留仍对正式主链有现实约束的事项 + 这一阶段的目标是: **让接下来所有开发都围绕同一套现实目标执行。** diff --git a/docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md b/docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md index 881927d7..e6c36e6b 100644 --- a/docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md +++ b/docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md @@ -164,7 +164,7 @@ Git 分支治理可以后置做,但不能和首轮工程清洗混在一起, 11. `src/services/typewriter.ts` 12. `src/prompts/customWorldOrchestratorPrompts.ts` 13. `src/prompts/storyOrchestratorPrompts.ts` -14. `src/data/buildTagSimilarity.generated.ts` +14. `src/data/buildTagSimilarity.generated.ts`(已在后续清理批次中删除) 这些文件不能直接判死刑,但必须进入“保留 / 接回 / 归档 / 删除”四选一清单。 diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE1_IMPLEMENTATION_PLAN_2026-04-13.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE1_IMPLEMENTATION_PLAN_2026-04-13.md index 7f73a297..52c13fd7 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE1_IMPLEMENTATION_PLAN_2026-04-13.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE1_IMPLEMENTATION_PLAN_2026-04-13.md @@ -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 必须新增: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE2_IMPLEMENTATION_PLAN_2026-04-13.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE2_IMPLEMENTATION_PLAN_2026-04-13.md index b1ae0d71..9f3ae993 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE2_IMPLEMENTATION_PLAN_2026-04-13.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE2_IMPLEMENTATION_PLAN_2026-04-13.md @@ -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 必须修改: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE3_IMPLEMENTATION_PLAN_2026-04-14.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE3_IMPLEMENTATION_PLAN_2026-04-14.md index 6aef83af..a4f1ddac 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE3_IMPLEMENTATION_PLAN_2026-04-14.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE3_IMPLEMENTATION_PLAN_2026-04-14.md @@ -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 必须修改: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE4_IMPLEMENTATION_PLAN_2026-04-14.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE4_IMPLEMENTATION_PLAN_2026-04-14.md index cca30769..a936102d 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE4_IMPLEMENTATION_PLAN_2026-04-14.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE4_IMPLEMENTATION_PLAN_2026-04-14.md @@ -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 必须修改: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE5_IMPLEMENTATION_PLAN_2026-04-14.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE5_IMPLEMENTATION_PLAN_2026-04-14.md index 6e9c4215..0e0cd627 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE5_IMPLEMENTATION_PLAN_2026-04-14.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE5_IMPLEMENTATION_PLAN_2026-04-14.md @@ -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` 时,新增: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE6_IMPLEMENTATION_PLAN_2026-04-14.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE6_IMPLEMENTATION_PLAN_2026-04-14.md index dd64840a..bb642bd1 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE6_IMPLEMENTATION_PLAN_2026-04-14.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PHASE6_IMPLEMENTATION_PLAN_2026-04-14.md @@ -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` 中,当当前卡类型为: diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md index c4cf9d55..4f6a457a 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md @@ -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 用于把以下几份分析文档收束成一份可直接指导编码落地的新创作工具产品需求文档: diff --git a/docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md b/docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md index 69dd8516..79f35c3c 100644 --- a/docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md +++ b/docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md @@ -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. 一句话结论 diff --git a/docs/reference/BUSINESS_PROMPT_INVENTORY_2026-04-19.md b/docs/reference/BUSINESS_PROMPT_INVENTORY_2026-04-19.md index eac71f06..35d88d63 100644 --- a/docs/reference/BUSINESS_PROMPT_INVENTORY_2026-04-19.md +++ b/docs/reference/BUSINESS_PROMPT_INVENTORY_2026-04-19.md @@ -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 目录中的相关文件 diff --git a/docs/technical/CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md b/docs/technical/CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md new file mode 100644 index 00000000..c20759b5 --- /dev/null +++ b/docs/technical/CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md @@ -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. 桌面端仍可通过左侧平台导航进入创作页。 diff --git a/docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md b/docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md index 4c4ae9e7..20c8aa9f 100644 --- a/docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md +++ b/docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md @@ -6,12 +6,13 @@ 当前这条 Agent 创作流已经完成阶段一到阶段三的主要收口。 -阶段四中的“文档清理”已经开始做,但还没有形成独立、完整的新主链。 +阶段四中的“文档清理”已经开始做,但还没有形成独立、完整的新主链审计闭环。 -因此这轮可以执行的清理只有一类: +因此这轮可以执行的清理现在有两类: 1. 删除已经不再从当前主入口可达的旧 `custom-world/sessions` 世界生成链 -2. 保留仍在服务 `Agent session` 主链或已保存作品兼容编辑体验的底层能力 +2. 删除已经完全脱离 `CustomWorldAgentWorkspace` 主链、只剩孤立互相引用与自测覆盖的 `custom-world-agent` 旧面板 +3. 保留仍在服务 `Agent session` 主链或已保存作品兼容编辑体验的底层能力 这轮不做: @@ -60,7 +61,8 @@ 原因: 1. 文档清理已经开始,但还没有完整收束到单一结论文档 -2. 旧 `custom-world/sessions` 生成链虽然已经不在主入口上,但还未清干净 +2. 旧 `custom-world/sessions` 生成链已经完成物理清理,但与之相关的审计/PRD/知识图谱文档仍需继续统一口径 +3. `custom-world-agent` 孤岛面板已经完成第二轮物理清理,但阶段四文档总收口仍未完全覆盖所有历史 PRD 口径 --- @@ -72,12 +74,29 @@ 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/services/customWorldSessionStore.ts` -2. `server-node/src/repositories/runtimeRepository.ts` 中被 Agent session 复用的 session 持久化能力 -3. `src/services/aiService.ts` 里仍在使用的 `generateCustomWorldProfile` 及其现代封装 +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` --- @@ -87,6 +106,8 @@ 1. `src/services/aiService.ts` 不再暴露旧 `custom-world/sessions` 请求函数 2. `server-node/src/routes/runtimeRoutes.ts` 不再挂旧 session 路由 -3. 仓库里不再有主流程可达的旧世界生成入口 -4. Agent 主链与已保存作品编辑链仍然可用 - +3. `server-node/src/services/customWorldSessionStore.ts` 与 `server-node/src/services/customWorldGenerationService.ts` 已物理删除 +4. 仓库里不再有主流程可达的旧世界生成入口 +5. `CustomWorldAgentWorkspace.tsx` 只保留当前正式主链需要的 5 个子模块 +6. 与旧 Agent 草稿面板相关的孤岛 UI 与自测不再继续占据正式目录注意力 +7. Agent 主链与已保存作品编辑链仍然可用 diff --git a/docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md b/docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md index cd2ba7e6..1768e251 100644 --- a/docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md +++ b/docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md @@ -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 精灵主图、精灵表、修帧与资产保存 --- diff --git a/docs/technical/EDITOR_ENTRY_CLEANUP_2026-04-21.md b/docs/technical/EDITOR_ENTRY_CLEANUP_2026-04-21.md new file mode 100644 index 00000000..1b2b0f8d --- /dev/null +++ b/docs/technical/EDITOR_ENTRY_CLEANUP_2026-04-21.md @@ -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. 若只剩文档、测试、兼容判断或独立路由壳,直接成批收口 diff --git a/docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md b/docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md index 7616c90e..20e1ddee 100644 --- a/docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md +++ b/docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md @@ -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 角色 diff --git a/docs/technical/PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md b/docs/technical/PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md index 1efe4240..beac32a6 100644 --- a/docs/technical/PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md +++ b/docs/technical/PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md @@ -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` diff --git a/docs/technical/QWEN_IMAGE_2_PIXELMOTION_REPRODUCTION_WORKFLOW_2026-04-07.md b/docs/technical/QWEN_IMAGE_2_PIXELMOTION_REPRODUCTION_WORKFLOW_2026-04-07.md index 3c124cec..83bd2db4 100644 --- a/docs/technical/QWEN_IMAGE_2_PIXELMOTION_REPRODUCTION_WORKFLOW_2026-04-07.md +++ b/docs/technical/QWEN_IMAGE_2_PIXELMOTION_REPRODUCTION_WORKFLOW_2026-04-07.md @@ -676,7 +676,7 @@ PixelMotion 很关键的一点,不是要求 16 帧都完美,而是允许修 ### 13.4 推荐目录结构 ```text -pixelmotion-qwen/ +pixelmotion-workflow/ refs/ master.png pose_board_run.png diff --git a/docs/technical/README.md b/docs/technical/README.md index 42272303..e98fb37a 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -13,6 +13,7 @@ - [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 结果页冻结为预览收口层。 - [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_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_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md](./NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md):当前 Node 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。 - [EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md](./EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md):Express 后端当前 contract 冻结版本、热点文件编辑规则与集成窗口清单。 diff --git a/package.json b/package.json index 42655d8c..2e82716e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,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", diff --git a/scripts/generate-build-tag-similarity.py b/scripts/generate-build-tag-similarity.py deleted file mode 100644 index e9071fee..00000000 --- a/scripts/generate-build-tag-similarity.py +++ /dev/null @@ -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 = [" - ] - 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() diff --git a/server-node/src/app.ts b/server-node/src/app.ts index e716912c..2227b813 100644 --- a/server-node/src/app.ts +++ b/server-node/src/app.ts @@ -8,7 +8,6 @@ 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'; @@ -119,18 +118,6 @@ export function createApp(context: AppContext) { createCharacterAssetRoutes(context.config, context.llmClient), ), ); - app.use( - scopeToPrefixes( - ['/api/assets/qwen-sprite'], - withRouteMeta({ routeVersion: '2026-04-08', operation: 'assets.qwen' }), - ), - ); - app.use( - scopeToPrefixes( - ['/api/assets/qwen-sprite'], - createQwenSpriteRoutes(context.config), - ), - ); app.use( '/api/auth', withRouteMeta({ routeVersion: '2026-04-08' }), diff --git a/server-node/src/context.ts b/server-node/src/context.ts index 8f195f8e..e978ac56 100644 --- a/server-node/src/context.ts +++ b/server-node/src/context.ts @@ -12,7 +12,6 @@ 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 { SmsVerificationService } from './services/smsVerificationService.js'; import type { WechatAuthService } from './services/wechatAuthService.js'; @@ -30,7 +29,6 @@ export type AppContext = { userSessionRepository: UserSessionRepository; runtimeRepository: RuntimeRepository; llmClient: UpstreamLlmClient; - customWorldSessions: CustomWorldSessionStore; customWorldAgentSessions: CustomWorldAgentSessionStore; customWorldAgentOrchestrator: CustomWorldAgentOrchestrator; smsVerificationService: SmsVerificationService; diff --git a/server-node/src/modules/assets/qwenSpriteRoutes.ts b/server-node/src/modules/assets/qwenSpriteRoutes.ts deleted file mode 100644 index 4053e78d..00000000 --- a/server-node/src/modules/assets/qwenSpriteRoutes.ts +++ /dev/null @@ -1,907 +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'; - -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); - } - - return new Promise>((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 { - 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; - bodyText?: string; - } = {}, -) { - return new Promise<{ - statusCode: number; - headers: Record; - 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; - } = {}, -) { - return new Promise<{ - statusCode: number; - headers: Record; - 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, -) { - 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) { - 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 = { - 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; - 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; - 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; - 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; - 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; - 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, -) { - 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.use( - QWEN_SPRITE_MASTER_GENERATE_PATH, - toExpressHandler((request, response) => - handleGenerateMaster(config, request, response), - ), - ); - router.use( - QWEN_SPRITE_SHEET_GENERATE_PATH, - toExpressHandler((request, response) => - handleGenerateSheet(config, request, response), - ), - ); - router.use( - QWEN_SPRITE_FRAME_REPAIR_PATH, - toExpressHandler((request, response) => - handleRepairFrame(config, request, response), - ), - ); - router.use( - QWEN_SPRITE_SAVE_PATH, - toExpressHandler((request, response) => - handleSaveAsset(config.projectRoot, request, response), - ), - ); - - return router; -} diff --git a/server-node/src/routes/runtimeRoutes.ts b/server-node/src/routes/runtimeRoutes.ts index 75b8dc27..b67a2538 100644 --- a/server-node/src/routes/runtimeRoutes.ts +++ b/server-node/src/routes/runtimeRoutes.ts @@ -3,8 +3,6 @@ import { z } from 'zod'; import type { ListCustomWorldWorksResponse } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import type { - AnswerCustomWorldSessionQuestionRequest, - CreateCustomWorldSessionRequest, CustomWorldGalleryDetailResponse, CustomWorldGalleryResponse, CustomWorldLibraryMutationResponse, @@ -21,7 +19,6 @@ import type { SavedGameSnapshotInput, } from '../../../packages/shared/src/contracts/runtime.js'; import { - CUSTOM_WORLD_GENERATION_MODES, PLATFORM_THEMES, } from '../../../packages/shared/src/contracts/runtime.js'; import type { @@ -41,7 +38,6 @@ import { badRequest, notFound } from '../errors.js'; import { asyncHandler, jsonClone, - prepareEventStreamResponse, sendApiResponse, } from '../http.js'; import { requireJwtAuth } from '../middleware/auth.js'; @@ -67,7 +63,6 @@ import { npcRecruitDialogueRequestSchema, } from '../services/chatService.js'; import { generateCustomWorldEntity } from '../services/customWorldEntityGenerationService.js'; -import { generateCustomWorldProfile } from '../services/customWorldGenerationService.js'; import { generateSceneNpcForLandmark } from '../services/customWorldSceneNpcGenerationService.js'; import { listCustomWorldWorkSummaries } from '../services/customWorldWorkSummaryService.js'; import { generateQuestForNpcEncounter } from '../services/questService.js'; @@ -133,17 +128,6 @@ const customWorldEntitySchema = z.object({ kind: z.enum(['playable', 'story', 'landmark']), }); -const customWorldSessionSchema = z.object({ - settingText: z.string().trim().min(1), - creatorIntent: jsonObjectSchema.nullable().optional().default(null), - generationMode: z.enum(CUSTOM_WORLD_GENERATION_MODES).default('fast'), -}); - -const customWorldAnswerSchema = z.object({ - questionId: z.string().trim().min(1), - answer: z.string().trim().min(1), -}); - const runtimeItemIntentSchema = z.object({ context: jsonObjectSchema, plans: z.array(jsonObjectSchema), @@ -792,128 +776,6 @@ export function createRuntimeRoutes(context: AppContext) { }), ); - router.post( - '/runtime/custom-world/sessions', - routeMeta({ operation: 'runtime.customWorldSession.create' }), - asyncHandler(async (request, response) => { - const payload = customWorldSessionSchema.parse( - request.body, - ) as CreateCustomWorldSessionRequest; - sendApiResponse( - response, - await context.customWorldSessions.create( - request.userId!, - payload.settingText, - payload.creatorIntent, - payload.generationMode, - ), - ); - }), - ); - - router.get( - '/runtime/custom-world/sessions/:sessionId', - routeMeta({ operation: 'runtime.customWorldSession.get' }), - asyncHandler(async (request, response) => { - const session = await context.customWorldSessions.get( - request.userId!, - readParam(request.params.sessionId), - ); - if (!session) { - throw notFound('custom world session not found'); - } - sendApiResponse(response, session); - }), - ); - - router.post( - '/runtime/custom-world/sessions/:sessionId/answers', - routeMeta({ operation: 'runtime.customWorldSession.answer' }), - asyncHandler(async (request, response) => { - const payload = customWorldAnswerSchema.parse( - request.body, - ) as AnswerCustomWorldSessionQuestionRequest; - const session = await context.customWorldSessions.answer( - request.userId!, - readParam(request.params.sessionId), - payload.questionId, - payload.answer, - ); - if (!session) { - throw notFound('custom world session not found'); - } - sendApiResponse(response, session); - }), - ); - - router.get( - '/runtime/custom-world/sessions/:sessionId/generate/stream', - routeMeta({ operation: 'runtime.customWorldSession.generateStream' }), - asyncHandler(async (request, response) => { - const session = await context.customWorldSessions.get( - request.userId!, - readParam(request.params.sessionId), - ); - if (!session) { - throw notFound('custom world session not found'); - } - - prepareEventStreamResponse(request, response); - const controller = new AbortController(); - - request.on('close', () => { - controller.abort(); - }); - - const writeEvent = (event: string, payload: Record) => { - response.write(`event: ${event}\n`); - response.write(`data: ${JSON.stringify(payload)}\n\n`); - }; - - writeEvent('progress', { phase: 'preparing', progress: 10 }); - await context.customWorldSessions.updateStatus( - request.userId!, - readParam(request.params.sessionId), - 'generating', - ); - writeEvent('progress', { phase: 'requesting_llm', progress: 45 }); - - try { - const profile = await generateCustomWorldProfile(context, session, { - signal: controller.signal, - onProgress: (progress) => { - writeEvent( - 'progress', - progress as unknown as Record, - ); - }, - }); - await context.customWorldSessions.setResult( - request.userId!, - readParam(request.params.sessionId), - profile, - ); - writeEvent('progress', { phase: 'completed', progress: 100 }); - writeEvent('result', { profile }); - writeEvent('done', { ok: true }); - } catch (error) { - const message = - error instanceof Error - ? error.message - : 'custom world generation failed'; - await context.customWorldSessions.updateStatus( - request.userId!, - readParam(request.params.sessionId), - 'generation_error', - message, - ); - writeEvent('error', { message }); - } finally { - response.end(); - } - }), - ); - router.post( '/runtime/items/runtime-intent', routeMeta({ operation: 'runtime.items.intent' }), diff --git a/server-node/src/server.ts b/server-node/src/server.ts index 3c303014..813da85a 100644 --- a/server-node/src/server.ts +++ b/server-node/src/server.ts @@ -16,7 +16,6 @@ import { CaptchaChallengeStore } from './services/captchaChallengeStore.js'; import { CustomWorldAgentAutoAssetService } from './services/customWorldAgentAutoAssetService.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 { createSmsVerificationService } from './services/smsVerificationService.js'; import { createWechatAuthService } from './services/wechatAuthService.js'; @@ -113,7 +112,6 @@ export async function createAppContext(config: AppConfig = loadConfig()) { userSessionRepository: new UserSessionRepository(db), runtimeRepository, llmClient: new UpstreamLlmClient(config, logger), - customWorldSessions: new CustomWorldSessionStore(runtimeRepository), customWorldAgentSessions, customWorldAgentOrchestrator: new CustomWorldAgentOrchestrator( customWorldAgentSessions, diff --git a/server-node/src/services/customWorldGenerationService.ts b/server-node/src/services/customWorldGenerationService.ts deleted file mode 100644 index 0a657318..00000000 --- a/server-node/src/services/customWorldGenerationService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AppContext } from '../context.js'; -import { - type CustomWorldGenerationProgress, - generateCustomWorldProfileFromOrchestrator, - type GenerateCustomWorldProfileInput, -} from '../modules/ai/customWorldOrchestrator.js'; -import type { CustomWorldSession } from './customWorldSessionStore.js'; - -export async function generateCustomWorldProfile( - context: AppContext, - session: CustomWorldSession, - options: { - onProgress?: (progress: CustomWorldGenerationProgress) => void; - signal?: AbortSignal; - } = {}, -) { - const input = { - settingText: session.settingText, - creatorIntent: session.creatorIntent, - generationMode: session.generationMode, - } satisfies GenerateCustomWorldProfileInput; - - const profile = await generateCustomWorldProfileFromOrchestrator( - context.llmClient, - input, - { - onProgress: options.onProgress, - signal: options.signal, - }, - ); - - return JSON.parse(JSON.stringify(profile)) as Record; -} diff --git a/server-node/src/services/customWorldSessionStore.ts b/server-node/src/services/customWorldSessionStore.ts deleted file mode 100644 index 6fb99c6a..00000000 --- a/server-node/src/services/customWorldSessionStore.ts +++ /dev/null @@ -1,229 +0,0 @@ -import crypto from 'node:crypto'; - -import type { JsonObject } from '../../../packages/shared/src/contracts/common.js'; -import type { - CustomWorldGenerationMode, - CustomWorldQuestion, - CustomWorldSessionRecord, - CustomWorldSessionStatus, -} from '../../../packages/shared/src/contracts/runtime.js'; -import type { RuntimeRepositoryPort } from '../repositories/runtimeRepository.js'; - -export type CustomWorldSession = { - sessionId: string; - status: CustomWorldSessionStatus; - settingText: string; - creatorIntent: JsonObject | null; - generationMode: CustomWorldGenerationMode; - questions: CustomWorldQuestion[]; - result?: JsonObject; - lastError?: string; - createdAt: string; - updatedAt: string; -}; - -function cloneSession(session: CustomWorldSession) { - return JSON.parse(JSON.stringify(session)) as CustomWorldSession; -} - -function toSessionRecord(session: CustomWorldSession): CustomWorldSessionRecord { - return { - sessionId: session.sessionId, - status: session.status, - settingText: session.settingText, - creatorIntent: session.creatorIntent, - generationMode: session.generationMode, - questions: session.questions, - result: session.result, - lastError: session.lastError, - createdAt: session.createdAt, - updatedAt: session.updatedAt, - }; -} - -function toSession(record: CustomWorldSessionRecord) { - return cloneSession({ - sessionId: record.sessionId, - status: record.status, - settingText: record.settingText, - creatorIntent: record.creatorIntent ?? null, - generationMode: record.generationMode, - questions: record.questions, - result: record.result, - lastError: record.lastError, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - }); -} - -function hasPendingQuestion(questions: CustomWorldQuestion[]) { - return questions.some((question) => !question.answer?.trim()); -} - -function buildClarificationQuestions( - settingText: string, - creatorIntent: JsonObject | null, -) { - const questions: CustomWorldQuestion[] = []; - const worldHook = - typeof creatorIntent?.worldHook === 'string' ? creatorIntent.worldHook.trim() : ''; - const playerPremise = - typeof creatorIntent?.playerPremise === 'string' ? creatorIntent.playerPremise.trim() : ''; - const openingSituation = - typeof creatorIntent?.openingSituation === 'string' - ? creatorIntent.openingSituation.trim() - : ''; - const coreConflicts = Array.isArray(creatorIntent?.coreConflicts) - ? creatorIntent.coreConflicts - : []; - - if (!worldHook && settingText.trim().length < 24) { - questions.push({ - id: 'world_hook', - label: '世界核心', - question: '请用一句话补充这个世界最核心的命题或独特卖点。', - }); - } - if (!playerPremise) { - questions.push({ - id: 'player_premise', - label: '玩家身份', - question: '玩家在这个世界里是什么身份、立场或来历?', - }); - } - if (!openingSituation) { - questions.push({ - id: 'opening_situation', - label: '开局处境', - question: '故事开局时,玩家正处于什么局面?', - }); - } - if (coreConflicts.length === 0) { - questions.push({ - id: 'core_conflict', - label: '核心冲突', - question: '这个世界当前最核心的冲突、危机或悬念是什么?', - }); - } - - return questions; -} - -export class CustomWorldSessionStore { - constructor( - private readonly runtimeRepository: RuntimeRepositoryPort, - ) {} - - async create( - userId: string, - settingText: string, - creatorIntent: JsonObject | null, - generationMode: CustomWorldGenerationMode, - ) { - const sessionId = `custom-world-session-${crypto.randomBytes(16).toString('hex')}`; - const now = new Date().toISOString(); - const session: CustomWorldSession = { - sessionId, - status: 'ready_to_generate', - settingText, - creatorIntent, - generationMode, - questions: buildClarificationQuestions(settingText, creatorIntent), - createdAt: now, - updatedAt: now, - }; - - if (hasPendingQuestion(session.questions)) { - session.status = 'clarifying'; - } - - await this.runtimeRepository.upsertCustomWorldSession( - userId, - sessionId, - toSessionRecord(session), - ); - return cloneSession(session); - } - - async list(userId: string) { - const sessions = await this.runtimeRepository.listCustomWorldSessions(userId); - return sessions.map((session) => toSession(session)); - } - - async get(userId: string, sessionId: string) { - const session = await this.runtimeRepository.getCustomWorldSession( - userId, - sessionId, - ); - return session ? toSession(session) : null; - } - - async answer( - userId: string, - sessionId: string, - questionId: string, - answer: string, - ) { - const session = await this.get(userId, sessionId); - if (!session) { - return null; - } - - const question = session.questions.find((item) => item.id === questionId); - if (!question) { - return null; - } - - question.answer = answer; - session.status = hasPendingQuestion(session.questions) - ? 'clarifying' - : 'ready_to_generate'; - session.updatedAt = new Date().toISOString(); - await this.runtimeRepository.upsertCustomWorldSession( - userId, - sessionId, - toSessionRecord(session), - ); - return cloneSession(session); - } - - async updateStatus( - userId: string, - sessionId: string, - status: CustomWorldSessionStatus, - lastError = '', - ) { - const session = await this.get(userId, sessionId); - if (!session) { - return null; - } - - session.status = status; - session.lastError = lastError || undefined; - session.updatedAt = new Date().toISOString(); - await this.runtimeRepository.upsertCustomWorldSession( - userId, - sessionId, - toSessionRecord(session), - ); - return cloneSession(session); - } - - async setResult(userId: string, sessionId: string, result: JsonObject) { - const session = await this.get(userId, sessionId); - if (!session) { - return null; - } - - session.status = 'completed'; - session.lastError = undefined; - session.result = JSON.parse(JSON.stringify(result)) as JsonObject; - session.updatedAt = new Date().toISOString(); - await this.runtimeRepository.upsertCustomWorldSession( - userId, - sessionId, - toSessionRecord(session), - ); - return cloneSession(session); - } -} diff --git a/src/components/GameShell.tsx b/src/components/GameShell.tsx deleted file mode 100644 index 8837b85f..00000000 --- a/src/components/GameShell.tsx +++ /dev/null @@ -1,807 +0,0 @@ -import {AnimatePresence, motion} from 'motion/react'; -import {lazy, Suspense, useCallback, useEffect, useMemo, useState} from 'react'; - -import {getLiveGamePlayTimeMs} from '../data/runtimeStats'; -import type { HydratedSavedGameSnapshot } from '../persistence/runtimeSnapshotTypes'; -import {getWorldCampScenePreset} from '../data/scenePresets'; -import {BottomTab} from '../hooks/useGameFlow'; -import {resolveActiveSceneActBlueprint} from '../services/customWorldSceneActRuntime'; -import { - type BattleRewardUi, - type CharacterChatUi, - type GoalFlowUi, - type InventoryFlowUi, - type NpcChatQuestOfferUi, - type QuestFlowUi, - type StoryGenerationNpcUi, -} from '../hooks/useStoryGeneration'; -import { - type Character, - type CustomWorldProfile, - type CompanionRenderState, - type GameState, - type StoryMoment, - type StoryOption, -} from '../types'; -import {CHROME_ICONS, getNineSliceStyle, TAB_ICONS, UI_CHROME} from '../uiAssets'; -import {CharacterSelectionFlow} from './game-shell/CharacterSelectionFlow'; -import {PreGameSelectionFlow} from './game-shell/PreGameSelectionFlow'; -import {SCENE_TRANSITION_FUNCTION_MODES, useSceneTransitionModel} from './game-shell/useSceneTransitionModel'; -import {useGameShellViewModel} from './game-shell/useGameShellViewModel'; -import {GameCanvas} from './GameCanvas'; -import {PixelIcon} from './PixelIcon'; - -interface GameShellSessionProps { - gameState: GameState; - currentStory: StoryMoment | null; - isLoading: boolean; - aiError: string | null; - bottomTab: BottomTab; - setBottomTab: (tab: BottomTab) => void; - isMapOpen: boolean; - setIsMapOpen: (open: boolean) => void; -} - -interface GameShellStoryProps { - displayedOptions: StoryOption[]; - canRefreshOptions: boolean; - handleRefreshOptions: () => void; - handleChoice: (option: StoryOption) => void; - handleNpcChatInput: (input: string) => boolean; - exitNpcChat: () => boolean; - handleMapTravelToScene: (sceneId: string) => boolean; - npcUi: StoryGenerationNpcUi; - characterChatUi: CharacterChatUi; - inventoryUi: InventoryFlowUi; - battleRewardUi: BattleRewardUi; - questUi: QuestFlowUi; - npcChatQuestOfferUi: NpcChatQuestOfferUi; - goalUi: GoalFlowUi; -} - -interface GameShellEntryProps { - hasSavedGame: boolean; - savedSnapshot: HydratedSavedGameSnapshot | null; - handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void; - handleStartNewGame: () => void; - handleSaveAndExit: () => void; - handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void; - handleBackToWorldSelect: () => void; - handleCharacterSelect: (character: Character) => void; -} - -interface GameShellCompanionProps { - companionRenderStates: CompanionRenderState[]; - buildCompanionRenderStates: (state: GameState) => CompanionRenderState[]; - onBenchCompanion: (npcId: string) => void; - onActivateRosterCompanion: (npcId: string, swapNpcId?: string | null) => void; -} - -interface GameShellAudioProps { - musicVolume: number; - onMusicVolumeChange: (value: number) => void; -} - -interface GameShellProps { - session: GameShellSessionProps; - story: GameShellStoryProps; - entry: GameShellEntryProps; - companions: GameShellCompanionProps; - audio: GameShellAudioProps; -} - -const AdventureEntityModal = lazy(async () => { - const module = await import('./AdventureEntityModal'); - - return { - default: module.AdventureEntityModal, - }; -}); - -const CharacterChatModal = lazy(async () => { - const module = await import('./CharacterChatModal'); - - return { - default: module.CharacterChatModal, - }; -}); - -const CompanionCampModal = lazy(async () => { - const module = await import('./CompanionCampModal'); - - return { - default: module.CompanionCampModal, - }; -}); - -const MapModal = lazy(async () => { - const module = await import('./MapModal'); - - return { - default: module.MapModal, - }; -}); - -const NpcModals = lazy(async () => { - const module = await import('./NpcModals'); - - return { - default: module.NpcModals, - }; -}); - -const AdventurePanel = lazy(async () => { - const module = await import('./AdventurePanel'); - - return { - default: module.AdventurePanel, - }; -}); - -const CharacterPanel = lazy(async () => { - const module = await import('./CharacterPanel'); - - return { - default: module.CharacterPanel, - }; -}); - -const InventoryPanel = lazy(async () => { - const module = await import('./InventoryPanel'); - - return { - default: module.InventoryPanel, - }; -}); - -function ModalLoadingFallback({ - label, - onClose, -}: { - label: string; - onClose?: (() => void) | null; -}) { - return ( -
-
event.stopPropagation()} - > - {label} -
-
- ); -} - -function PanelLoadingFallback({ - label, -}: { - label: string; -}) { - return ( -
- {label} -
- ); -} - -export function GameShell({session, story, entry, companions, audio}: GameShellProps) { - const { - gameState, - currentStory, - isLoading, - aiError, - bottomTab, - setBottomTab, - isMapOpen, - setIsMapOpen, - } = session; - const { - displayedOptions, - canRefreshOptions, - handleRefreshOptions, - handleChoice, - handleNpcChatInput, - exitNpcChat, - handleMapTravelToScene, - npcUi, - characterChatUi, - inventoryUi, - battleRewardUi, - questUi, - npcChatQuestOfferUi, - goalUi, - } = story; - const { - hasSavedGame, - savedSnapshot, - handleContinueGame, - handleStartNewGame, - handleSaveAndExit, - handleCustomWorldSelect, - handleBackToWorldSelect, - handleCharacterSelect, - } = entry; - const { - companionRenderStates, - buildCompanionRenderStates, - onBenchCompanion, - onActivateRosterCompanion, - } = companions; - const {musicVolume, onMusicVolumeChange} = audio; - - const [clockNow, setClockNow] = useState(() => Date.now()); - const openingCampSceneId = useMemo( - () => (gameState.worldType ? getWorldCampScenePreset(gameState.worldType)?.id ?? null : null), - [gameState.worldType], - ); - const hasNpcModalOpen = Boolean(npcUi.tradeModal || npcUi.giftModal || npcUi.recruitModal); - const { - selectionStage, - setSelectionStage, - overlayPanel, - openOverlayPanel, - closeOverlayPanel, - selectedSceneEntity, - setSelectedSceneEntity, - openPartyMemberDetails, - closeAdventureEntityModal, - showTeamModal, - openCampModal, - closeCampModal, - resetForSaveAndExit, - shouldMountAdventureEntityModal, - shouldMountCampModal, - shouldMountMapModal, - shouldMountCharacterChatModal, - shouldMountNpcModals, - } = useGameShellViewModel({ - gameState, - isMapOpen, - characterChatModalOpen: Boolean(characterChatUi.modal), - hasNpcModalOpen, - }); - const { - visibleGameState, - visibleCurrentStory, - sceneTransitionPhase, - sceneTransitionToken, - setSceneTransitionDurations, - beginSceneTransition, - } = useSceneTransitionModel({ - gameState, - currentStory, - openingCampSceneId, - }); - const isCharacterSelectionStage = - gameState.currentScene === 'Selection' && - Boolean(gameState.worldType) && - !gameState.playerCharacter; - const collapseTopStage = gameState.currentScene === 'Selection'; - const shouldHideStoryOptions = sceneTransitionPhase !== 'idle'; - const visibleStoryForRender = visibleCurrentStory; - - const dialogueIndicator = useMemo(() => { - if (!isLoading || visibleCurrentStory?.displayMode !== 'dialogue' || visibleGameState.currentEncounter?.kind !== 'npc') { - return null; - } - - const lastSpeaker = visibleCurrentStory.dialogue?.[visibleCurrentStory.dialogue.length - 1]?.speaker ?? null; - return { - showPlayer: true, - showEncounter: true, - activeSpeaker: lastSpeaker === 'player' ? 'player' : lastSpeaker ? 'npc' : null, - } as const; - }, [visibleCurrentStory?.dialogue, visibleCurrentStory?.displayMode, visibleGameState.currentEncounter?.kind, isLoading]); - - const characterChatSummaries = useMemo( - () => - Object.fromEntries( - Object.entries(gameState.characterChats ?? {}).map(([characterId, record]) => [characterId, record.summary]), - ), - [gameState.characterChats], - ); - - const visibleCompanionRenderStates = useMemo( - () => buildCompanionRenderStates(visibleGameState), - [buildCompanionRenderStates, visibleGameState], - ); - - const canvasCompanionRenderStates = useMemo(() => { - const activeEncounterNpcId = visibleGameState.currentEncounter?.kind === 'npc' - ? visibleGameState.currentEncounter.id ?? null - : null; - if (!activeEncounterNpcId) return visibleCompanionRenderStates; - return visibleCompanionRenderStates.filter(companion => companion.npcId !== activeEncounterNpcId); - }, [visibleCompanionRenderStates, visibleGameState.currentEncounter]); - - const livePlayTimeMs = useMemo( - () => getLiveGamePlayTimeMs(gameState.runtimeStats, clockNow), - [clockNow, gameState.runtimeStats], - ); - const activeSceneAct = useMemo( - () => resolveActiveSceneActBlueprint({ - profile: visibleGameState.customWorldProfile, - sceneId: visibleGameState.currentScenePreset?.id ?? null, - storyEngineMemory: visibleGameState.storyEngineMemory, - }), - [ - visibleGameState.currentScenePreset?.id, - visibleGameState.customWorldProfile, - visibleGameState.storyEngineMemory, - ], - ); - const activeSceneChapter = useMemo(() => { - if (!visibleGameState.customWorldProfile || !visibleGameState.currentScenePreset?.id) { - return null; - } - - return ( - visibleGameState.customWorldProfile.sceneChapterBlueprints?.find( - entry => entry.sceneId === visibleGameState.currentScenePreset?.id - || entry.linkedLandmarkIds.includes(visibleGameState.currentScenePreset?.id ?? ''), - ) ?? null - ); - }, [ - visibleGameState.currentScenePreset?.id, - visibleGameState.customWorldProfile, - ]); - - const adventureStatistics = useMemo( - () => ({ - playTimeMs: livePlayTimeMs, - hostileNpcsDefeated: gameState.runtimeStats.hostileNpcsDefeated, - questsAccepted: gameState.runtimeStats.questsAccepted, - questsCompleted: visibleGameState.quests.filter(quest => quest.status === 'completed' || quest.status === 'turned_in').length, - questsTurnedIn: visibleGameState.quests.filter(quest => quest.status === 'turned_in').length, - itemsUsed: gameState.runtimeStats.itemsUsed, - scenesTraveled: gameState.runtimeStats.scenesTraveled, - currentSceneName: visibleGameState.currentScenePreset?.name ?? 'Current Area', - playerCurrency: visibleGameState.playerCurrency, - inventoryItemCount: visibleGameState.playerInventory.reduce((sum, item) => sum + item.quantity, 0), - inventoryStackCount: visibleGameState.playerInventory.length, - activeCompanionCount: visibleGameState.companions.length, - rosterCompanionCount: visibleGameState.roster.length, - }), - [ - gameState.runtimeStats.itemsUsed, - gameState.runtimeStats.hostileNpcsDefeated, - gameState.runtimeStats.questsAccepted, - gameState.runtimeStats.scenesTraveled, - livePlayTimeMs, - visibleGameState.companions.length, - visibleGameState.currentScenePreset?.name, - visibleGameState.playerCurrency, - visibleGameState.playerInventory, - visibleGameState.quests, - visibleGameState.roster.length, - ], - ); - - useEffect(() => { - if (!gameState.playerCharacter || gameState.currentScene !== 'Story') { - return; - } - - setClockNow(Date.now()); - const intervalId = window.setInterval(() => setClockNow(Date.now()), 1000); - return () => window.clearInterval(intervalId); - }, [gameState.currentScene, gameState.playerCharacter]); - - const handleSceneTransitionChoice = useCallback((option: StoryOption) => { - const transitionMode = SCENE_TRANSITION_FUNCTION_MODES[option.functionId]; - if (transitionMode) { - beginSceneTransition(transitionMode); - } - handleChoice(option); - }, [beginSceneTransition, handleChoice]); - - return ( -
-
- {collapseTopStage ? null : ( - setIsMapOpen(true)} - sceneTransitionPhase={sceneTransitionPhase} - sceneTransitionToken={sceneTransitionToken} - onSceneTransitionDurationsChange={setSceneTransitionDurations} - /> - )} -
- -
- - {!gameState.worldType && ( - - )} - - {gameState.worldType && !gameState.playerCharacter && ( - - { - handleBackToWorldSelect(); - setSelectionStage('platform'); - }} - onConfirm={handleCharacterSelect} - /> - - )} - - {visibleGameState.playerCharacter && visibleStoryForRender && ( - -
- - - -
- - {bottomTab === 'character' && ( - }> - - - )} - - {bottomTab === 'adventure' && ( - }> - openOverlayPanel('character')} - onOpenInventory={() => openOverlayPanel('inventory')} - playerCharacter={visibleGameState.playerCharacter} - worldType={visibleGameState.worldType} - quests={visibleGameState.quests} - questUi={questUi} - npcChatQuestOfferUi={npcChatQuestOfferUi} - goalStack={goalUi.goalStack} - goalPulse={goalUi.pulse} - onDismissGoalPulse={goalUi.dismissPulse} - battleRewardUi={battleRewardUi} - playerHp={visibleGameState.playerHp} - playerMaxHp={visibleGameState.playerMaxHp} - playerMana={visibleGameState.playerMana} - playerMaxMana={visibleGameState.playerMaxMana} - playerSkillCooldowns={visibleGameState.playerSkillCooldowns} - inBattle={visibleGameState.inBattle} - currentNpcBattleMode={visibleGameState.currentNpcBattleMode} - chapterState={visibleGameState.chapterState ?? null} - journeyBeat={ - visibleGameState.storyEngineMemory?.currentJourneyBeat ?? null - } - currentSceneActTitle={activeSceneAct?.title ?? null} - currentSceneActIndex={ - activeSceneChapter && activeSceneAct - ? (() => { - const actIndex = activeSceneChapter.acts.findIndex( - act => act.id === activeSceneAct.id, - ); - return actIndex >= 0 ? actIndex + 1 : null; - })() - : null - } - currentSceneActCount={activeSceneChapter?.acts.length ?? null} - statistics={adventureStatistics} - musicVolume={musicVolume} - onMusicVolumeChange={onMusicVolumeChange} - onSaveAndExit={() => { - resetForSaveAndExit(); - handleSaveAndExit(); - }} - /> - - )} - - {bottomTab === 'inventory' && ( - }> - - - )} -
- )} -
-
- - {shouldMountAdventureEntityModal && ( - }> - { - closeAdventureEntityModal(); - characterChatUi.openChat(target); - }} - /> - - )} - - - {overlayPanel && gameState.playerCharacter && ( - - event.stopPropagation()} - > -
-
{overlayPanel === 'character' ? '队伍' : '背包'}
- -
-
- {overlayPanel === 'character' ? ( - }> - { - closeOverlayPanel(); - openCampModal(); - }} - onOpenCharacterChat={target => { - closeOverlayPanel(); - characterChatUi.openChat(target); - }} - chatSummaries={characterChatSummaries} - onInspectMember={openPartyMemberDetails} - /> - - ) : ( - }> - - - )} -
-
-
- )} -
- - {shouldMountCampModal && ( - }> - - - )} - - {shouldMountMapModal && ( - setIsMapOpen(false)} />}> - { - const triggered = handleMapTravelToScene(scene.id); - if (triggered) { - setIsMapOpen(false); - } - }} - isTraveling={isLoading} - onClose={() => setIsMapOpen(false)} - /> - - )} - - {shouldMountCharacterChatModal && ( - }> - - - )} - - {shouldMountNpcModals && ( - }> - - - )} -
- ); -} diff --git a/src/components/auth/AuthGate.test.tsx b/src/components/auth/AuthGate.test.tsx index 7514003f..65ba9ba2 100644 --- a/src/components/auth/AuthGate.test.tsx +++ b/src/components/auth/AuthGate.test.tsx @@ -9,9 +9,9 @@ import { AuthGate } from './AuthGate'; import { useAuthUi } from './AuthUiContext'; const authMocks = vi.hoisted(() => ({ - getStoredAccessToken: vi.fn(), ensureAutoAuthUser: vi.fn(), getAuthLoginOptions: vi.fn(), + getCurrentAuthUser: vi.fn(), loginWithPhoneCode: vi.fn(), sendPhoneLoginCode: vi.fn(), startWechatLogin: vi.fn(), @@ -20,7 +20,6 @@ const authMocks = vi.hoisted(() => ({ vi.mock('../../services/apiClient', () => ({ AUTH_STATE_EVENT: 'genarrative-auth-state-changed', - getStoredAccessToken: authMocks.getStoredAccessToken, })); vi.mock('../../services/authService', () => ({ @@ -31,9 +30,9 @@ vi.mock('../../services/authService', () => ({ getAuthAuditLogs: vi.fn(), getAuthLoginOptions: authMocks.getAuthLoginOptions, getAuthRiskBlocks: vi.fn(), + getCurrentAuthUser: authMocks.getCurrentAuthUser, getAuthSessions: vi.fn(), getCaptchaChallengeFromError: vi.fn(() => null), - getCurrentAuthUser: vi.fn(), liftAuthRiskBlock: vi.fn(), loginWithPhoneCode: authMocks.loginWithPhoneCode, logoutAllAuthSessions: vi.fn(), @@ -76,8 +75,11 @@ const mockUser: AuthUser = { beforeEach(() => { vi.clearAllMocks(); - authMocks.getStoredAccessToken.mockReturnValue(null); authMocks.consumeAuthCallbackResult.mockReturnValue(null); + authMocks.getCurrentAuthUser.mockResolvedValue({ + user: null, + availableLoginMethods: ['phone'], + }); authMocks.loginWithPhoneCode.mockResolvedValue(mockUser); authMocks.sendPhoneLoginCode.mockResolvedValue({ cooldownSeconds: 60, diff --git a/src/components/auth/AuthGate.tsx b/src/components/auth/AuthGate.tsx index 31894296..919fdafd 100644 --- a/src/components/auth/AuthGate.tsx +++ b/src/components/auth/AuthGate.tsx @@ -10,7 +10,6 @@ import { import { useGameSettings } from '../../hooks/useGameSettings'; import { AUTH_STATE_EVENT, - getStoredAccessToken, } from '../../services/apiClient'; import { type AuthAuditLogEntry, @@ -228,12 +227,6 @@ export function AuthGate({ children }: AuthGateProps) { setShowLoginModal(true); } - const token = getStoredAccessToken(); - if (!token) { - await resolveGuestFallback(); - return; - } - try { const nextSession = await getCurrentAuthUser(); if (!isActive) { @@ -241,9 +234,8 @@ export function AuthGate({ children }: AuthGateProps) { } if (!nextSession.user) { - setUser(null); setAvailableLoginMethods(nextSession.availableLoginMethods); - setStatus('unauthenticated'); + await resolveGuestFallback(); return; } diff --git a/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.test.tsx b/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.test.tsx deleted file mode 100644 index 53a4f105..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* @vitest-environment jsdom */ - -import { render, screen } from '@testing-library/react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { afterEach, expect, test, vi } from 'vitest'; - -import { CustomWorldAgentClarificationPanel } from './CustomWorldAgentClarificationPanel'; - -afterEach(() => { - vi.restoreAllMocks(); -}); - -test('clarification panel shows pending questions and ready state', () => { - const pendingHtml = renderToStaticMarkup( - , - ); - const readyHtml = renderToStaticMarkup( - , - ); - - expect(pendingHtml).toContain('待补充问题'); - expect(pendingHtml).toContain('玩家是谁,故事开场时卡在什么处境里'); - expect(readyHtml).toContain('当前设定已齐备,可以进入下一阶段'); -}); - -test('falls back to stable keys when clarification ids are empty', () => { - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined); - - render( - , - ); - - expect(screen.getByText(/玩家身份与开局/u)).toBeTruthy(); - expect(screen.getByText(/核心冲突/u)).toBeTruthy(); - - const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) => - call.some( - (arg) => - typeof arg === 'string' && - arg.includes('Encountered two children with the same key'), - ), - ); - - expect(duplicateKeyCalls).toHaveLength(0); -}); diff --git a/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.tsx b/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.tsx deleted file mode 100644 index a0e9ca8c..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentClarificationPanel.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import type { - CreatorIntentReadiness, - CustomWorldPendingClarification, -} from '../../../packages/shared/src/contracts/customWorldAgent'; - -type CustomWorldAgentClarificationPanelProps = { - pendingClarifications: CustomWorldPendingClarification[]; - readiness: CreatorIntentReadiness; -}; - -export function CustomWorldAgentClarificationPanel({ - pendingClarifications, - readiness, -}: CustomWorldAgentClarificationPanelProps) { - if (readiness.isReady) { - return ( -
-
- 下一阶段 -
-
- 当前设定已齐备,可以进入下一阶段 -
-
- ); - } - - return ( -
-
-
-
- 待补充问题 -
-
- 先补最关键的 1 到 3 项 -
-
- - {pendingClarifications.length} - -
- -
- {pendingClarifications.slice(0, 3).map((item, index) => ( -
-
-
- {index + 1}. {item.label} -
-
P{item.priority}
-
-
- {item.question} -
-
- ))} -
-
- ); -} diff --git a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.interaction.test.tsx b/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.interaction.test.tsx deleted file mode 100644 index 14a2f771..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.interaction.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* @vitest-environment jsdom */ - -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { useState } from 'react'; -import { expect, test } from 'vitest'; - -import type { CustomWorldDraftCardDetail } from '../../../packages/shared/src/contracts/customWorldAgent'; -import { CustomWorldAgentDraftDetailPanel } from './CustomWorldAgentDraftDetailPanel'; -import { CustomWorldGenerateEntityModal } from './CustomWorldGenerateEntityModal'; - -const CHARACTER_DETAIL: CustomWorldDraftCardDetail = { - id: 'character-1', - kind: 'character', - title: '沈砺', - sections: [ - { - id: 'name', - label: '角色名', - value: '沈砺', - }, - { - id: 'publicMask', - label: '外显身份', - value: '守灯会里最熟悉旧航道的人。', - }, - { - id: 'summary', - label: '角色摘要', - value: '他像旧友,但也像一把始终没收回鞘的刀。', - }, - ], - linkedIds: ['thread-1'], - locked: false, - editable: true, - editableSectionIds: ['name', 'publicMask', 'summary'], - warningMessages: [], - assetStatus: 'missing', - assetStatusLabel: '待生成主图', -}; - -function DetailInteractionHarness() { - const [editMode, setEditMode] = useState(false); - const [generateMode, setGenerateMode] = useState<'character' | 'landmark' | null>( - null, - ); - const [savedPayload, setSavedPayload] = useState(''); - - return ( - <> - {}} - onStartEdit={() => { - setEditMode(true); - }} - onCancelEdit={() => { - setEditMode(false); - }} - onSave={(sections) => { - setSavedPayload(JSON.stringify(sections)); - setEditMode(false); - }} - onGenerateCharacter={() => { - setGenerateMode('character'); - }} - onGenerateLandmark={() => { - setGenerateMode('landmark'); - }} - onOpenRoleAssetStudio={() => {}} - /> - { - setGenerateMode(null); - }} - onSubmit={() => { - setGenerateMode(null); - }} - /> -
{savedPayload}
- - ); -} - -test('draft detail panel supports edit save and opening generate modals', async () => { - const user = userEvent.setup(); - - render(); - - await user.click(screen.getByRole('button', { name: '编辑设定' })); - const summaryInput = screen.getByLabelText('角色摘要'); - await user.clear(summaryInput); - await user.type(summaryInput, '他像旧友,也像最早知道航道秘密的人。'); - await user.click(screen.getByRole('button', { name: '保存' })); - - expect(screen.getByTestId('saved-payload').textContent).toContain( - '他像旧友,也像最早知道航道秘密的人。', - ); - - await user.click(screen.getByRole('button', { name: '新增角色' })); - expect(screen.getByRole('button', { name: '生成角色' })).toBeTruthy(); - expect(screen.getByText('当前参考卡')).toBeTruthy(); - const closeButtons = screen.getAllByRole('button', { name: '关闭' }); - await user.click(closeButtons[closeButtons.length - 1]!); - - expect(screen.getByRole('button', { name: '角色资产' })).toBeTruthy(); - - await user.click(screen.getByRole('button', { name: '新增场景' })); - expect(screen.getByRole('button', { name: '生成场景' })).toBeTruthy(); -}); diff --git a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.test.tsx b/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.test.tsx deleted file mode 100644 index 315e1c62..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server'; -import { expect, test } from 'vitest'; - -import { CustomWorldAgentDraftDetailPanel } from './CustomWorldAgentDraftDetailPanel'; - -test('draft detail panel renders sections and warnings', () => { - const html = renderToStaticMarkup( - {}} - onStartEdit={() => {}} - onGenerateCharacter={() => {}} - onGenerateLandmark={() => {}} - />, - ); - - expect(html).toContain('谁掌握航道解释权'); - expect(html).toContain('线程类型'); - expect(html).toContain('守灯会与沉船商盟'); - expect(html).toContain('继续精修'); - expect(html).toContain('编辑设定'); - expect(html).toContain('新增角色'); -}); - -test('draft detail panel renders scene chapter label and background preview', () => { - const html = renderToStaticMarkup( - {}} - onStartEdit={() => {}} - />, - ); - - expect(html).toContain('场景章节'); - expect(html).toContain('第 1 幕背景图'); - expect(html).toContain('img'); -}); diff --git a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx b/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx deleted file mode 100644 index 85e1c001..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentDraftDetailPanel.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import type { CustomWorldDraftCardDetail } from '../../../packages/shared/src/contracts/customWorldAgent'; -import { CustomWorldDraftEditPanel } from './CustomWorldDraftEditPanel'; - -type CustomWorldAgentDraftDetailPanelProps = { - detail: CustomWorldDraftCardDetail | null; - loading: boolean; - busy?: boolean; - editMode?: boolean; - onClose: () => void; - onStartEdit?: () => void; - onCancelEdit?: () => void; - onSave?: ( - sections: Array<{ - sectionId: string; - value: string; - }>, - ) => void; - onGenerateCharacter?: () => void; - onGenerateLandmark?: () => void; - onOpenRoleAssetStudio?: () => void; -}; - -function resolveKindLabel(kind: CustomWorldDraftCardDetail['kind']) { - if (kind === 'world') return '世界总卡'; - if (kind === 'camp') return '营地'; - if (kind === 'faction') return '势力'; - if (kind === 'character') return '角色'; - if (kind === 'landmark') return '地点'; - if (kind === 'thread') return '线程'; - if (kind === 'chapter') return '第一幕'; - if (kind === 'scene_chapter') return '场景章节'; - return '草稿卡'; -} - -function ActionButton(props: { - label: string; - onClick?: () => void; - disabled?: boolean; - tone?: 'default' | 'sky'; -}) { - const { label, onClick, disabled = false, tone = 'default' } = props; - - if (!onClick) { - return null; - } - - return ( - - ); -} - -export function CustomWorldAgentDraftDetailPanel({ - detail, - loading, - busy = false, - editMode = false, - onClose, - onStartEdit, - onCancelEdit, - onSave, - onGenerateCharacter, - onGenerateLandmark, - onOpenRoleAssetStudio, -}: CustomWorldAgentDraftDetailPanelProps) { - const shouldRenderImagePreview = ( - detailKind: CustomWorldDraftCardDetail['kind'], - sectionId: string, - value: string, - ) => - detailKind === 'scene_chapter' && - sectionId.endsWith(':backgroundImageSrc') && - value !== '待继续精修'; - - return ( -
-
-
-
- 卡片详情 -
-
- {loading ? '正在读取' : detail?.title || '选择一张草稿卡'} -
-
- -
- - {loading ? ( -
- 正在整理这张卡的内容。 -
- ) : detail ? ( -
-
- - {resolveKindLabel(detail.kind)} - - - 关联 {detail.linkedIds.length} - - {detail.editable ? ( - - 可编辑 - - ) : null} - {detail.kind === 'character' && detail.assetStatusLabel ? ( - - {detail.assetStatusLabel} - - ) : null} -
- -
- {!editMode && detail.editable ? ( - - ) : null} - {!editMode && detail.kind === 'character' ? ( - - ) : null} - {!editMode ? ( - <> - - - - ) : null} -
- - {editMode && onSave && onCancelEdit ? ( - - ) : ( -
- {detail.sections.map((section) => ( -
-
- {section.label} -
- {shouldRenderImagePreview(detail.kind, section.id, section.value) ? ( - {section.label} - ) : null} -
- {section.value} -
-
- ))} -
- )} - - {detail.warningMessages.length > 0 ? ( -
-
- 继续精修 -
-
- {detail.warningMessages.map((message, index) => ( -
- {message} -
- ))} -
-
- ) : null} -
- ) : ( -
- 从草稿抽屉里点开一张卡,就能在这里看世界底稿的具体内容。 -
- )} -
- ); -} diff --git a/src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx b/src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx deleted file mode 100644 index cd2a3812..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import type { CustomWorldDraftCardSummary } from '../../../packages/shared/src/contracts/customWorldAgent'; - -type CustomWorldAgentDraftDrawerProps = { - draftCards: CustomWorldDraftCardSummary[]; - activeCardId?: string | null; - onSelectCard: (cardId: string) => void; -}; - -const DRAWER_KIND_ORDER: CustomWorldDraftCardSummary['kind'][] = [ - 'world', - 'chapter', - 'scene_chapter', - 'thread', - 'faction', - 'character', - 'landmark', - 'camp', -]; - -function resolveGroupLabel(kind: CustomWorldDraftCardSummary['kind']) { - if (kind === 'world') return '世界总卡'; - if (kind === 'chapter') return '第一幕'; - if (kind === 'scene_chapter') return '场景章节'; - if (kind === 'thread') return '世界线程'; - if (kind === 'faction') return '势力'; - if (kind === 'character') return '关键角色'; - if (kind === 'landmark') return '关键地点'; - if (kind === 'camp') return '营地'; - return '草稿卡'; -} - -export function CustomWorldAgentDraftDrawer({ - draftCards, - activeCardId, - onSelectCard, -}: CustomWorldAgentDraftDrawerProps) { - const groupedCards = DRAWER_KIND_ORDER.map((kind) => ({ - kind, - items: draftCards.filter((card) => card.kind === kind), - })).filter((group) => group.items.length > 0); - - return ( -
-
- 草稿抽屉 -
- {groupedCards.length > 0 ? ( -
- {groupedCards.map((group) => ( -
-
-
- {resolveGroupLabel(group.kind)} -
-
- {group.items.length} -
-
-
- {group.items.map((card, index) => { - const isActive = activeCardId === card.id; - - return ( - - ); - })} -
-
- ))} -
- ) : ( -
- 当前设定收束后,世界底稿会先从这里长出来。 -
- )} -
- ); -} diff --git a/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.test.tsx b/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.test.tsx deleted file mode 100644 index 3b8d6cf5..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server'; -import { expect, test } from 'vitest'; - -import { CustomWorldAgentIntentSummaryPanel } from './CustomWorldAgentIntentSummaryPanel'; - -test('intent summary panel shows collected custom world anchors', () => { - const html = renderToStaticMarkup( - , - ); - - expect(html).toContain('已收集设定'); - expect(html).toContain('世界一句话'); - expect(html).toContain('一个被潮雾切开的列岛世界'); - expect(html).toContain('守灯会与沉船商盟争夺航道解释权'); - expect(html).toContain('5/6'); -}); diff --git a/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.tsx b/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.tsx deleted file mode 100644 index 3d00a72c..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentIntentSummaryPanel.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { CreatorIntentReadiness } from '../../../packages/shared/src/contracts/customWorldAgent'; -import { - evaluateCustomWorldCreatorIntentReadiness, - hasMeaningfulCustomWorldCreatorIntent, - normalizeCustomWorldCreatorIntent, -} from '../../services/customWorldCreatorIntent'; - -type CustomWorldAgentIntentSummaryPanelProps = { - creatorIntent: Record | null; - readiness: CreatorIntentReadiness; -}; - -export function CustomWorldAgentIntentSummaryPanel({ - creatorIntent, - readiness, -}: CustomWorldAgentIntentSummaryPanelProps) { - const intent = normalizeCustomWorldCreatorIntent(creatorIntent); - const resolvedReadiness = - readiness ?? evaluateCustomWorldCreatorIntentReadiness(intent); - const items = [ - { - label: '世界一句话', - value: intent?.worldHook || '', - ready: resolvedReadiness.completedKeys.includes('world_hook'), - }, - { - label: '玩家身份', - value: intent?.playerPremise || '', - ready: Boolean(intent?.playerPremise), - }, - { - label: '开局处境', - value: intent?.openingSituation || '', - ready: Boolean(intent?.openingSituation), - }, - { - label: '核心冲突', - value: intent?.coreConflicts.join('、') || '', - ready: resolvedReadiness.completedKeys.includes('core_conflict'), - }, - { - label: '主题气质', - value: - [...(intent?.themeKeywords ?? []), ...(intent?.toneDirectives ?? [])] - .filter(Boolean) - .join('、') || '', - ready: resolvedReadiness.completedKeys.includes('theme_and_tone'), - }, - { - label: '标志性要素', - value: intent?.iconicElements.join('、') || '', - ready: resolvedReadiness.completedKeys.includes('iconic_element'), - }, - ]; - - return ( -
-
-
-
- 已收集设定 -
-
- {resolvedReadiness.isReady ? '创作输入已齐备' : '继续收世界骨架'} -
-
- - {resolvedReadiness.completedKeys.length}/6 - -
- - {hasMeaningfulCustomWorldCreatorIntent(intent) ? ( -
- {items.map((item) => ( -
-
- {item.label} -
-
- {item.value || '待补充'} -
-
- ))} -
- ) : ( -
- 还在收集你的世界设定 -
- )} -
- ); -} diff --git a/src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx b/src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx deleted file mode 100644 index 29f901b8..00000000 --- a/src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { X } from 'lucide-react'; - -type CustomWorldAgentLauncherModalProps = { - isOpen: boolean; - seedText: string; - isBusy: boolean; - error: string | null; - onClose: () => void; - onSeedTextChange: (value: string) => void; - onConfirm: () => void; -}; - -export function CustomWorldAgentLauncherModal({ - isOpen, - seedText, - isBusy, - error, - onClose, - onSeedTextChange, - onConfirm, -}: CustomWorldAgentLauncherModalProps) { - if (!isOpen) { - return null; - } - - return ( -
-
-
-
-
- 开始和 Agent 共创 -
-
- 输入一段种子灵感,先进入新的工作区。 -
-
- -
- -
-