From c810e255a59a7d13ae2a7fb053caa29727faf799 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 7 Jun 2026 15:55:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=BC=E5=9B=BE=E6=96=87?= =?UTF-8?q?=E5=AD=97=E7=9B=B4=E5=88=9B=E8=BF=87=E6=97=A9=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正拼图文字直创 compile 回包未出图时继续保持生成中 补充文字直创无正式图的回归测试 更新玩法链路文档和 Hermes 踩坑记录 --- .hermes/shared-memory/pitfalls.md | 8 ++ ...玩法创作】平台入口与玩法链路-2026-05-15.md | 2 +- .../PlatformEntryFlowShellImpl.tsx | 71 ++++++++++++--- ...gEntryFlowShell.agent.interaction.test.tsx | 89 +++++++++++++++++++ 4 files changed, 158 insertions(+), 12 deletions(-) diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 82638b45..5f628f58 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -2067,6 +2067,14 @@ - 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle draft generation auto starts trial and runtime back opens draft result"`。 - 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 +## 拼图文字直创的 compile 回包不等于生成完成 + +- 现象:只输入文字点击生成拼图时,页面刚进入生成页就弹出“生成任务已完成,可以继续查看草稿。”,随后又提示“请先选择一张正式拼图图片。”,结果页关卡里也没有图。 +- 原因:统一创作表单路径把 `compile_puzzle_draft` 的同步回包无条件当成 ready;但后端在 AI 重绘路径会先返回 `stage=image_refining`、`progressPercent=88` 的会话,只表示首关草稿已编译且后台首图 / UI 资产任务已启动,还没有正式封面或候选图。 +- 处理:前端必须继续用 `isPuzzleCompileActionReady(...)` 判断回包 session;没有 `draft.coverImageSrc`、首关 `coverImageSrc` 或候选图时保持生成中,不弹完成、不把作品架 pending 标 ready、不自动试玩。生成页轮询合并 session 进度时,未进入编译态或进度无变化就返回原 state,避免轮询制造重复 render。 +- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "puzzle text-only form stays generating|puzzle draft generation auto starts trial|running puzzle draft opens generation progress"`。 +- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## CreativeImageInputPanel 主图点击默认预览 - 现象:复用 `CreativeImageInputPanel` 的结果页 / 编辑页已有主图时,用户点击图片却触发上传,无法直接查看大图;不同玩法若各自手写上传按钮会让主图、历史图、AI 重绘和参考图行为再次分叉。 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 7ed2198f..3292a9ac 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -111,7 +111,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列 - 图像输入复用 `CreativeImageInputPanel`。 - 结果页每关画面编辑复用 `CreativeImageInputPanel`;入口页和关卡画面只共享受控 UI 模块,不共享数据源、状态、action 或存储位置:入口页继续写 `formDraft` 与草稿编译 payload,关卡画面写 `levels[].pictureReference/pictureDescription` 并触发 `generate_puzzle_images`。结果页删除独立“素材配置”Tab,不再提供单独 UI 背景生成入口。通用图片面板的展示图和 AI 重绘参考图能力必须分开控制:结果页正式关卡图只作为预览图,不因存在正式图自动暴露 AI 重绘开关;只有本地上传、历史选择或已保存 `pictureReference` 可作为重绘参考图时,才显示 AI 重绘开关并把状态带入 `generate_puzzle_images`。用户在本次编辑中上传或选择历史图后,该图优先占据主图卡片,可删除、切换 AI 重绘,也可关闭 AI 重绘直用;仅有正式图预览时,画面描述框仍可上传多张参考图。关卡详情弹窗应使用加宽面板,关卡名称、画面图和画面描述合并在同一个纵向列表中,名称输入和画面编辑模块外层不再包独立 `platform-subpanel`;画面图卡仍必须保留稳定最小高度,避免弹窗内 `flex-1` 布局坍缩后只剩标题、描述输入和操作按钮。 - 支持画面描述生图、多参考图生图、上传或历史生成主图后 AI 重绘、上传或历史生成主图后不重绘;主链要求浏览器先经 `/api/assets/direct-upload-tickets` 直传 OSS 并确认 `asset_object`,创作 action 只提交 `referenceImageAssetObjectId(s)`,由后端校验 owner / bucket / kind / MIME / size 后签发 OSS 只读 URL 并下载为 VectorEngine `/v1/images/edits` 的 multipart `image` part。本地上传 Data URL 与历史 `/generated-*` 图片路径仅保留为旧草稿、旧入口或未迁移客户端的兼容输入;关闭 AI 重绘时,后端统一解析为首关或当前关卡正式图后再持久化,不调用第一段拼图首图生成。 -- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要,生成完成并回写关卡拼图画面、关卡画面参考图、UI spritesheet 和关卡背景图后再变为 `ready`;当前不自动生成背景音乐。生成页步骤推进必须跟随后端 session `progressPercent` 的真实里程碑:`88` 表示草稿编译完成并进入出图步骤,`94` 表示生成图已保存并进入 UI / 背景步骤,`96` 表示正式图与 UI 背景已确认并进入写入步骤,最终 action 成功或发布才进入完成态;每个步骤内部可以按实际等待时间使用假进度平滑推进。`88/94/96` 只负责切换当前步骤,不作为总进度地板;总进度按已完成步骤权重加当前步骤内假进度推导,非完成态最多停在 `98%`。任一同步 action 回包到达时立即以真实完成/失败结果冻结进度。 +- 草稿生成会先持久化 `generationStatus=generating` 的作品摘要,生成完成并回写关卡拼图画面、关卡画面参考图、UI spritesheet 和关卡背景图后再变为 `ready`;当前不自动生成背景音乐。生成页步骤推进必须跟随后端 session `progressPercent` 的真实里程碑:`88` 表示草稿编译完成并进入出图步骤,`94` 表示生成图已保存并进入 UI / 背景步骤,`96` 表示正式图与 UI 背景已确认并进入写入步骤,最终 action 成功或发布才进入完成态;每个步骤内部可以按实际等待时间使用假进度平滑推进。`88/94/96` 只负责切换当前步骤,不作为总进度地板;总进度按已完成步骤权重加当前步骤内假进度推导,非完成态最多停在 `98%`。文字直创的 `compile_puzzle_draft` 同步回包只表示已编译首关草稿并启动后台首图 / UI 资产生成;只要回包 session 仍缺正式 `draft.coverImageSrc`、首关 `coverImageSrc` 和候选图,前端必须继续保持生成中,不弹完成通知、不把草稿卡标记为 ready,也不得自动进入结果页或试玩。 - 作品架拼图草稿的“生成中”遮罩只表示初始草稿还没有可查看结果;只要作品摘要、首关封面或任一关卡候选图已经可用,后续 UI 背景重生成和追加关卡生图都必须作为结果页局部生成态处理,不能阻止打开草稿结果页。生成失败后,同一浏览器会话内的失败 notice 必须覆盖后端可能仍短暂返回的 `generationStatus=generating` 摘要,作品架保留对应草稿卡但不再显示“生成中”,点击后回到失败 / 重试状态。 - 拼图草稿编译是长耗时 action,前端 action 请求默认等待 `1_800_000ms`(30 分钟)且不自动重试。每次图片生成调用的预期用时按 90 秒计算,但 `生成拼图首图` 单独按 4 分钟展示;完整 AI 重绘路径为 `编译首关草稿` 8 秒、`生成关卡名称` 10 秒、`生成拼图首图` 4 分钟、`生成关卡画面` 90 秒、`生成UI与背景` 90 秒、`写入正式草稿` 10 秒,合计约 448 秒。上传图且关闭 AI 重绘时必须跳过 `生成拼图首图`,直接进入 `生成关卡画面` 和 `生成UI与背景`,合计约 208 秒。生成页恢复时必须使用后端 session `updatedAt` 或作品摘要 `updatedAt` 作为原始 `startedAtMs`;失败/完成态用 `finishedAtMs` 冻结耗时。生成完成后若自动进入草稿试玩,进入 `/runtime/puzzle` 前必须先把 `/creation/puzzle/result` 和当前 `sessionId/profileId/workId` 写成浏览器历史前一站;运行态返回按钮和系统返回都应回到结果页,不得退回生成进度页或暴露重新生成入口。未收到对应后端里程碑前,后续步骤保持待处理;即使当前步骤预计时长耗尽,也只能让当前步骤内部进度停在 `98%` 内,不能自动完成当前步骤或跳到后续步骤。生成页每个步骤只展示标题和进度,不展示步骤详细描述。 - 前端创作、结果页、生成页和错误提示不展示 GPT / Gemini 等具体模型名称;如需在内部保留模型路由,UI 只使用“标准模式”“创意模式”等产品化名称。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 982a10f7..e143cb0a 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -2620,26 +2620,33 @@ function mergePuzzleSessionProgressIntoGenerationState( const isCompiledGenerationSession = Boolean( session.draft && !session.draft.formDraft, ); + if (!isCompiledGenerationSession) { + return state; + } - const nextPhaseId = isCompiledGenerationSession - ? resolvePuzzlePhaseFromSessionProgress(state, session) - : state.metadata?.puzzleActivePhaseId; + const nextPhaseId = resolvePuzzlePhaseFromSessionProgress(state, session); const shouldResetActiveStepStart = - isCompiledGenerationSession && nextPhaseId != null && nextPhaseId !== state.metadata?.puzzleActivePhaseId; + const nextActiveStepStartedAtMs = shouldResetActiveStepStart + ? resolveMiniGameDraftGenerationStartedAtMs(session.updatedAt) + : state.metadata?.puzzleActiveStepStartedAtMs; + + if ( + state.metadata?.puzzleActivePhaseId === nextPhaseId && + state.metadata?.puzzleActiveStepStartedAtMs === nextActiveStepStartedAtMs && + state.metadata?.puzzleProgressPercent === session.progressPercent + ) { + return state; + } return { ...state, metadata: { ...state.metadata, puzzleActivePhaseId: nextPhaseId, - puzzleActiveStepStartedAtMs: shouldResetActiveStepStart - ? resolveMiniGameDraftGenerationStartedAtMs(session.updatedAt) - : state.metadata?.puzzleActiveStepStartedAtMs, - puzzleProgressPercent: isCompiledGenerationSession - ? session.progressPercent - : state.metadata?.puzzleProgressPercent, + puzzleActiveStepStartedAtMs: nextActiveStepStartedAtMs, + puzzleProgressPercent: session.progressPercent, }, }; } @@ -7977,7 +7984,49 @@ export function PlatformEntryFlowShellImpl({ actionPayload, ); setPuzzleOperation(response.operation); - const openResult = isViewingPuzzleGeneration(nextSession.sessionId); + const openResult = + isViewingPuzzleGeneration(nextSession.sessionId) || + isViewingPuzzleGeneration(response.session.sessionId); + if (!isPuzzleCompileActionReady(response.session)) { + // 中文注释:文字直创的同步 action 回包只代表后台生图任务已启动; + // 未拿到正式图前继续保持生成中,避免误弹完成或启动空草稿试玩。 + const nextGenerationState = mergePuzzleSessionProgressIntoGenerationState( + generationState, + response.session, + ); + activePuzzleGenerationSessionIdRef.current = response.session.sessionId; + setPuzzleBackgroundCompileTasks((current) => { + const next = { ...current }; + if (nextSession.sessionId !== response.session.sessionId) { + delete next[nextSession.sessionId]; + } + next[response.session.sessionId] = { + session: response.session, + payload, + generationState: nextGenerationState, + error: null, + }; + return next; + }); + puzzleFlow.setSession(response.session); + if (openResult) { + setPuzzleGenerationState(nextGenerationState); + } + markDraftGenerating('puzzle', [ + response.session.sessionId, + buildPuzzleResultWorkId(response.session.sessionId), + response.session.publishedProfileId, + buildPuzzleResultProfileId(response.session.sessionId), + ]); + markPendingDraftGenerating( + 'puzzle', + response.session.sessionId, + buildPendingPuzzleDraftMetadata(payload), + ); + void refreshPuzzleShelf(); + return; + } + const readyGenerationState = resolveFinishedMiniGameDraftGenerationState( generationState, diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 751c4ed3..40159ab2 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -5217,6 +5217,95 @@ test('running puzzle draft opens generation progress from draft tab', async () = }); }); +test('puzzle text-only form stays generating when compile starts background image without cover', async () => { + const user = userEvent.setup(); + const initialSession = buildMockPuzzleAgentSession({ + sessionId: 'puzzle-session-text-only', + stage: 'collecting_anchors', + progressPercent: 0, + draft: null, + }); + const generatingDraft = buildReadyPuzzleDraft({ + workTitle: '文字直创拼图', + workDescription: '只输入文字后后台继续生成图片。', + candidates: [], + selectedCandidateId: null, + coverImageSrc: null, + coverAssetId: null, + generationStatus: 'generating', + levels: [ + { + ...buildReadyPuzzleDraft().levels![0]!, + candidates: [], + selectedCandidateId: null, + coverImageSrc: null, + coverAssetId: null, + generationStatus: 'generating', + }, + ], + }); + const generatingSession = buildMockPuzzleAgentSession({ + sessionId: 'puzzle-session-text-only', + stage: 'image_refining', + progressPercent: 88, + draft: generatingDraft, + lastAssistantReply: '已编译首关草稿,并启动首关画面和 UI 资产后台生成。', + resultPreview: { + draft: generatingDraft, + blockers: [ + { + id: 'missing-cover-image-puzzle-level-1', + code: 'MISSING_COVER_IMAGE', + message: '正式拼图图片尚未确定', + }, + ], + qualityFindings: [], + publishReady: false, + }, + }); + + vi.mocked(createPuzzleAgentSession).mockResolvedValueOnce({ + session: initialSession, + }); + vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({ + operation: { + operationId: 'compile-puzzle-text-only', + type: 'compile_puzzle_draft', + status: 'completed', + phaseLabel: '首关拼图草稿', + phaseDetail: '已编译首关草稿,并启动首关画面和 UI 资产后台生成。', + progress: 0.88, + }, + session: generatingSession, + }); + vi.mocked(getPuzzleAgentSession).mockResolvedValue({ + session: generatingSession, + }); + + render(); + + await openCreateTemplateHub(user); + await user.click(await findCreationTypeButton('拼图')); + await user.click(await screen.findByRole('button', { name: '生成草稿' })); + + expect( + await screen.findByRole('progressbar', { + name: '拼图图片生成进度', + }), + ).toBeTruthy(); + await waitFor(() => { + expect(executePuzzleAgentAction).toHaveBeenCalledWith( + 'puzzle-session-text-only', + expect.objectContaining({ action: 'compile_puzzle_draft' }), + ); + }); + expect(screen.queryByRole('dialog', { name: '生成完成' })).toBeNull(); + expect(screen.queryByText('请先选择一张正式拼图图片。')).toBeNull(); + expect(screen.queryByText('拼图结果页')).toBeNull(); + expect(updatePuzzleWork).not.toHaveBeenCalled(); + expect(startLocalPuzzleRun).not.toHaveBeenCalled(); +}); + test('puzzle form checks mud points before creating a draft', async () => { const user = userEvent.setup(); vi.mocked(getProfileDashboard).mockResolvedValue({