From df5e20d550e090f9d86ee4fc5ee6b2b219c420a9 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 04:56:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20Match3D=20?= =?UTF-8?q?=E7=94=9F=E6=88=90=E8=B5=84=E4=BA=A7=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 1 + docs/README.md | 2 +- ...eDraftGenerationStateModel收口计划-2026-06-04.md | 10 ++- .../PlatformEntryFlowShellImpl.tsx | 33 +------- ...mMiniGameDraftGenerationStateModel.test.ts | 81 +++++++++++++++++++ ...atformMiniGameDraftGenerationStateModel.ts | 30 +++++++ 6 files changed, 122 insertions(+), 35 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 52dbd032..c4e689ac 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1423,6 +1423,7 @@ - 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护小游戏生成状态恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定,壳层同时承担 API / background task 副作用和 `MiniGameDraftGenerationState` 生命周期细节。 - 决策:新增 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,收口恢复态、失败态、完成态、展示 rebase、拼图 progress phase 阈值和进度 metadata 合并。壳层继续负责 API、后台任务、React state 写入、作品架刷新、URL 和 stage 切换。 +- 追加决策:抓大鹅轮询作品素材时的旁路进度合并也归入该 Module,由 `mergeMatch3DGeneratedAssetsIntoGenerationState(state, assets)` 统一统计可用图片素材、至少 5 个总素材计数、`match3d-generate-views` phase 推进和首个素材错误传播;壳层只负责轮询 session / work detail 与写入 state。 - 影响范围:拼图 / 抓大鹅 / 大鱼吃小鱼 / 方洞 / 跳一跳 / 敲木鱼 / 宝贝识物生成状态恢复、完成失败收尾、生成页返回展示和拼图轮询进度合并。 - 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 diff --git a/docs/README.md b/docs/README.md index 94a90752..57dac10e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,7 +57,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 平台壳的拼图 runtime 恢复 work、跳一跳 pending session、敲木鱼 detail 恢复 session 和敲木鱼 pending session DTO 映射收口到 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,壳层只保留网络、状态、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md)。 -平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定收口到 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,壳层只保留 API、后台任务、React state、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md)。 +平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并、抓大鹅生成资产旁路进度合并和 ready / generating 判定收口到 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,壳层只保留 API、后台任务、React state、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md)。 平台小游戏草稿恢复和提交所需的拼图 / 抓大鹅表单 payload、拼图编译 action、pending metadata 与拼图 form-only 草稿判定收口到 `src/components/platform-entry/platformMiniGameDraftPayloadModel.ts`,壳层只保留 API、Action 执行、background task 与状态副作用,规则见 [【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftPayloadModel收口计划-2026-06-04.md)。 diff --git a/docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md b/docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md index bbfb5ed8..1b6e6cc7 100644 --- a/docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md +++ b/docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md @@ -2,7 +2,7 @@ ## 背景 -`PlatformEntryFlowShellImpl.tsx` 曾内联维护小游戏生成状态的恢复、失败/完成收尾、展示 rebase、拼图后端进度合并和生成中 / ready 判定。壳层因此既要处理 API 回包、React state、后台任务、URL 和 stage,又要记住 `MiniGameDraftGenerationState` 的生命周期细节。 +`PlatformEntryFlowShellImpl.tsx` 曾内联维护小游戏生成状态的恢复、失败/完成收尾、展示 rebase、拼图后端进度合并、抓大鹅生成资产旁路进度合并和生成中 / ready 判定。壳层因此既要处理 API 回包、React state、后台任务、URL 和 stage,又要记住 `MiniGameDraftGenerationState` 的生命周期细节。 这些状态变换不读取 DOM,不请求网络,也不写 React state;它们属于平台层小游戏草稿生成状态 **Module**。壳层只应决定何时调用、把返回值写入对应 state。 @@ -14,6 +14,7 @@ - `createFailedMiniGameDraftGenerationStateForRestoredDraft(kind, updatedAt, error, metadata?)`:恢复失败草稿时按后端 `updatedAt` 建立失败态。 - `rebaseMiniGameDraftGenerationStateForDisplay(state)` 与 `rebaseMiniGameDraftBackgroundCompileTaskForDisplay(task)`:清理展示用 `finishedAtMs`,避免返回生成页后沿用结束态计时。 - `createPuzzleDraftGenerationStateFromPayload(payload, session?)`、`resolvePuzzlePhaseFromSessionProgress(state, session)`、`mergePuzzleSessionProgressIntoGenerationState(state, session)`:集中处理拼图生成的 aiRedraw、后端进度百分比和 phase 推进。 +- `mergeMatch3DGeneratedAssetsIntoGenerationState(state, assets)`:抓大鹅轮询到作品素材后,按可用图片数量推进生成页资产计数,并把首个素材错误传播到生成态。 - `resolveFinishedMiniGameDraftGenerationState(state, phase, options?)`:统一完成 / 失败收尾的 `finishedAtMs`、错误与资产计数合并。 - `isMiniGameDraftReady(state)` 与 `isMiniGameDraftGenerating(state)`:统一生成态轻量判定。 @@ -27,12 +28,15 @@ - 拼图 session 只有在 `draft` 存在且不是 `formDraft` 时才视为后端编译生成中 session,才写入 `puzzleProgressPercent` 并推进 phase。 - 拼图进度阈值保持旧值:`>=96` 到 `puzzle-select-image`,`>=94` 到 `puzzle-ui-assets`,`>=88` 时按 `puzzleAiRedraw=false` 进入 `puzzle-level-scene`,否则进入 `puzzle-cover-image`。 - phase 变化时 `puzzleActiveStepStartedAtMs` 使用 session `updatedAt` 解析值;phase 不变时保留旧值。 +- 抓大鹅资产旁路进度不得覆盖 `ready` 或 `failed` 终态;非终态下只统计有 `imageViews[].imageObjectKey` / `imageViews[].imageSrc`、顶层 `imageObjectKey` 或顶层 `imageSrc` 的素材。 +- 抓大鹅资产旁路进度的 `totalAssetCount` 至少为 `5`,保留当前五物品首批生成节奏;已有素材数量超过 `5` 时按真实素材数量展示。 +- 抓大鹅已有可用素材时 phase 推进到 `match3d-generate-views`;无可用素材时保留原 phase;首个素材错误写入 `error`,无素材错误时保留原错误。 - 展示 rebase 只清理 `finishedAtMs`,不得修改 phase、error、资产计数或 metadata。 ## Depth / Leverage / Locality -- **Depth**:壳层以状态变换函数表达意图;生成态字段、拼图阈值、时间解析与计数合并藏入 Module Implementation。 -- **Leverage**:后续新增小游戏生成恢复或调整拼图后端进度阈值时,先改 Module 与单测,再让壳层 Adapter 保持调用点不变。 +- **Depth**:壳层以状态变换函数表达意图;生成态字段、拼图阈值、抓大鹅素材计数、时间解析与计数合并藏入 Module Implementation。 +- **Leverage**:后续新增小游戏生成恢复、调整拼图后端进度阈值或改变抓大鹅素材批次展示时,先改 Module 与单测,再让壳层 Adapter 保持调用点不变。 - **Locality**:小游戏生成状态规则集中到一个纯测试面,避免在大型壳层的 API callback、background task 和恢复流程中重复推理 `MiniGameDraftGenerationState`。 ## 验收 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index c55f45cd..6de3218b 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -49,7 +49,6 @@ import type { } from '../../../packages/shared/src/contracts/match3dAgent'; import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime'; import type { - Match3DGeneratedItemAsset, Match3DWorkProfile, Match3DWorkSummary, } from '../../../packages/shared/src/contracts/match3dWorks'; @@ -516,6 +515,7 @@ import { createPuzzleDraftGenerationStateFromPayload, isMiniGameDraftGenerating, isMiniGameDraftReady, + mergeMatch3DGeneratedAssetsIntoGenerationState, mergePuzzleSessionProgressIntoGenerationState, rebaseMiniGameDraftBackgroundCompileTaskForDisplay, rebaseMiniGameDraftGenerationStateForDisplay, @@ -751,35 +751,6 @@ function mapVisualNovelWorkDetailToSession( }; } -function resolveMatch3DGenerationStateFromAssets( - current: MiniGameDraftGenerationState | null, - assets: readonly Match3DGeneratedItemAsset[] | null | undefined, -): MiniGameDraftGenerationState | null { - if (!current || current.phase === 'ready' || current.phase === 'failed') { - return current; - } - - const assetList = assets ?? []; - const imageReadyCount = assetList.filter( - (asset) => - asset.imageViews?.some( - (view) => view.imageObjectKey?.trim() || view.imageSrc?.trim(), - ) || - asset.imageObjectKey?.trim() || - asset.imageSrc?.trim(), - ).length; - const totalAssetCount = Math.max(5, assetList.length); - const failedAsset = assetList.find((asset) => asset.error?.trim()); - - return { - ...current, - phase: imageReadyCount > 0 ? 'match3d-generate-views' : current.phase, - completedAssetCount: imageReadyCount, - totalAssetCount, - error: failedAsset?.error?.trim() || current.error, - }; -} - function buildSquareHoleProfileFromSession( session: SquareHoleSessionSnapshot | null, ): SquareHoleWorkProfile | null { @@ -4807,7 +4778,7 @@ export function PlatformEntryFlowShellImpl({ const normalizedItem = normalizeMatch3DWorkForRuntimeUi(item); setMatch3DProfile(normalizedItem); setMatch3DGenerationState((current) => - resolveMatch3DGenerationStateFromAssets( + mergeMatch3DGeneratedAssetsIntoGenerationState( current, normalizedItem.generatedItemAssets, ), diff --git a/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts b/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts index df06fd1c..d398ee27 100644 --- a/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts +++ b/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import type { Match3DGeneratedItemAsset } from '../../../packages/shared/src/contracts/match3dWorks'; import type { PuzzleAnchorPack } from '../../../packages/shared/src/contracts/puzzleAgentDraft'; import type { CreatePuzzleAgentSessionRequest, @@ -12,6 +13,7 @@ import { createPuzzleDraftGenerationStateFromPayload, isMiniGameDraftGenerating, isMiniGameDraftReady, + mergeMatch3DGeneratedAssetsIntoGenerationState, mergePuzzleSessionProgressIntoGenerationState, rebaseMiniGameDraftBackgroundCompileTaskForDisplay, rebaseMiniGameDraftGenerationStateForDisplay, @@ -96,6 +98,17 @@ function buildState( }; } +function buildMatch3DAsset( + overrides: Partial = {}, +): Match3DGeneratedItemAsset { + return { + itemId: 'item-1', + itemName: '红宝石', + status: 'pending', + ...overrides, + }; +} + beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(NOW); @@ -277,6 +290,74 @@ describe('platformMiniGameDraftGenerationStateModel', () => { }); }); + test('merges match3d generated assets into active generation state', () => { + const state = buildState({ + kind: 'match3d', + phase: 'match3d-material-sheet', + completedAssetCount: 0, + totalAssetCount: 0, + error: '旧错误', + }); + + expect( + mergeMatch3DGeneratedAssetsIntoGenerationState(state, [ + buildMatch3DAsset({ + itemId: 'item-with-view', + imageViews: [ + { + viewId: 'front', + viewIndex: 0, + imageObjectKey: 'objects/front.png', + }, + ], + }), + buildMatch3DAsset({ + itemId: 'item-with-src', + imageSrc: '/generated/item.png', + }), + buildMatch3DAsset({ + itemId: 'item-with-error', + error: '切图失败', + }), + ]), + ).toMatchObject({ + phase: 'match3d-generate-views', + completedAssetCount: 2, + totalAssetCount: 5, + error: '切图失败', + }); + }); + + test('keeps match3d generated asset merge away from finished states', () => { + const readyState = buildState({ + kind: 'match3d', + phase: 'ready', + completedAssetCount: 5, + totalAssetCount: 5, + }); + const failedState = buildState({ + kind: 'match3d', + phase: 'failed', + error: '已失败', + }); + + expect( + mergeMatch3DGeneratedAssetsIntoGenerationState(readyState, [ + buildMatch3DAsset({ imageSrc: '/generated/new.png' }), + ]), + ).toBe(readyState); + expect( + mergeMatch3DGeneratedAssetsIntoGenerationState(failedState, [ + buildMatch3DAsset({ imageSrc: '/generated/new.png' }), + ]), + ).toBe(failedState); + expect( + mergeMatch3DGeneratedAssetsIntoGenerationState(null, [ + buildMatch3DAsset({ imageSrc: '/generated/new.png' }), + ]), + ).toBeNull(); + }); + test('finishes generation state and resolves ready/generating flags', () => { const failedState = resolveFinishedMiniGameDraftGenerationState( buildState({ error: '旧错误' }), diff --git a/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts b/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts index 00ee2e39..2dc18268 100644 --- a/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts +++ b/src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts @@ -1,3 +1,4 @@ +import type { Match3DGeneratedItemAsset } from '../../../packages/shared/src/contracts/match3dWorks'; import type { CreatePuzzleAgentSessionRequest, PuzzleAgentSessionSnapshot, @@ -134,6 +135,35 @@ export function mergePuzzleSessionProgressIntoGenerationState( }; } +export function mergeMatch3DGeneratedAssetsIntoGenerationState( + current: MiniGameDraftGenerationState | null, + assets: readonly Match3DGeneratedItemAsset[] | null | undefined, +): MiniGameDraftGenerationState | null { + if (!current || current.phase === 'ready' || current.phase === 'failed') { + return current; + } + + const assetList = assets ?? []; + const imageReadyCount = assetList.filter( + (asset) => + asset.imageViews?.some( + (view) => view.imageObjectKey?.trim() || view.imageSrc?.trim(), + ) || + asset.imageObjectKey?.trim() || + asset.imageSrc?.trim(), + ).length; + const totalAssetCount = Math.max(5, assetList.length); + const failedAsset = assetList.find((asset) => asset.error?.trim()); + + return { + ...current, + phase: imageReadyCount > 0 ? 'match3d-generate-views' : current.phase, + completedAssetCount: imageReadyCount, + totalAssetCount, + error: failedAsset?.error?.trim() || current.error, + }; +} + export function resolveFinishedMiniGameDraftGenerationState( state: MiniGameDraftGenerationState, phase: 'ready' | 'failed',