refactor: 收口作品架 Source Adapter registry

This commit is contained in:
2026-06-03 15:47:26 +08:00
parent cf0840d9e9
commit 5783bfeea6
4 changed files with 225 additions and 72 deletions

View File

@@ -5,10 +5,11 @@ import { expect, test, vi } from 'vitest';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
buildCreationWorkShelfItems,
buildCreationWorkShelfItemsFromSources,
type CreationWorkShelfItem,
getCreationWorkShelfItemTime,
hasBarkBattleRequiredImages,
isPersistedBarkBattleDraftGenerating,
type CreationWorkShelfItem,
} from './creationWorkShelf';
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
@@ -56,6 +57,86 @@ test('buildCreationWorkShelfItems maps visual novel items with VN public code',
expect(items[1]?.publicWorkCode).toBeNull();
});
test('buildCreationWorkShelfItemsFromSources flattens source adapters and applies runtime state', () => {
const [staleRpgItem] = buildCreationWorkShelfItems({
rpgItems: [
{
workId: 'draft:rpg-source-adapter',
sourceType: 'agent_session',
status: 'draft',
title: '旧 RPG 草稿',
subtitle: '待完善',
summary: '通过 source adapter 输入。',
coverImageSrc: null,
updatedAt: '2026-05-01T00:00:00.000Z',
publishedAt: null,
stage: 'clarifying',
stageLabel: '待完善',
playableNpcCount: 0,
landmarkCount: 0,
sessionId: 'rpg-source-adapter',
profileId: null,
canResume: true,
canEnterWorld: false,
},
],
bigFishItems: [],
puzzleItems: [],
});
const [freshPuzzleItem] = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [
{
workId: 'puzzle:source-adapter',
profileId: 'puzzle-source-adapter',
ownerUserId: 'user-1',
authorDisplayName: '拼图作者',
levelName: '新拼图',
summary: '新近拼图。',
themeTags: ['灯塔'],
coverImageSrc: null,
publicationStatus: 'draft',
updatedAt: '2026-05-03T00:00:00.000Z',
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: false,
},
],
});
const items = buildCreationWorkShelfItemsFromSources({
sources: [
{
kind: 'rpg',
buildItems: () => (staleRpgItem ? [staleRpgItem] : []),
},
{
kind: 'puzzle',
buildItems: () => (freshPuzzleItem ? [freshPuzzleItem] : []),
},
],
getItemState: (item) =>
item.id === staleRpgItem?.id
? {
isGenerating: true,
hasUnreadUpdate: true,
titleOverride: '生成中 RPG 草稿',
}
: null,
});
expect(items.map((item) => item.id)).toEqual([
'puzzle:source-adapter',
'draft:rpg-source-adapter',
]);
expect(items[1]?.title).toBe('生成中 RPG 草稿');
expect(items[1]?.isGenerating).toBe(true);
expect(items[1]?.hasUnreadUpdate).toBe(true);
});
test('buildCreationWorkShelfItems maps wooden fish items with WF public code', () => {
const onOpenWoodenFishDetail = vi.fn();
const woodenFishWork = {

View File

@@ -2,19 +2,19 @@ import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contrac
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
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 { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import {
buildBabyObjectMatchPublicWorkCode,
buildCustomWorldPublicWorkCode,
buildBarkBattlePublicWorkCode,
buildBigFishPublicWorkCode,
buildCustomWorldPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
@@ -157,6 +157,11 @@ export type CreationWorkShelfRuntimeState = {
summaryOverride?: string;
};
export type CreationWorkShelfSourceAdapter = {
kind: CreationWorkShelfKind;
buildItems: () => readonly CreationWorkShelfItem[];
};
export function buildCreationWorkShelfItems(params: {
rpgItems: CustomWorldWorkSummary[];
rpgLibraryEntries?: CustomWorldLibraryEntry<CustomWorldProfile>[];
@@ -252,70 +257,135 @@ export function buildCreationWorkShelfItems(params: {
getItemState,
} = params;
return [
...rpgItems.map((item) =>
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries, {
onOpenDraft: onOpenRpgDraft,
onEnterPublished: onEnterRpgPublished,
onDelete: onDeleteRpg,
}),
),
...bigFishItems.map((item) =>
mapBigFishWorkToShelfItem(item, canDeleteBigFish, {
onOpen: onOpenBigFishDetail,
onDelete: onDeleteBigFish,
}),
),
...match3dItems.map((item) =>
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D, {
onOpen: onOpenMatch3DDetail,
onDelete: onDeleteMatch3D,
}),
),
...squareHoleItems.map((item) =>
mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole, {
onOpen: onOpenSquareHoleDetail,
onDelete: onDeleteSquareHole,
}),
),
...jumpHopItems.map((item) =>
mapJumpHopWorkToShelfItem(item, canDeleteJumpHop, {
onOpen: onOpenJumpHopDetail,
onDelete: onDeleteJumpHop,
}),
),
...woodenFishItems.map((item) =>
mapWoodenFishWorkToShelfItem(item, canDeleteWoodenFish, {
onOpen: onOpenWoodenFishDetail,
onDelete: onDeleteWoodenFish,
}),
),
...puzzleItems.map((item) =>
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
onOpen: onOpenPuzzleDetail,
onDelete: onDeletePuzzle,
onClaimPointIncentive: onClaimPuzzlePointIncentive,
}),
),
...babyObjectMatchItems.map((item) =>
mapBabyObjectMatchDraftToShelfItem(item, canDeleteBabyObjectMatch, {
onOpen: onOpenBabyObjectMatchDetail,
onDelete: onDeleteBabyObjectMatch,
}),
),
...mergeBarkBattleShelfSourceItems(barkBattleItems).map((item) =>
mapBarkBattleWorkToShelfItem(item, canDeleteBarkBattle, {
onOpen: onOpenBarkBattleDetail,
onDelete: onDeleteBarkBattle,
}),
),
...visualNovelItems.map((item) =>
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
onOpen: onOpenVisualNovelDetail,
onDelete: onDeleteVisualNovel,
}),
),
]
return buildCreationWorkShelfItemsFromSources({
sources: [
{
kind: 'rpg',
buildItems: () =>
rpgItems.map((item) =>
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries, {
onOpenDraft: onOpenRpgDraft,
onEnterPublished: onEnterRpgPublished,
onDelete: onDeleteRpg,
}),
),
},
{
kind: 'big-fish',
buildItems: () =>
bigFishItems.map((item) =>
mapBigFishWorkToShelfItem(item, canDeleteBigFish, {
onOpen: onOpenBigFishDetail,
onDelete: onDeleteBigFish,
}),
),
},
{
kind: 'match3d',
buildItems: () =>
match3dItems.map((item) =>
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D, {
onOpen: onOpenMatch3DDetail,
onDelete: onDeleteMatch3D,
}),
),
},
{
kind: 'square-hole',
buildItems: () =>
squareHoleItems.map((item) =>
mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole, {
onOpen: onOpenSquareHoleDetail,
onDelete: onDeleteSquareHole,
}),
),
},
{
kind: 'jump-hop',
buildItems: () =>
jumpHopItems.map((item) =>
mapJumpHopWorkToShelfItem(item, canDeleteJumpHop, {
onOpen: onOpenJumpHopDetail,
onDelete: onDeleteJumpHop,
}),
),
},
{
kind: 'wooden-fish',
buildItems: () =>
woodenFishItems.map((item) =>
mapWoodenFishWorkToShelfItem(item, canDeleteWoodenFish, {
onOpen: onOpenWoodenFishDetail,
onDelete: onDeleteWoodenFish,
}),
),
},
{
kind: 'puzzle',
buildItems: () =>
puzzleItems.map((item) =>
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
onOpen: onOpenPuzzleDetail,
onDelete: onDeletePuzzle,
onClaimPointIncentive: onClaimPuzzlePointIncentive,
}),
),
},
{
kind: 'baby-object-match',
buildItems: () =>
babyObjectMatchItems.map((item) =>
mapBabyObjectMatchDraftToShelfItem(
item,
canDeleteBabyObjectMatch,
{
onOpen: onOpenBabyObjectMatchDetail,
onDelete: onDeleteBabyObjectMatch,
},
),
),
},
{
kind: 'bark-battle',
buildItems: () =>
mergeBarkBattleShelfSourceItems(barkBattleItems).map((item) =>
mapBarkBattleWorkToShelfItem(item, canDeleteBarkBattle, {
onOpen: onOpenBarkBattleDetail,
onDelete: onDeleteBarkBattle,
}),
),
},
{
kind: 'visual-novel',
buildItems: () =>
visualNovelItems.map((item) =>
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
onOpen: onOpenVisualNovelDetail,
onDelete: onDeleteVisualNovel,
}),
),
},
],
getItemState,
});
}
export function buildCreationWorkShelfItemsFromSources(params: {
sources: readonly CreationWorkShelfSourceAdapter[];
getItemState?: (
item: CreationWorkShelfItem,
) => CreationWorkShelfRuntimeState | null;
}) {
const { sources, getItemState } = params;
const sourceItems = sources.reduce<CreationWorkShelfItem[]>(
(items, source) => {
items.push(...source.buildItems());
return items;
},
[],
);
return sourceItems
.map((item) => {
const state = getItemState?.(item);
const persistedIsGenerating = isPersistedCreationWorkGenerating(item);