refactor: 补齐草稿与SSE收口
This commit is contained in:
@@ -439,7 +439,6 @@ import {
|
||||
type DraftGenerationNoticeMap,
|
||||
type DraftGenerationNoticeStatus,
|
||||
getGenerationNoticeShelfKeys,
|
||||
hasDraftGenerationNoticeStatus,
|
||||
hasUnreadDraftGenerationUpdates,
|
||||
mergeBigFishWorkSummary,
|
||||
mergePuzzleWorkSummary,
|
||||
@@ -448,10 +447,12 @@ import {
|
||||
type PendingDraftShelfMap,
|
||||
type PendingDraftShelfMetadata,
|
||||
resolveBigFishDraftOpenIntent,
|
||||
resolveJumpHopDraftOpenIntent,
|
||||
resolveMatch3DDraftOpenIntent,
|
||||
resolvePuzzleDraftOpenIntent,
|
||||
resolveSquareHoleDraftOpenIntent,
|
||||
resolveVisualNovelDraftOpenIntent,
|
||||
resolveWoodenFishDraftOpenIntent,
|
||||
} from './platformDraftGenerationShelfModel';
|
||||
import {
|
||||
canExposePublicWork,
|
||||
@@ -2010,11 +2011,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
activePuzzleGenerationSessionIdRef.current === sessionId
|
||||
);
|
||||
}, []);
|
||||
const isDraftNoticeFailed = useCallback(
|
||||
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) =>
|
||||
hasDraftGenerationNoticeStatus(draftGenerationNotices, kind, ids, 'failed'),
|
||||
[draftGenerationNotices],
|
||||
);
|
||||
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
|
||||
async (pointsCost: number) => {
|
||||
try {
|
||||
@@ -9920,12 +9916,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const openJumpHopDraft = useCallback(
|
||||
async (item: JumpHopWorkSummaryResponse) => {
|
||||
const noticeIds = [item.workId, item.profileId, item.sourceSessionId];
|
||||
const hasFailedNotice = isDraftNoticeFailed('jump-hop', noticeIds);
|
||||
const sessionId = normalizeCreationUrlValue(item.sourceSessionId);
|
||||
markDraftNoticeSeen(collectDraftNoticeKeys('jump-hop', noticeIds));
|
||||
const openIntent = resolveJumpHopDraftOpenIntent({
|
||||
item,
|
||||
notices: draftGenerationNotices,
|
||||
generation: {
|
||||
activeSessionId: jumpHopSession?.sessionId,
|
||||
hasActiveGenerationFailure: jumpHopGenerationState?.phase === 'failed',
|
||||
},
|
||||
});
|
||||
markDraftNoticeSeen(openIntent.noticeKeys);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
if (openIntent.type === 'open-published-detail') {
|
||||
void openJumpHopPublicWorkDetail(item.profileId);
|
||||
return;
|
||||
}
|
||||
@@ -9933,18 +9934,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setIsJumpHopBusy(true);
|
||||
if (
|
||||
hasFailedNotice &&
|
||||
sessionId === jumpHopSession?.sessionId &&
|
||||
jumpHopGenerationState?.phase === 'failed'
|
||||
) {
|
||||
if (openIntent.type === 'active-failed-generation') {
|
||||
enterCreateTab();
|
||||
setSelectionStage('jump-hop-generating');
|
||||
setIsJumpHopBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.generationStatus === 'generating' && !hasFailedNotice) {
|
||||
if (openIntent.type === 'restore-generating') {
|
||||
const pendingSession = buildJumpHopPendingSession(item);
|
||||
setJumpHopSession(pendingSession);
|
||||
setJumpHopRun(null);
|
||||
@@ -9981,8 +9978,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
draftGenerationNotices,
|
||||
enterCreateTab,
|
||||
isDraftNoticeFailed,
|
||||
jumpHopGenerationState?.phase,
|
||||
jumpHopSession?.sessionId,
|
||||
markDraftNoticeSeen,
|
||||
@@ -10016,13 +10013,18 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const openWoodenFishDraft = useCallback(
|
||||
async (item: WoodenFishWorkSummaryResponse) => {
|
||||
const noticeIds = [item.workId, item.profileId, item.sourceSessionId];
|
||||
const hasFailedNotice = isDraftNoticeFailed('wooden-fish', noticeIds);
|
||||
const sessionId =
|
||||
normalizeCreationUrlValue(item.sourceSessionId) ?? item.profileId;
|
||||
markDraftNoticeSeen(collectDraftNoticeKeys('wooden-fish', noticeIds));
|
||||
const openIntent = resolveWoodenFishDraftOpenIntent({
|
||||
item,
|
||||
notices: draftGenerationNotices,
|
||||
generation: {
|
||||
activeSessionId: woodenFishSession?.sessionId,
|
||||
hasActiveGenerationFailure:
|
||||
woodenFishGenerationState?.phase === 'failed',
|
||||
},
|
||||
});
|
||||
markDraftNoticeSeen(openIntent.noticeKeys);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
if (openIntent.type === 'open-published-detail') {
|
||||
void openWoodenFishPublicWorkDetail(item.profileId);
|
||||
return;
|
||||
}
|
||||
@@ -10030,18 +10032,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setIsWoodenFishBusy(true);
|
||||
if (
|
||||
hasFailedNotice &&
|
||||
sessionId === woodenFishSession?.sessionId &&
|
||||
woodenFishGenerationState?.phase === 'failed'
|
||||
) {
|
||||
if (openIntent.type === 'active-failed-generation') {
|
||||
enterCreateTab();
|
||||
setSelectionStage('wooden-fish-generating');
|
||||
setIsWoodenFishBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.generationStatus === 'generating' && !hasFailedNotice) {
|
||||
if (openIntent.type === 'restore-generating') {
|
||||
const pendingSession = buildWoodenFishPendingSession(item);
|
||||
setWoodenFishSession(pendingSession);
|
||||
setWoodenFishRun(null);
|
||||
@@ -10083,16 +10081,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
resolveRpgCreationErrorMessage(error, '读取敲木鱼草稿失败。'),
|
||||
);
|
||||
enterCreateTab();
|
||||
setSelectionStage(
|
||||
hasFailedNotice ? 'wooden-fish-workspace' : 'wooden-fish-generating',
|
||||
);
|
||||
setSelectionStage(openIntent.failureFallbackStage);
|
||||
} finally {
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
draftGenerationNotices,
|
||||
enterCreateTab,
|
||||
isDraftNoticeFailed,
|
||||
markDraftNoticeSeen,
|
||||
openWoodenFishPublicWorkDetail,
|
||||
woodenFishGenerationState?.phase,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { buildCreationWorkShelfItems } from '../custom-world-home/creationWorkShelf';
|
||||
import {
|
||||
buildCreationWorkShelfRuntimeState,
|
||||
@@ -19,10 +21,12 @@ import {
|
||||
mergeBigFishWorkSummary,
|
||||
mergePuzzleWorkSummary,
|
||||
resolveBigFishDraftOpenIntent,
|
||||
resolveJumpHopDraftOpenIntent,
|
||||
resolveMatch3DDraftOpenIntent,
|
||||
resolvePuzzleDraftOpenIntent,
|
||||
resolveSquareHoleDraftOpenIntent,
|
||||
resolveVisualNovelDraftOpenIntent,
|
||||
resolveWoodenFishDraftOpenIntent,
|
||||
} from './platformDraftGenerationShelfModel';
|
||||
|
||||
describe('platformDraftGenerationShelfModel', () => {
|
||||
@@ -280,6 +284,121 @@ describe('platformDraftGenerationShelfModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveJumpHopDraftOpenIntent handles published, failed current generation, generating and detail states', () => {
|
||||
expect(
|
||||
resolveJumpHopDraftOpenIntent({
|
||||
item: buildJumpHopWork({ publicationStatus: 'published' }),
|
||||
notices: {},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'open-published-detail',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveJumpHopDraftOpenIntent({
|
||||
item: buildJumpHopWork(),
|
||||
notices: {
|
||||
'jump-hop:jump-hop-session-base': {
|
||||
status: 'failed',
|
||||
seen: false,
|
||||
},
|
||||
},
|
||||
generation: emptyGenerationFacts({
|
||||
activeSessionId: 'jump-hop-session-base',
|
||||
hasActiveGenerationFailure: true,
|
||||
}),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'active-failed-generation',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveJumpHopDraftOpenIntent({
|
||||
item: buildJumpHopWork({ generationStatus: 'generating' }),
|
||||
notices: {},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'restore-generating',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveJumpHopDraftOpenIntent({
|
||||
item: buildJumpHopWork({ generationStatus: 'generating' }),
|
||||
notices: {
|
||||
'jump-hop:jump-hop-session-base': {
|
||||
status: 'failed',
|
||||
seen: false,
|
||||
},
|
||||
},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'load-detail',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveWoodenFishDraftOpenIntent uses profile fallback and failure fallback stage', () => {
|
||||
expect(
|
||||
resolveWoodenFishDraftOpenIntent({
|
||||
item: buildWoodenFishWork({
|
||||
sourceSessionId: null,
|
||||
generationStatus: 'generating',
|
||||
}),
|
||||
notices: {
|
||||
'wooden-fish:wooden-fish-profile-base': {
|
||||
status: 'failed',
|
||||
seen: false,
|
||||
},
|
||||
},
|
||||
generation: emptyGenerationFacts({
|
||||
activeSessionId: 'wooden-fish-profile-base',
|
||||
hasActiveGenerationFailure: true,
|
||||
}),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'active-failed-generation',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveWoodenFishDraftOpenIntent({
|
||||
item: buildWoodenFishWork({ generationStatus: 'generating' }),
|
||||
notices: {},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'restore-generating',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveWoodenFishDraftOpenIntent({
|
||||
item: buildWoodenFishWork(),
|
||||
notices: {
|
||||
'wooden-fish:wooden-fish-session-base': {
|
||||
status: 'failed',
|
||||
seen: false,
|
||||
},
|
||||
},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'load-detail',
|
||||
failureFallbackStage: 'wooden-fish-workspace',
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveWoodenFishDraftOpenIntent({
|
||||
item: buildWoodenFishWork(),
|
||||
notices: {},
|
||||
generation: emptyGenerationFacts(),
|
||||
}),
|
||||
).toMatchObject({
|
||||
type: 'load-detail',
|
||||
failureFallbackStage: 'wooden-fish-generating',
|
||||
});
|
||||
});
|
||||
|
||||
test('buildPendingPuzzleWorks creates failed puzzle placeholder with stable ids and fallback title', () => {
|
||||
const pending = buildPendingPuzzleWorks(
|
||||
{
|
||||
@@ -604,3 +723,51 @@ function buildVisualNovelWork(
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildJumpHopWork(
|
||||
overrides: Partial<JumpHopWorkSummaryResponse> = {},
|
||||
): JumpHopWorkSummaryResponse {
|
||||
return {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-base',
|
||||
profileId: 'jump-hop-profile-base',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'jump-hop-session-base',
|
||||
workTitle: '潮雾跳一跳',
|
||||
workDescription: '潮雾港口跳一跳。',
|
||||
themeTags: [],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-06-03T08:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'ready',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWoodenFishWork(
|
||||
overrides: Partial<WoodenFishWorkSummaryResponse> = {},
|
||||
): WoodenFishWorkSummaryResponse {
|
||||
return {
|
||||
runtimeKind: 'wooden-fish',
|
||||
workId: 'wooden-fish-work-base',
|
||||
profileId: 'wooden-fish-profile-base',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'wooden-fish-session-base',
|
||||
workTitle: '潮雾敲木鱼',
|
||||
workDescription: '潮雾港口敲木鱼。',
|
||||
themeTags: ['敲木鱼'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-06-03T08:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'ready',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,6 +203,43 @@ export type VisualNovelDraftOpenIntent =
|
||||
profileId: string;
|
||||
};
|
||||
|
||||
export type JumpHopDraftOpenIntent =
|
||||
| {
|
||||
type: 'open-published-detail';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'active-failed-generation';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'restore-generating';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'load-detail';
|
||||
noticeKeys: string[];
|
||||
};
|
||||
|
||||
export type WoodenFishDraftOpenIntent =
|
||||
| {
|
||||
type: 'open-published-detail';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'active-failed-generation';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'restore-generating';
|
||||
noticeKeys: string[];
|
||||
}
|
||||
| {
|
||||
type: 'load-detail';
|
||||
noticeKeys: string[];
|
||||
failureFallbackStage: 'wooden-fish-workspace' | 'wooden-fish-generating';
|
||||
};
|
||||
|
||||
export function buildDraftNoticeKey(
|
||||
kind: CreationWorkShelfKind,
|
||||
id: string,
|
||||
@@ -505,6 +542,26 @@ export function buildSquareHoleDraftOpenNoticeKeys(
|
||||
]);
|
||||
}
|
||||
|
||||
export function buildJumpHopDraftOpenNoticeKeys(
|
||||
item: JumpHopWorkSummaryResponse,
|
||||
) {
|
||||
return collectDraftNoticeKeys('jump-hop', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]);
|
||||
}
|
||||
|
||||
export function buildWoodenFishDraftOpenNoticeKeys(
|
||||
item: WoodenFishWorkSummaryResponse,
|
||||
) {
|
||||
return collectDraftNoticeKeys('wooden-fish', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]);
|
||||
}
|
||||
|
||||
export function buildVisualNovelDraftOpenNoticeKeys(
|
||||
item: VisualNovelWorkSummary,
|
||||
) {
|
||||
@@ -817,6 +874,96 @@ export function resolveVisualNovelDraftOpenIntent(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveJumpHopDraftOpenIntent(params: {
|
||||
item: JumpHopWorkSummaryResponse;
|
||||
notices: DraftGenerationNoticeMap;
|
||||
generation: Pick<
|
||||
DraftOpenGenerationFacts,
|
||||
'activeSessionId' | 'hasActiveGenerationFailure'
|
||||
>;
|
||||
}): JumpHopDraftOpenIntent {
|
||||
const { item, notices, generation } = params;
|
||||
const noticeKeys = buildJumpHopDraftOpenNoticeKeys(item);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
return { type: 'open-published-detail', noticeKeys };
|
||||
}
|
||||
|
||||
const noticeIds = [item.workId, item.profileId, item.sourceSessionId];
|
||||
const sourceSessionId = normalizeDraftNoticeId(item.sourceSessionId);
|
||||
const hasFailedNotice = hasDraftGenerationNoticeStatus(
|
||||
notices,
|
||||
'jump-hop',
|
||||
noticeIds,
|
||||
'failed',
|
||||
);
|
||||
const isCurrentSession =
|
||||
sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId);
|
||||
|
||||
if (
|
||||
hasFailedNotice &&
|
||||
isCurrentSession &&
|
||||
generation.hasActiveGenerationFailure
|
||||
) {
|
||||
return { type: 'active-failed-generation', noticeKeys };
|
||||
}
|
||||
|
||||
if (isPersistedDraftGenerating(item.generationStatus) && !hasFailedNotice) {
|
||||
return { type: 'restore-generating', noticeKeys };
|
||||
}
|
||||
|
||||
return { type: 'load-detail', noticeKeys };
|
||||
}
|
||||
|
||||
export function resolveWoodenFishDraftOpenIntent(params: {
|
||||
item: WoodenFishWorkSummaryResponse;
|
||||
notices: DraftGenerationNoticeMap;
|
||||
generation: Pick<
|
||||
DraftOpenGenerationFacts,
|
||||
'activeSessionId' | 'hasActiveGenerationFailure'
|
||||
>;
|
||||
}): WoodenFishDraftOpenIntent {
|
||||
const { item, notices, generation } = params;
|
||||
const noticeKeys = buildWoodenFishDraftOpenNoticeKeys(item);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
return { type: 'open-published-detail', noticeKeys };
|
||||
}
|
||||
|
||||
const noticeIds = [item.workId, item.profileId, item.sourceSessionId];
|
||||
const sourceSessionId =
|
||||
normalizeDraftNoticeId(item.sourceSessionId) ??
|
||||
normalizeDraftNoticeId(item.profileId);
|
||||
const hasFailedNotice = hasDraftGenerationNoticeStatus(
|
||||
notices,
|
||||
'wooden-fish',
|
||||
noticeIds,
|
||||
'failed',
|
||||
);
|
||||
const isCurrentSession =
|
||||
sourceSessionId === normalizeDraftNoticeId(generation.activeSessionId);
|
||||
|
||||
if (
|
||||
hasFailedNotice &&
|
||||
isCurrentSession &&
|
||||
generation.hasActiveGenerationFailure
|
||||
) {
|
||||
return { type: 'active-failed-generation', noticeKeys };
|
||||
}
|
||||
|
||||
if (isPersistedDraftGenerating(item.generationStatus) && !hasFailedNotice) {
|
||||
return { type: 'restore-generating', noticeKeys };
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'load-detail',
|
||||
noticeKeys,
|
||||
failureFallbackStage: hasFailedNotice
|
||||
? 'wooden-fish-workspace'
|
||||
: 'wooden-fish-generating',
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCreationWorkShelfRuntimeState(params: {
|
||||
item: CreationWorkShelfItem;
|
||||
notices: DraftGenerationNoticeMap;
|
||||
|
||||
Reference in New Issue
Block a user