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 草稿状态收口
- 背景:`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)。
平台入口创作生成通知、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)。

View File

@@ -15,9 +15,10 @@
- `buildCreationWorkShelfRuntimeState({ item, notices, pendingShelfItems })`:统一输出 `CreationWorkShelfRuntimeState`,处理失败覆盖、拼图空标题 `拼图草稿` 兜底、summary 占位覆盖、生成中遮罩和 ready 未读点。
- `collectVisibleDraftNoticeKeys(...)` / `hasUnreadDraftGenerationUpdates(...)`:统一草稿 Tab 顶部未读点规则。
- `mergePuzzleWorkSummary(current, updated)``mergeBigFishWorkSummary(current, updated)`:统一作品详情更新后回填作品架和当前详情的身份匹配规则。
- `resolvePuzzleDraftOpenIntent(...)``resolveMatch3DDraftOpenIntent(...)`:统一拼图 / 抓大鹅草稿打开时的已发布详情、缺 session、ready 未读试玩、失败生成页、active / background 生成页、持久化 generating 恢复和普通草稿恢复优先级。
- `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` 匹配回填。
- 失败 notice 优先级高于持久化 generating且可通过 pending metadata 提供更具体 summary否则回退玩法默认失败摘要。
- 已有封面的拼图草稿即使局部关卡仍在后台生成,也不得被整卡遮罩为不可打开的生成中状态。
- 拼图 / 抓大鹅草稿打开 intent 只返回纯计划与 notice keys不创建失败生成态、不请求详情、不写 stage这些仍由壳层 Adapter 执行。
-**Module** 不做网络请求、路由切换、弹窗副作用或 React state 写入,只保留纯 **Implementation**,以提高 **Depth**、**Leverage** 与 **Locality**
## 验证

View File

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

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from 'vitest';
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 { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
import {
@@ -15,9 +16,140 @@ import {
hasUnreadDraftGenerationUpdates,
mergeBigFishWorkSummary,
mergePuzzleWorkSummary,
resolveMatch3DDraftOpenIntent,
resolvePuzzleDraftOpenIntent,
} from './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', () => {
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(
overrides: Partial<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(
overrides: Partial<BigFishWorkSummary> = {},
): BigFishWorkSummary {

View File

@@ -12,6 +12,7 @@ import {
type CreationWorkShelfItem,
type CreationWorkShelfKind,
type CreationWorkShelfRuntimeState,
isPersistedPuzzleDraftGenerating,
resolvePuzzleWorkCoverImageSrc,
} from '../custom-world-home/creationWorkShelf';
import {
@@ -67,6 +68,86 @@ export type PlatformDraftGenerationVisibleShelfSources = {
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(
kind: CreationWorkShelfKind,
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: {
item: CreationWorkShelfItem;
notices: DraftGenerationNoticeMap;