refactor: 收口草稿打开状态规则

This commit is contained in:
2026-06-04 05:51:09 +08:00
parent b037ce1e32
commit 217cc881e6
6 changed files with 589 additions and 180 deletions

View File

@@ -16,6 +16,14 @@
--- ---
## 2026-06-04 Draft Generation Shelf 草稿打开 intent 收口
- 背景:`openPuzzleDraft` / `openMatch3DDraft` 在平台壳内重复判断已发布作品、缺 session、ready 未读、失败 notice、active / background 生成中、持久化 generating 和普通草稿恢复,导致壳层继续理解拼图稳定 ID、抓大鹅 notice key 与生成状态优先级。
- 决策:扩展 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,以 `resolvePuzzleDraftOpenIntent(...)``resolveMatch3DDraftOpenIntent(...)` 返回纯打开计划和 notice keys壳层只按 intent 执行网络读取、生成态 rebase、试玩启动、错误写入、路由 / stage 和 notice seen 副作用。
- 影响范围:创作中心作品架打开拼图 / 抓大鹅草稿、公开码搜索强制打开抓大鹅草稿、生成完成后 ready 未读试玩、失败草稿恢复和后续 pending / persisted generating 判定。
- 验证方式:`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对 Draft Shelf Module 与平台壳执行 ESLint、`npm run typecheck``npm run check:encoding`
- 关联文档:`docs/technical/【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md`
## 2026-06-04 Bark Battle Work Cache 草稿状态收口 ## 2026-06-04 Bark Battle Work Cache 草稿状态收口
- 背景:`PlatformEntryFlowShellImpl.tsx` 仍内联维护 Bark Battle 草稿三图完整性、生成状态归一、作品架摘要恢复草稿配置,以及草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 字段映射,导致结果页试玩、作品架启动、草稿恢复和公开详情启动都要理解同一份资产字段清单。 - 背景:`PlatformEntryFlowShellImpl.tsx` 仍内联维护 Bark Battle 草稿三图完整性、生成状态归一、作品架摘要恢复草稿配置,以及草稿 / 已发布作品进入 runtime 前的 `BarkBattlePublishedConfig` 字段映射,导致结果页试玩、作品架启动、草稿恢复和公开详情启动都要理解同一份资产字段清单。

View File

@@ -77,7 +77,7 @@ Bark Battle 草稿三图完整性、生成状态归一、作品架摘要恢复
RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/components/platform-entry/platformRpgAgentResultPreviewModel.ts`,壳层只保留 session/profile 编排和结果页 props 传递,规则见 [【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md)。 RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/components/platform-entry/platformRpgAgentResultPreviewModel.ts`,壳层只保留 session/profile 编排和结果页 props 传递,规则见 [【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md)。
平台入口创作生成通知、pending 作品架占位、作品详情更新回填、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 平台入口创作生成通知、pending 作品架占位、作品详情更新回填、失败覆盖、拼图 / 抓大鹅草稿打开优先级、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、恢复目标身份匹配、跳一跳 / 敲木鱼恢复阶段落点、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts``src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。 平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、恢复目标身份匹配、跳一跳 / 敲木鱼恢复阶段落点、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts``src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。

View File

@@ -15,9 +15,10 @@
- `buildCreationWorkShelfRuntimeState({ item, notices, pendingShelfItems })`:统一输出 `CreationWorkShelfRuntimeState`,处理失败覆盖、拼图空标题 `拼图草稿` 兜底、summary 占位覆盖、生成中遮罩和 ready 未读点。 - `buildCreationWorkShelfRuntimeState({ item, notices, pendingShelfItems })`:统一输出 `CreationWorkShelfRuntimeState`,处理失败覆盖、拼图空标题 `拼图草稿` 兜底、summary 占位覆盖、生成中遮罩和 ready 未读点。
- `collectVisibleDraftNoticeKeys(...)` / `hasUnreadDraftGenerationUpdates(...)`:统一草稿 Tab 顶部未读点规则。 - `collectVisibleDraftNoticeKeys(...)` / `hasUnreadDraftGenerationUpdates(...)`:统一草稿 Tab 顶部未读点规则。
- `mergePuzzleWorkSummary(current, updated)``mergeBigFishWorkSummary(current, updated)`:统一作品详情更新后回填作品架和当前详情的身份匹配规则。 - `mergePuzzleWorkSummary(current, updated)``mergeBigFishWorkSummary(current, updated)`:统一作品详情更新后回填作品架和当前详情的身份匹配规则。
- `resolvePuzzleDraftOpenIntent(...)``resolveMatch3DDraftOpenIntent(...)`:统一拼图 / 抓大鹅草稿打开时的已发布详情、缺 session、ready 未读试玩、失败生成页、active / background 生成页、持久化 generating 恢复和普通草稿恢复优先级。
- `buildPuzzleResultWorkId(...)` / `buildPuzzleResultProfileId(...)``isPersistedDraftGenerating(...)` / `isPersistedDraftFailed(...)`:把拼图稳定 ID 与持久化状态判断收在同一 **Seam** - `buildPuzzleResultWorkId(...)` / `buildPuzzleResultProfileId(...)``isPersistedDraftGenerating(...)` / `isPersistedDraftFailed(...)`:把拼图稳定 ID 与持久化状态判断收在同一 **Seam**
`PlatformEntryFlowShellImpl.tsx` 仍作为 React state 与副作用 **Adapter**:负责写入 `draftGenerationNotices` / `pendingDraftShelfItems`启动生成、刷新后端列表、打开结果页和弹窗;它不再内联 pending shelf row shape、notice key 汇总作品架 runtime state 规则 `PlatformEntryFlowShellImpl.tsx` 仍作为 React state 与副作用 **Adapter**:负责写入 `draftGenerationNotices` / `pendingDraftShelfItems`读取生成 session、启动 ready 草稿试玩、刷新后端列表、打开结果页和弹窗;它不再内联 pending shelf row shape、notice key 汇总作品架 runtime state 和拼图 / 抓大鹅草稿打开优先级
## 约定 ## 约定
@@ -26,6 +27,7 @@
- 拼图作品详情更新只以 `profileId` 匹配回填;大鱼吃小鱼作品详情更新只以 `sourceSessionId` 匹配回填。 - 拼图作品详情更新只以 `profileId` 匹配回填;大鱼吃小鱼作品详情更新只以 `sourceSessionId` 匹配回填。
- 失败 notice 优先级高于持久化 generating且可通过 pending metadata 提供更具体 summary否则回退玩法默认失败摘要。 - 失败 notice 优先级高于持久化 generating且可通过 pending metadata 提供更具体 summary否则回退玩法默认失败摘要。
- 已有封面的拼图草稿即使局部关卡仍在后台生成,也不得被整卡遮罩为不可打开的生成中状态。 - 已有封面的拼图草稿即使局部关卡仍在后台生成,也不得被整卡遮罩为不可打开的生成中状态。
- 拼图 / 抓大鹅草稿打开 intent 只返回纯计划与 notice keys不创建失败生成态、不请求详情、不写 stage这些仍由壳层 Adapter 执行。
-**Module** 不做网络请求、路由切换、弹窗副作用或 React state 写入,只保留纯 **Implementation**,以提高 **Depth**、**Leverage** 与 **Locality** -**Module** 不做网络请求、路由切换、弹窗副作用或 React state 写入,只保留纯 **Implementation**,以提高 **Depth**、**Leverage** 与 **Locality**
## 验证 ## 验证

View File

@@ -349,8 +349,6 @@ import {
buildCreationWorkShelfItems, buildCreationWorkShelfItems,
type CreationWorkShelfItem, type CreationWorkShelfItem,
isPersistedBarkBattleDraftGenerating, isPersistedBarkBattleDraftGenerating,
isPersistedPuzzleDraftGenerating,
resolvePuzzleWorkCoverImageSrc,
} from '../custom-world-home/creationWorkShelf'; } from '../custom-world-home/creationWorkShelf';
import { import {
buildPlatformRecommendFeedEntries, buildPlatformRecommendFeedEntries,
@@ -437,7 +435,6 @@ import {
import { import {
buildCreationWorkShelfRuntimeState, buildCreationWorkShelfRuntimeState,
buildDraftCompletionDialogSource, buildDraftCompletionDialogSource,
buildDraftFailedShelfSummary,
buildPendingBarkBattleWorks, buildPendingBarkBattleWorks,
buildPendingBigFishWorks, buildPendingBigFishWorks,
buildPendingJumpHopWorks, buildPendingJumpHopWorks,
@@ -451,19 +448,17 @@ import {
createPendingDraftShelfState, createPendingDraftShelfState,
type DraftGenerationNoticeMap, type DraftGenerationNoticeMap,
type DraftGenerationNoticeStatus, type DraftGenerationNoticeStatus,
getDraftGenerationNotice,
getGenerationNoticeShelfKeys, getGenerationNoticeShelfKeys,
hasDraftGenerationNoticeStatus, hasDraftGenerationNoticeStatus,
hasUnreadDraftGenerationUpdates, hasUnreadDraftGenerationUpdates,
hasUnreadReadyDraftGenerationNotice,
isPersistedDraftFailed,
isPersistedDraftGenerating,
mergeBigFishWorkSummary, mergeBigFishWorkSummary,
mergePuzzleWorkSummary, mergePuzzleWorkSummary,
normalizeDraftNoticeId, normalizeDraftNoticeId,
type PendingDraftShelfKind, type PendingDraftShelfKind,
type PendingDraftShelfMap, type PendingDraftShelfMap,
type PendingDraftShelfMetadata, type PendingDraftShelfMetadata,
resolveMatch3DDraftOpenIntent,
resolvePuzzleDraftOpenIntent,
} from './platformDraftGenerationShelfModel'; } from './platformDraftGenerationShelfModel';
import { import {
canExposePublicWork, canExposePublicWork,
@@ -2012,26 +2007,11 @@ export function PlatformEntryFlowShellImpl({
activePuzzleGenerationSessionIdRef.current === sessionId activePuzzleGenerationSessionIdRef.current === sessionId
); );
}, []); }, []);
const isDraftNoticeGenerating = useCallback(
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) =>
hasDraftGenerationNoticeStatus(
draftGenerationNotices,
kind,
ids,
'generating',
),
[draftGenerationNotices],
);
const isDraftNoticeFailed = useCallback( const isDraftNoticeFailed = useCallback(
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) => (kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) =>
hasDraftGenerationNoticeStatus(draftGenerationNotices, kind, ids, 'failed'), hasDraftGenerationNoticeStatus(draftGenerationNotices, kind, ids, 'failed'),
[draftGenerationNotices], [draftGenerationNotices],
); );
const isDraftNoticeReadyUnread = useCallback(
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) =>
hasUnreadReadyDraftGenerationNotice(draftGenerationNotices, kind, ids),
[draftGenerationNotices],
);
const ensureEnoughDraftGenerationPointsFromServer = useCallback( const ensureEnoughDraftGenerationPointsFromServer = useCallback(
async (pointsCost: number) => { async (pointsCost: number) => {
try { try {
@@ -10221,83 +10201,67 @@ export function PlatformEntryFlowShellImpl({
const openPuzzleDraft = useCallback( const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => { async (item: PuzzleWorkSummary) => {
const noticeKeys = collectDraftNoticeKeys('puzzle', [ const sourceSessionId = item.sourceSessionId?.trim() ?? '';
item.workId, const backgroundTask = sourceSessionId
item.profileId, ? getPuzzleBackgroundCompileTask(sourceSessionId)
item.sourceSessionId, : null;
buildPuzzleResultWorkId(item.sourceSessionId), const activeGenerationState =
buildPuzzleResultProfileId(item.sourceSessionId), backgroundTask?.generationState ?? puzzleGenerationViewState;
]); const openIntent = resolvePuzzleDraftOpenIntent({
const failedNotice = getDraftGenerationNotice( item,
draftGenerationNotices, notices: draftGenerationNotices,
noticeKeys, generation: {
); activeSessionId: puzzleSession?.sessionId,
const isPersistedFailed = isPersistedDraftFailed(item.generationStatus); hasActiveGenerationFailure:
const hasGeneratingNotice = isDraftNoticeGenerating('puzzle', [ activeGenerationState?.phase === 'failed',
item.workId, hasActiveGenerationRunning: isMiniGameDraftGenerating(
item.profileId, activeGenerationState ?? null,
item.sourceSessionId, ),
buildPuzzleResultWorkId(item.sourceSessionId), hasBackgroundGenerationFailure:
buildPuzzleResultProfileId(item.sourceSessionId), backgroundTask?.generationState.phase === 'failed',
]); hasBackgroundGenerationRunning: isMiniGameDraftGenerating(
const hasFailedNotice = isDraftNoticeFailed('puzzle', [ backgroundTask?.generationState ?? null,
item.workId, ),
item.profileId, },
item.sourceSessionId, });
buildPuzzleResultWorkId(item.sourceSessionId), const { noticeKeys } = openIntent;
buildPuzzleResultProfileId(item.sourceSessionId),
]);
const noticeErrorMessage =
failedNotice?.status === 'failed'
? (failedNotice.message ?? buildDraftFailedShelfSummary('puzzle'))
: buildDraftFailedShelfSummary('puzzle');
const isMarkedGenerating =
!hasFailedNotice &&
((hasGeneratingNotice && !resolvePuzzleWorkCoverImageSrc(item)) ||
isPersistedPuzzleDraftGenerating(item));
setPuzzleOperation(null); setPuzzleOperation(null);
setPuzzleRun(null); setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default'); setPuzzleRuntimeAuthMode('default');
setSelectedPuzzleDetail(null); setSelectedPuzzleDetail(null);
if (!item.sourceSessionId?.trim()) {
if (item.publicationStatus === 'published') {
await openPuzzleDetail(item.profileId, { tab: 'create' });
return;
}
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。'); if (openIntent.type === 'open-published-detail') {
await openPuzzleDetail(item.profileId, { tab: 'create' });
return; return;
} }
const backgroundTask = getPuzzleBackgroundCompileTask( if (openIntent.type === 'missing-session') {
item.sourceSessionId, setPuzzleError(openIntent.errorMessage);
); return;
const activeGenerationState = }
backgroundTask?.generationState ?? puzzleGenerationViewState;
const failedGenerationState = if (openIntent.type === 'failed-generation') {
backgroundTask?.generationState.phase === 'failed' const failedGenerationState =
? backgroundTask.generationState openIntent.source === 'background'
: item.sourceSessionId === puzzleSession?.sessionId && ? backgroundTask?.generationState
activeGenerationState?.phase === 'failed' : openIntent.source === 'active'
? activeGenerationState ? activeGenerationState
: hasFailedNotice || isPersistedFailed : createFailedMiniGameDraftGenerationStateForRestoredDraft(
? createFailedMiniGameDraftGenerationStateForRestoredDraft(
'puzzle', 'puzzle',
item.updatedAt, item.updatedAt,
noticeErrorMessage, openIntent.errorMessage,
{ puzzleAiRedraw: true }, { puzzleAiRedraw: true },
) );
: null; if (!failedGenerationState) {
return;
if ((hasFailedNotice || isPersistedFailed) && failedGenerationState) { }
let failedSession = backgroundTask?.session ?? null; let failedSession = backgroundTask?.session ?? null;
let failedPayload = backgroundTask?.payload ?? null; let failedPayload = backgroundTask?.payload ?? null;
const failedError = const failedError = backgroundTask?.error ?? openIntent.errorMessage;
backgroundTask?.error ?? failedNotice?.message ?? noticeErrorMessage;
if (!failedSession) { if (!failedSession) {
try { try {
const { session: latestSession } = await getPuzzleAgentSession( const { session: latestSession } = await getPuzzleAgentSession(
item.sourceSessionId, sourceSessionId,
); );
failedSession = latestSession; failedSession = latestSession;
failedPayload = buildPuzzleFormPayloadFromSession(latestSession); failedPayload = buildPuzzleFormPayloadFromSession(latestSession);
@@ -10330,16 +10294,13 @@ export function PlatformEntryFlowShellImpl({
} }
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'puzzle-generating'; selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId; activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setPuzzleGenerationState(failedGenerationState); setPuzzleGenerationState(failedGenerationState);
setSelectionStage('puzzle-generating'); setSelectionStage('puzzle-generating');
return; return;
} }
if ( if (openIntent.type === 'active-generation') {
item.sourceSessionId === puzzleSession?.sessionId &&
isMiniGameDraftGenerating(activeGenerationState)
) {
if (!activeGenerationState) { if (!activeGenerationState) {
return; return;
} }
@@ -10347,7 +10308,7 @@ export function PlatformEntryFlowShellImpl({
rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState); rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState);
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'puzzle-generating'; selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId; activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setPuzzleGenerationState(rebasedGenerationState); setPuzzleGenerationState(rebasedGenerationState);
if (backgroundTask) { if (backgroundTask) {
setPuzzleBackgroundCompileTasks((current) => ({ setPuzzleBackgroundCompileTasks((current) => ({
@@ -10362,10 +10323,7 @@ export function PlatformEntryFlowShellImpl({
return; return;
} }
if ( if (openIntent.type === 'background-generation' && backgroundTask) {
backgroundTask &&
isMiniGameDraftGenerating(backgroundTask.generationState)
) {
const rebasedTask = const rebasedTask =
rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask); rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask);
puzzleFlow.setSession(rebasedTask.session); puzzleFlow.setSession(rebasedTask.session);
@@ -10380,15 +10338,15 @@ export function PlatformEntryFlowShellImpl({
} }
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'puzzle-generating'; selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId; activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('puzzle-generating'); setSelectionStage('puzzle-generating');
return; return;
} }
if (isMarkedGenerating) { if (openIntent.type === 'restore-generating') {
try { try {
const { session: latestSession } = await getPuzzleAgentSession( const { session: latestSession } = await getPuzzleAgentSession(
item.sourceSessionId, sourceSessionId,
); );
const payload = buildPuzzleFormPayloadFromSession(latestSession); const payload = buildPuzzleFormPayloadFromSession(latestSession);
const startedAtMs = resolveMiniGameDraftGenerationStartedAtMs( const startedAtMs = resolveMiniGameDraftGenerationStartedAtMs(
@@ -10424,7 +10382,7 @@ export function PlatformEntryFlowShellImpl({
})); }));
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'puzzle-generating'; selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = item.sourceSessionId; activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('puzzle-generating'); setSelectionStage('puzzle-generating');
return; return;
} catch (error) { } catch (error) {
@@ -10439,7 +10397,7 @@ export function PlatformEntryFlowShellImpl({
markDraftNoticeSeen(noticeKeys); markDraftNoticeSeen(noticeKeys);
const restoredSession = await puzzleFlow.restoreDraft( const restoredSession = await puzzleFlow.restoreDraft(
item.sourceSessionId, sourceSessionId,
); );
if (!restoredSession) { if (!restoredSession) {
await refreshPuzzleShelf().catch(() => undefined); await refreshPuzzleShelf().catch(() => undefined);
@@ -10459,8 +10417,6 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab, enterCreateTab,
draftGenerationNotices, draftGenerationNotices,
getPuzzleBackgroundCompileTask, getPuzzleBackgroundCompileTask,
isDraftNoticeFailed,
isDraftNoticeGenerating,
markDraftNoticeSeen, markDraftNoticeSeen,
openPuzzleDetail, openPuzzleDetail,
puzzleFlow, puzzleFlow,
@@ -10478,78 +10434,52 @@ export function PlatformEntryFlowShellImpl({
item: Match3DWorkSummary, item: Match3DWorkSummary,
options: { forceDraft?: boolean } = {}, options: { forceDraft?: boolean } = {},
) => { ) => {
const noticeKeys = collectDraftNoticeKeys('match3d', [ const sourceSessionId = item.sourceSessionId?.trim() ?? '';
item.workId, const backgroundTask = sourceSessionId
item.profileId, ? getMatch3DBackgroundCompileTask(sourceSessionId)
item.sourceSessionId, : null;
]); const activeGenerationState =
const hasUnreadReadyNotice = isDraftNoticeReadyUnread('match3d', [ backgroundTask?.generationState ?? match3dGenerationViewState;
item.workId, const openIntent = resolveMatch3DDraftOpenIntent({
item.profileId, item,
item.sourceSessionId, notices: draftGenerationNotices,
]); forceDraft: options.forceDraft,
generation: {
activeSessionId: match3dSession?.sessionId,
hasActiveGenerationFailure:
activeGenerationState?.phase === 'failed',
hasActiveGenerationRunning: isMiniGameDraftGenerating(
activeGenerationState ?? null,
),
hasBackgroundGenerationFailure:
backgroundTask?.generationState.phase === 'failed',
hasBackgroundGenerationRunning: isMiniGameDraftGenerating(
backgroundTask?.generationState ?? null,
),
},
});
const { noticeKeys } = openIntent;
setMatch3DRun(null); setMatch3DRun(null);
setMatch3DError(null); setMatch3DError(null);
setMatch3DProfile(null); setMatch3DProfile(null);
setMatch3DRuntimeProfile(null); setMatch3DRuntimeProfile(null);
if (item.publicationStatus === 'published' && !options.forceDraft) { if (openIntent.type === 'open-published-detail') {
markDraftNoticeSeen(noticeKeys); markDraftNoticeSeen(noticeKeys);
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(item)); openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(item));
return; return;
} }
if (!item.sourceSessionId?.trim()) { if (openIntent.type === 'missing-session') {
markDraftNoticeSeen(noticeKeys); markDraftNoticeSeen(noticeKeys);
setMatch3DError('这份抓大鹅草稿缺少会话信息,请重新开始创作。'); setMatch3DError(openIntent.errorMessage);
return; return;
} }
const failedNotice = getDraftGenerationNotice( if (openIntent.type === 'ready-unread') {
draftGenerationNotices,
noticeKeys,
);
const hasFailedNotice = isDraftNoticeFailed('match3d', [
item.workId,
item.profileId,
item.sourceSessionId,
]);
const noticeErrorMessage =
failedNotice?.status === 'failed'
? (failedNotice.message ?? buildDraftFailedShelfSummary('match3d'))
: buildDraftFailedShelfSummary('match3d');
const isMarkedGenerating =
!hasFailedNotice &&
(isDraftNoticeGenerating('match3d', [
item.workId,
item.profileId,
item.sourceSessionId,
]) ||
isPersistedDraftGenerating(item.generationStatus));
const backgroundTask = getMatch3DBackgroundCompileTask(
item.sourceSessionId,
);
const activeGenerationState =
backgroundTask?.generationState ?? match3dGenerationViewState;
const failedGenerationState =
backgroundTask?.generationState.phase === 'failed'
? backgroundTask.generationState
: item.sourceSessionId === match3dSession?.sessionId &&
activeGenerationState?.phase === 'failed'
? activeGenerationState
: hasFailedNotice
? createFailedMiniGameDraftGenerationStateForRestoredDraft(
'match3d',
item.updatedAt,
noticeErrorMessage,
)
: null;
if (hasUnreadReadyNotice) {
try { try {
const { session: latestSession } = const { session: latestSession } =
await match3dCreationClient.getSession(item.sourceSessionId); await match3dCreationClient.getSession(sourceSessionId);
setMatch3DSession(latestSession); setMatch3DSession(latestSession);
setMatch3DFormDraftPayload(null); setMatch3DFormDraftPayload(null);
const profileId = latestSession.draft?.profileId ?? item.profileId; const profileId = latestSession.draft?.profileId ?? item.profileId;
@@ -10577,15 +10507,27 @@ export function PlatformEntryFlowShellImpl({
} }
} }
if (failedGenerationState) { if (openIntent.type === 'failed-generation') {
const failedGenerationState =
openIntent.source === 'background'
? backgroundTask?.generationState
: openIntent.source === 'active'
? activeGenerationState
: createFailedMiniGameDraftGenerationStateForRestoredDraft(
'match3d',
item.updatedAt,
openIntent.errorMessage,
);
if (!failedGenerationState) {
return;
}
let failedSession = backgroundTask?.session ?? null; let failedSession = backgroundTask?.session ?? null;
let failedPayload = backgroundTask?.payload ?? null; let failedPayload = backgroundTask?.payload ?? null;
const failedError = const failedError = backgroundTask?.error ?? openIntent.errorMessage;
backgroundTask?.error ?? failedNotice?.message ?? noticeErrorMessage;
if (!failedSession) { if (!failedSession) {
try { try {
const { session: latestSession } = const { session: latestSession } =
await match3dCreationClient.getSession(item.sourceSessionId); await match3dCreationClient.getSession(sourceSessionId);
failedSession = latestSession; failedSession = latestSession;
failedPayload = buildMatch3DFormPayloadFromSession(latestSession); failedPayload = buildMatch3DFormPayloadFromSession(latestSession);
} catch { } catch {
@@ -10617,16 +10559,13 @@ export function PlatformEntryFlowShellImpl({
} }
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'match3d-generating'; selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = item.sourceSessionId; activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setMatch3DGenerationState(failedGenerationState); setMatch3DGenerationState(failedGenerationState);
setSelectionStage('match3d-generating'); setSelectionStage('match3d-generating');
return; return;
} }
if ( if (openIntent.type === 'active-generation') {
item.sourceSessionId === match3dSession?.sessionId &&
isMiniGameDraftGenerating(activeGenerationState)
) {
if (!activeGenerationState) { if (!activeGenerationState) {
return; return;
} }
@@ -10634,7 +10573,7 @@ export function PlatformEntryFlowShellImpl({
rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState); rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState);
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'match3d-generating'; selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = item.sourceSessionId; activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setMatch3DGenerationState(rebasedGenerationState); setMatch3DGenerationState(rebasedGenerationState);
if (backgroundTask) { if (backgroundTask) {
setMatch3DBackgroundCompileTasks((current) => ({ setMatch3DBackgroundCompileTasks((current) => ({
@@ -10649,10 +10588,7 @@ export function PlatformEntryFlowShellImpl({
return; return;
} }
if ( if (openIntent.type === 'background-generation' && backgroundTask) {
backgroundTask &&
isMiniGameDraftGenerating(backgroundTask.generationState)
) {
const rebasedTask = const rebasedTask =
rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask); rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask);
setMatch3DSession(rebasedTask.session); setMatch3DSession(rebasedTask.session);
@@ -10667,15 +10603,15 @@ export function PlatformEntryFlowShellImpl({
} }
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'match3d-generating'; selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = item.sourceSessionId; activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('match3d-generating'); setSelectionStage('match3d-generating');
return; return;
} }
if (isMarkedGenerating) { if (openIntent.type === 'restore-generating') {
try { try {
const { session: latestSession } = const { session: latestSession } =
await match3dCreationClient.getSession(item.sourceSessionId); await match3dCreationClient.getSession(sourceSessionId);
setMatch3DSession(latestSession); setMatch3DSession(latestSession);
setMatch3DFormDraftPayload(null); setMatch3DFormDraftPayload(null);
setMatch3DProfile(null); setMatch3DProfile(null);
@@ -10690,7 +10626,7 @@ export function PlatformEntryFlowShellImpl({
setMatch3DGenerationState(generationState); setMatch3DGenerationState(generationState);
enterCreateTab(); enterCreateTab();
selectionStageRef.current = 'match3d-generating'; selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = item.sourceSessionId; activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('match3d-generating'); setSelectionStage('match3d-generating');
return; return;
} catch (error) { } catch (error) {
@@ -10705,7 +10641,7 @@ export function PlatformEntryFlowShellImpl({
markDraftNoticeSeen(noticeKeys); markDraftNoticeSeen(noticeKeys);
const restoredSession = await match3dFlow.restoreDraft( const restoredSession = await match3dFlow.restoreDraft(
item.sourceSessionId, sourceSessionId,
); );
if (!restoredSession) { if (!restoredSession) {
await refreshMatch3DShelf().catch(() => undefined); await refreshMatch3DShelf().catch(() => undefined);
@@ -10729,9 +10665,6 @@ export function PlatformEntryFlowShellImpl({
enterCreateTab, enterCreateTab,
draftGenerationNotices, draftGenerationNotices,
getMatch3DBackgroundCompileTask, getMatch3DBackgroundCompileTask,
isDraftNoticeFailed,
isDraftNoticeGenerating,
isDraftNoticeReadyUnread,
markDraftNoticeSeen, markDraftNoticeSeen,
match3dFlow, match3dFlow,
match3dGenerationViewState, match3dGenerationViewState,

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf'; import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
import { import {
@@ -15,9 +16,140 @@ import {
hasUnreadDraftGenerationUpdates, hasUnreadDraftGenerationUpdates,
mergeBigFishWorkSummary, mergeBigFishWorkSummary,
mergePuzzleWorkSummary, mergePuzzleWorkSummary,
resolveMatch3DDraftOpenIntent,
resolvePuzzleDraftOpenIntent,
} from './platformDraftGenerationShelfModel'; } from './platformDraftGenerationShelfModel';
describe('platformDraftGenerationShelfModel', () => { describe('platformDraftGenerationShelfModel', () => {
test('resolvePuzzleDraftOpenIntent sends published puzzle without session to detail', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
sourceSessionId: null,
publicationStatus: 'published',
}),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'open-published-detail',
});
});
test('resolvePuzzleDraftOpenIntent restores failed puzzle generation with notice copy', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork(),
notices: {
'puzzle:puzzle-session-base': {
status: 'failed',
seen: false,
message: '首图生成失败。',
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'failed-generation',
source: 'restored',
errorMessage: '首图生成失败。',
});
});
test('resolvePuzzleDraftOpenIntent prefers active generation before restoring draft', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork(),
notices: {},
generation: emptyGenerationFacts({
activeSessionId: 'puzzle-session-base',
hasActiveGenerationRunning: true,
}),
}),
).toMatchObject({
type: 'active-generation',
});
});
test('resolvePuzzleDraftOpenIntent does not lock a puzzle draft that already has a cover', () => {
expect(
resolvePuzzleDraftOpenIntent({
item: buildPuzzleWork({
coverImageSrc: '/media/puzzle-cover.png',
}),
notices: {
'puzzle:puzzle-session-base': {
status: 'generating',
seen: false,
},
},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-draft',
});
});
test('resolveMatch3DDraftOpenIntent opens published work detail unless forced into draft', () => {
const item = buildMatch3DWork({
publicationStatus: 'published',
});
expect(
resolveMatch3DDraftOpenIntent({
item,
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'open-published-detail',
});
expect(
resolveMatch3DDraftOpenIntent({
item,
notices: {},
forceDraft: true,
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-draft',
});
});
test('resolveMatch3DDraftOpenIntent starts ready unread draft before failure fallback', () => {
expect(
resolveMatch3DDraftOpenIntent({
item: buildMatch3DWork(),
notices: {
'match3d:match3d-session-base': {
status: 'ready',
seen: false,
},
},
generation: emptyGenerationFacts({
hasBackgroundGenerationFailure: true,
}),
}),
).toMatchObject({
type: 'ready-unread',
});
});
test('resolveMatch3DDraftOpenIntent restores persisted generating draft', () => {
expect(
resolveMatch3DDraftOpenIntent({
item: buildMatch3DWork({
generationStatus: 'generating',
}),
notices: {},
generation: emptyGenerationFacts(),
}),
).toMatchObject({
type: 'restore-generating',
});
});
test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => { test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => {
const pending = buildPendingPuzzleWorks( const pending = buildPendingPuzzleWorks(
{ {
@@ -199,6 +331,19 @@ describe('platformDraftGenerationShelfModel', () => {
}); });
}); });
function emptyGenerationFacts(
overrides: Partial<Parameters<typeof resolvePuzzleDraftOpenIntent>[0]['generation']> = {},
): Parameters<typeof resolvePuzzleDraftOpenIntent>[0]['generation'] {
return {
activeSessionId: null,
hasActiveGenerationFailure: false,
hasActiveGenerationRunning: false,
hasBackgroundGenerationFailure: false,
hasBackgroundGenerationRunning: false,
...overrides,
};
}
function buildPuzzleWork( function buildPuzzleWork(
overrides: Partial<PuzzleWorkSummary> = {}, overrides: Partial<PuzzleWorkSummary> = {},
): PuzzleWorkSummary { ): PuzzleWorkSummary {
@@ -227,6 +372,33 @@ function buildPuzzleWork(
}; };
} }
function buildMatch3DWork(
overrides: Partial<Match3DWorkSummary> = {},
): Match3DWorkSummary {
return {
workId: 'match3d-work-base',
profileId: 'match3d-profile-base',
ownerUserId: 'user-1',
sourceSessionId: 'match3d-session-base',
gameName: '潮雾抓大鹅',
themeText: '潮雾港口',
summary: '潮雾港口抓大鹅。',
tags: [],
coverImageSrc: null,
referenceImageSrc: null,
clearCount: 0,
difficulty: 1,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-06-03T08:00:00.000Z',
publishedAt: null,
publishReady: false,
generationStatus: 'ready',
generatedItemAssets: [],
...overrides,
};
}
function buildBigFishWork( function buildBigFishWork(
overrides: Partial<BigFishWorkSummary> = {}, overrides: Partial<BigFishWorkSummary> = {},
): BigFishWorkSummary { ): BigFishWorkSummary {

View File

@@ -12,6 +12,7 @@ import {
type CreationWorkShelfItem, type CreationWorkShelfItem,
type CreationWorkShelfKind, type CreationWorkShelfKind,
type CreationWorkShelfRuntimeState, type CreationWorkShelfRuntimeState,
isPersistedPuzzleDraftGenerating,
resolvePuzzleWorkCoverImageSrc, resolvePuzzleWorkCoverImageSrc,
} from '../custom-world-home/creationWorkShelf'; } from '../custom-world-home/creationWorkShelf';
import { import {
@@ -67,6 +68,86 @@ export type PlatformDraftGenerationVisibleShelfSources = {
babyObjectMatchItems: readonly BabyObjectMatchDraft[]; babyObjectMatchItems: readonly BabyObjectMatchDraft[];
}; };
type DraftOpenGenerationFacts = {
activeSessionId?: string | null;
hasActiveGenerationFailure: boolean;
hasActiveGenerationRunning: boolean;
hasBackgroundGenerationFailure: boolean;
hasBackgroundGenerationRunning: boolean;
};
type FailedDraftGenerationSource = 'background' | 'active' | 'restored';
export type PuzzleDraftOpenIntent =
| {
type: 'open-published-detail';
noticeKeys: string[];
}
| {
type: 'missing-session';
noticeKeys: string[];
errorMessage: string;
}
| {
type: 'failed-generation';
noticeKeys: string[];
errorMessage: string;
source: FailedDraftGenerationSource;
}
| {
type: 'active-generation';
noticeKeys: string[];
}
| {
type: 'background-generation';
noticeKeys: string[];
}
| {
type: 'restore-generating';
noticeKeys: string[];
}
| {
type: 'restore-draft';
noticeKeys: string[];
};
export type Match3DDraftOpenIntent =
| {
type: 'open-published-detail';
noticeKeys: string[];
}
| {
type: 'missing-session';
noticeKeys: string[];
errorMessage: string;
}
| {
type: 'ready-unread';
noticeKeys: string[];
}
| {
type: 'failed-generation';
noticeKeys: string[];
errorMessage: string;
source: FailedDraftGenerationSource;
}
| {
type: 'active-generation';
noticeKeys: string[];
}
| {
type: 'background-generation';
noticeKeys: string[];
}
| {
type: 'restore-generating';
noticeKeys: string[];
}
| {
type: 'restore-draft';
noticeKeys: string[];
};
export function buildDraftNoticeKey( export function buildDraftNoticeKey(
kind: CreationWorkShelfKind, kind: CreationWorkShelfKind,
id: string, id: string,
@@ -334,6 +415,219 @@ export function hasUnreadReadyDraftGenerationNotice(
}); });
} }
export function buildPuzzleDraftOpenNoticeKeys(item: PuzzleWorkSummary) {
return collectDraftNoticeKeys('puzzle', [
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
]);
}
export function buildMatch3DDraftOpenNoticeKeys(item: Match3DWorkSummary) {
return collectDraftNoticeKeys('match3d', [
item.workId,
item.profileId,
item.sourceSessionId,
]);
}
export function resolvePuzzleDraftOpenIntent(params: {
item: PuzzleWorkSummary;
notices: DraftGenerationNoticeMap;
generation: DraftOpenGenerationFacts;
}): PuzzleDraftOpenIntent {
const { item, notices, generation } = params;
const noticeKeys = buildPuzzleDraftOpenNoticeKeys(item);
const sourceSessionId = normalizeDraftNoticeId(item.sourceSessionId);
if (!sourceSessionId) {
if (item.publicationStatus === 'published') {
return { type: 'open-published-detail', noticeKeys };
}
return {
type: 'missing-session',
noticeKeys,
errorMessage: '这份拼图草稿缺少会话信息,请重新开始创作。',
};
}
const failedNotice = getDraftGenerationNotice(notices, noticeKeys);
const hasFailedNotice = hasDraftGenerationNoticeStatus(
notices,
'puzzle',
[
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
],
'failed',
);
const hasGeneratingNotice = hasDraftGenerationNoticeStatus(
notices,
'puzzle',
[
item.workId,
item.profileId,
item.sourceSessionId,
buildPuzzleResultWorkId(item.sourceSessionId),
buildPuzzleResultProfileId(item.sourceSessionId),
],
'generating',
);
const noticeErrorMessage =
failedNotice?.status === 'failed'
? (failedNotice.message ?? buildDraftFailedShelfSummary('puzzle'))
: buildDraftFailedShelfSummary('puzzle');
const isCurrentSession =
sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId);
if (generation.hasBackgroundGenerationFailure) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'background',
};
}
if (isCurrentSession && generation.hasActiveGenerationFailure) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'active',
};
}
if (hasFailedNotice || isPersistedDraftFailed(item.generationStatus)) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'restored',
};
}
if (isCurrentSession && generation.hasActiveGenerationRunning) {
return { type: 'active-generation', noticeKeys };
}
if (generation.hasBackgroundGenerationRunning) {
return { type: 'background-generation', noticeKeys };
}
const isMarkedGenerating =
!hasFailedNotice &&
((hasGeneratingNotice && !resolvePuzzleWorkCoverImageSrc(item)) ||
isPersistedPuzzleDraftGenerating(item));
if (isMarkedGenerating) {
return { type: 'restore-generating', noticeKeys };
}
return { type: 'restore-draft', noticeKeys };
}
export function resolveMatch3DDraftOpenIntent(params: {
item: Match3DWorkSummary;
notices: DraftGenerationNoticeMap;
forceDraft?: boolean;
generation: DraftOpenGenerationFacts;
}): Match3DDraftOpenIntent {
const { item, notices, forceDraft = false, generation } = params;
const noticeKeys = buildMatch3DDraftOpenNoticeKeys(item);
if (item.publicationStatus === 'published' && !forceDraft) {
return { type: 'open-published-detail', noticeKeys };
}
const sourceSessionId = normalizeDraftNoticeId(item.sourceSessionId);
if (!sourceSessionId) {
return {
type: 'missing-session',
noticeKeys,
errorMessage: '这份抓大鹅草稿缺少会话信息,请重新开始创作。',
};
}
if (
hasUnreadReadyDraftGenerationNotice(notices, 'match3d', [
item.workId,
item.profileId,
item.sourceSessionId,
])
) {
return { type: 'ready-unread', noticeKeys };
}
const failedNotice = getDraftGenerationNotice(notices, noticeKeys);
const hasFailedNotice = hasDraftGenerationNoticeStatus(
notices,
'match3d',
[item.workId, item.profileId, item.sourceSessionId],
'failed',
);
const noticeErrorMessage =
failedNotice?.status === 'failed'
? (failedNotice.message ?? buildDraftFailedShelfSummary('match3d'))
: buildDraftFailedShelfSummary('match3d');
const isCurrentSession =
sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId);
if (generation.hasBackgroundGenerationFailure) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'background',
};
}
if (isCurrentSession && generation.hasActiveGenerationFailure) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'active',
};
}
if (hasFailedNotice) {
return {
type: 'failed-generation',
noticeKeys,
errorMessage: noticeErrorMessage,
source: 'restored',
};
}
if (isCurrentSession && generation.hasActiveGenerationRunning) {
return { type: 'active-generation', noticeKeys };
}
if (generation.hasBackgroundGenerationRunning) {
return { type: 'background-generation', noticeKeys };
}
if (
hasDraftGenerationNoticeStatus(
notices,
'match3d',
[item.workId, item.profileId, item.sourceSessionId],
'generating',
) ||
isPersistedDraftGenerating(item.generationStatus)
) {
return { type: 'restore-generating', noticeKeys };
}
return { type: 'restore-draft', noticeKeys };
}
export function buildCreationWorkShelfRuntimeState(params: { export function buildCreationWorkShelfRuntimeState(params: {
item: CreationWorkShelfItem; item: CreationWorkShelfItem;
notices: DraftGenerationNoticeMap; notices: DraftGenerationNoticeMap;