feat: restore agent sessions from creation drafts

This commit is contained in:
2026-04-23 21:23:46 +08:00
parent 349a397888
commit 53a9cdd791
11 changed files with 949 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
import {
@@ -26,8 +27,11 @@ type CustomWorldCreationHubProps = {
onDeletePublished?: ((item: CustomWorldWorkSummary) => void) | null;
deletingWorkId?: string | null;
onExperienceRpg?: ((item: CustomWorldWorkSummary) => void) | null;
bigFishItems?: BigFishWorkSummary[];
onOpenBigFishDetail?: (item: BigFishWorkSummary) => void;
onExperienceBigFish?: ((item: BigFishWorkSummary) => void) | null;
puzzleItems?: PuzzleWorkSummary[];
onOpenPuzzleDetail?: (profileId: string) => void;
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
onExperiencePuzzle?: ((profileId: string) => void) | null;
};
@@ -54,6 +58,9 @@ export function CustomWorldCreationHub({
onDeletePublished = null,
deletingWorkId = null,
onExperienceRpg = null,
bigFishItems = [],
onOpenBigFishDetail,
onExperienceBigFish = null,
puzzleItems = [],
onOpenPuzzleDetail,
onExperiencePuzzle = null,
@@ -63,18 +70,23 @@ export function CustomWorldCreationHub({
const unifiedItems = useMemo<UnifiedCreationWorkItem[]>(
() => [
...items.map((item) => ({ kind: 'rpg', item }) as const),
...bigFishItems.map((item) => ({ kind: 'big-fish', item }) as const),
...puzzleItems.map((item) => ({ kind: 'puzzle', item }) as const),
],
[items, puzzleItems],
[bigFishItems, items, puzzleItems],
);
const draftCount = unifiedItems.filter((entry) =>
entry.kind === 'puzzle'
? entry.item.publicationStatus === 'draft'
: entry.kind === 'big-fish'
? entry.item.status === 'draft'
: entry.item.status === 'draft',
).length;
const publishedCount = unifiedItems.filter((entry) =>
entry.kind === 'puzzle'
? entry.item.publicationStatus === 'published'
: entry.kind === 'big-fish'
? entry.item.status === 'published'
: entry.item.status === 'published',
).length;
const filteredItems = useMemo(
@@ -84,6 +96,8 @@ export function CustomWorldCreationHub({
? true
: entry.kind === 'puzzle'
? entry.item.publicationStatus === activeFilter
: entry.kind === 'big-fish'
? entry.item.status === activeFilter
: entry.item.status === activeFilter,
),
[activeFilter, unifiedItems],
@@ -144,7 +158,12 @@ export function CustomWorldCreationHub({
item={item}
onOpen={() => {
if (item.kind === 'puzzle') {
onOpenPuzzleDetail?.(item.item.profileId);
onOpenPuzzleDetail?.(item.item);
return;
}
if (item.kind === 'big-fish') {
onOpenBigFishDetail?.(item.item);
return;
}
@@ -167,6 +186,12 @@ export function CustomWorldCreationHub({
onExperiencePuzzle?.(item.item.profileId);
}
: null
: item.kind === 'big-fish'
? item.item.status === 'published'
? () => {
onExperienceBigFish?.(item.item);
}
: null
: item.item.status === 'published' && item.item.canEnterWorld
? () => {
onExperienceRpg?.(item.item);

View File

@@ -1,4 +1,5 @@
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
@@ -21,6 +22,10 @@ export type UnifiedCreationWorkItem =
kind: 'rpg';
item: CustomWorldWorkSummary;
}
| {
kind: 'big-fish';
item: BigFishWorkSummary;
}
| {
kind: 'puzzle';
item: PuzzleWorkSummary;
@@ -42,19 +47,26 @@ export function CustomWorldWorkCard({
deleteBusy = false,
}: CustomWorldWorkCardProps) {
const isPuzzle = item.kind === 'puzzle';
const isBigFish = item.kind === 'big-fish';
const isDraft =
item.kind === 'puzzle'
? item.item.publicationStatus === 'draft'
: item.kind === 'big-fish'
? item.item.status === 'draft'
: item.item.status === 'draft';
const openActionLabel = isPuzzle
? '查看详情'
const openActionLabel = isPuzzle || isBigFish
? isDraft
? '继续创作'
: '查看详情'
: isDraft
? item.item.playableNpcCount > 0 || item.item.landmarkCount > 0
? '继续完善'
: '继续创作'
: '查看详情';
const title = isPuzzle ? item.item.levelName : item.item.title;
const subtitle = isPuzzle ? item.item.authorDisplayName : item.item.subtitle;
const title =
item.kind === 'puzzle' ? item.item.levelName : item.item.title;
const subtitle =
item.kind === 'puzzle' ? item.item.authorDisplayName : item.item.subtitle;
const summary = item.item.summary;
const updatedAt = item.item.updatedAt;
const coverImageSrc = item.item.coverImageSrc ?? null;
@@ -87,9 +99,9 @@ export function CustomWorldWorkCard({
{isDraft ? '草稿' : '已发布'}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{isPuzzle ? '拼图' : 'RPG'}
{isPuzzle ? '拼图' : isBigFish ? '大鱼' : 'RPG'}
</span>
{!isPuzzle && item.item.stageLabel ? (
{item.kind === 'rpg' && item.item.stageLabel ? (
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.stageLabel}
</span>
@@ -133,6 +145,23 @@ export function CustomWorldWorkCard({
{item.item.playCount}
</span>
</>
) : isBigFish ? (
<>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelCount}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelMainImageReadyCount}
</span>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">
{item.item.levelMotionReadyCount}
</span>
{item.item.backgroundReady ? (
<span className="platform-pill platform-pill--success px-3 py-1 text-[10px]">
</span>
) : null}
</>
) : (
<>
<span className="platform-pill platform-pill--neutral px-3 py-1 text-[10px]">

View File

@@ -16,6 +16,7 @@ import type {
SendBigFishMessageRequest,
SubmitBigFishInputRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type {
PuzzleAgentActionRequest,
PuzzleAgentOperationRecord,
@@ -31,8 +32,10 @@ import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets'
import {
createBigFishCreationSession,
executeBigFishCreationAction,
getBigFishCreationSession,
streamBigFishCreationMessage,
} from '../../services/big-fish-creation';
import { listBigFishWorks } from '../../services/big-fish-works';
import {
startBigFishRuntimeRun,
submitBigFishRuntimeInput,
@@ -149,10 +152,12 @@ export function PlatformEntryFlowShellImpl({
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [bigFishSession, setBigFishSession] =
useState<BigFishSessionSnapshotResponse | null>(null);
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
const [bigFishRun, setBigFishRun] =
useState<BigFishRuntimeSnapshotResponse | null>(null);
const [bigFishError, setBigFishError] = useState<string | null>(null);
const [isBigFishBusy, setIsBigFishBusy] = useState(false);
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
const [streamingBigFishReplyText, setStreamingBigFishReplyText] =
useState('');
const [isStreamingBigFishReply, setIsStreamingBigFishReply] = useState(false);
@@ -404,6 +409,22 @@ export function PlatformEntryFlowShellImpl({
[],
);
const refreshBigFishShelf = useCallback(async () => {
setIsBigFishLoadingLibrary(true);
try {
const worksResponse = await listBigFishWorks();
setBigFishWorks(worksResponse.items);
setBigFishError(null);
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
);
} finally {
setIsBigFishLoadingLibrary(false);
}
}, [resolveBigFishErrorMessage]);
const refreshPuzzleShelf = useCallback(async () => {
setIsPuzzleLoadingLibrary(true);
@@ -1007,6 +1028,112 @@ export function PlatformEntryFlowShellImpl({
[enterCreateTab, resolvePuzzleErrorMessage, setSelectionStage],
);
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
setPuzzleOperation(null);
setPuzzleRun(null);
setSelectedPuzzleDetail(null);
setStreamingPuzzleReplyText('');
setIsStreamingPuzzleReply(false);
try {
const { session } = await getPuzzleAgentSession(sessionId);
setPuzzleSession(session);
enterCreateTab();
setSelectionStage(session.draft ? 'puzzle-result' : 'puzzle-agent-workspace');
} catch (error) {
await refreshPuzzleShelf().catch(() => undefined);
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图创作草稿失败。'));
enterCreateTab();
setSelectionStage('platform');
} finally {
setIsPuzzleBusy(false);
}
},
[
enterCreateTab,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const openBigFishDraft = useCallback(
async (item: BigFishWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('这份大鱼吃小鱼草稿缺少会话信息,请重新开始创作。');
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
setBigFishRun(null);
setStreamingBigFishReplyText('');
setIsStreamingBigFishReply(false);
try {
const { session } = await getBigFishCreationSession(sessionId);
setBigFishSession(session);
enterCreateTab();
setSelectionStage(
session.draft ? 'big-fish-result' : 'big-fish-agent-workspace',
);
} catch (error) {
await refreshBigFishShelf().catch(() => undefined);
setBigFishError(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼创作草稿失败。'),
);
enterCreateTab();
setSelectionStage('platform');
} finally {
setIsBigFishBusy(false);
}
},
[
enterCreateTab,
refreshBigFishShelf,
resolveBigFishErrorMessage,
setSelectionStage,
],
);
const startBigFishRunFromWork = useCallback(
async (item: BigFishWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
try {
const { session } = await getBigFishCreationSession(sessionId);
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishSession(session);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
} finally {
setIsBigFishBusy(false);
}
},
[resolveBigFishErrorMessage, setSelectionStage],
);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
@@ -1022,24 +1149,50 @@ export function PlatformEntryFlowShellImpl({
selectionStage,
]);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshBigFishShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBigFishShelf,
selectionStage,
]);
const creationHubContent = (
<CustomWorldCreationHub
items={creationHubItems}
loading={platformBootstrap.isLoadingPlatform || isPuzzleLoadingLibrary}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
}
error={
platformBootstrap.isLoadingPlatform || isPuzzleLoadingLibrary
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
puzzleError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setBigFishError(null);
setPuzzleError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
void refreshBigFishShelf();
void refreshPuzzleShelf();
}}
createError={
sessionController.creationTypeError ?? bigFishError ?? puzzleError
@@ -1073,10 +1226,25 @@ export function PlatformEntryFlowShellImpl({
onExperienceRpg={(item) => {
handleExperienceRpgWork(item);
}}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(profileId) => {
bigFishItems={bigFishWorks}
onOpenBigFishDetail={(item) => {
runProtectedAction(() => {
void openPuzzleDetail(profileId);
void openBigFishDraft(item);
});
}}
onExperienceBigFish={(item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {
if (item.publicationStatus === 'draft') {
void openPuzzleDraft(item);
return;
}
void openPuzzleDetail(item.profileId);
});
}}
onExperiencePuzzle={(profileId) => {

View File

@@ -234,6 +234,7 @@ export function PuzzleResultView({
<div className="flex items-start justify-between gap-3">
<button
type="button"
aria-label="返回"
onClick={onBack}
disabled={isBusy}
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84 disabled:opacity-45"

View File

@@ -32,8 +32,15 @@ import {
unpublishRpgEntryWorldProfile,
upsertRpgProfileBrowseHistory as upsertProfileBrowseHistory,
} from '../../services/rpg-entry';
import { createBigFishCreationSession } from '../../services/big-fish-creation';
import { createPuzzleAgentSession } from '../../services/puzzle-agent';
import {
createBigFishCreationSession,
getBigFishCreationSession,
} from '../../services/big-fish-creation';
import { listBigFishWorks } from '../../services/big-fish-works';
import {
createPuzzleAgentSession,
getPuzzleAgentSession,
} from '../../services/puzzle-agent';
import { listPuzzleWorks } from '../../services/puzzle-works';
import type { GameState } from '../../types';
import {
@@ -114,9 +121,14 @@ vi.mock('../../services/puzzle-works', () => ({
vi.mock('../../services/big-fish-creation', () => ({
createBigFishCreationSession: vi.fn(),
executeBigFishCreationAction: vi.fn(),
getBigFishCreationSession: vi.fn(),
streamBigFishCreationMessage: vi.fn(),
}));
vi.mock('../../services/big-fish-works', () => ({
listBigFishWorks: vi.fn(),
}));
vi.mock('../../services/puzzle-agent', () => ({
createPuzzleAgentSession: vi.fn(),
executePuzzleAgentAction: vi.fn(),
@@ -124,6 +136,39 @@ vi.mock('../../services/puzzle-agent', () => ({
streamPuzzleAgentMessage: vi.fn(),
}));
vi.mock('../big-fish-creation/BigFishAgentWorkspace', () => ({
BigFishAgentWorkspace: ({
session,
}: {
session: { sessionId: string; messages: Array<{ text: string }> } | null;
}) => (
<div className="big-fish-agent-workspace-mock">
{session?.sessionId ?? 'missing-session'}
{session?.messages.map((message) => (
<div key={`${session.sessionId}-${message.text}`}>{message.text}</div>
))}
</div>
),
}));
vi.mock('../big-fish-result/BigFishResultView', () => ({
BigFishResultView: ({
session,
onBack,
}: {
session: { draft?: { title: string } | null };
onBack: () => void;
}) => (
<div className="big-fish-result-view-mock">
<div></div>
<div>{session.draft?.title ?? '缺少草稿标题'}</div>
<button type="button" onClick={onBack}>
</button>
</div>
),
}));
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
CustomWorldAgentWorkspace: ({
session,
@@ -593,6 +638,110 @@ beforeEach(() => {
updatedAt: '2026-04-22T12:00:00.000Z',
},
});
vi.mocked(getBigFishCreationSession).mockResolvedValue({
session: {
sessionId: 'big-fish-session-1',
currentTurn: 2,
progressPercent: 90,
stage: 'draft_ready',
anchorPack: {
gameplayPromise: {
key: 'gameplay_promise',
label: '核心玩法',
value: '机械微生物吞并进化',
status: 'confirmed',
},
ecologyVisualTheme: {
key: 'ecology_visual_theme',
label: '生态视觉',
value: '深海机械浮游生态',
status: 'confirmed',
},
growthLadder: {
key: 'growth_ladder',
label: '成长阶梯',
value: '从微光孢子到深海巨鲲',
status: 'confirmed',
},
riskTempo: {
key: 'risk_tempo',
label: '风险节奏',
value: '快节奏吞并,后段压迫感增强',
status: 'confirmed',
},
},
draft: {
title: '机械深海 大鱼吃小鱼',
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
coreFun: '吞并更小机械生命并持续合体成长',
ecologyTheme: '深海机械浮游生态',
levels: [
{
level: 1,
name: '微光孢子',
oneLineFantasy: '像发光尘埃一样在深海漂浮。',
silhouetteDirection: '圆润微型机械球',
sizeRatio: 1,
visualPromptSeed: 'deep sea glowing mechanical spore',
motionPromptSeed: 'soft floating mechanical spore',
mergeSourceLevel: null,
preyWindow: [1],
threatWindow: [2],
isFinalLevel: false,
},
],
background: {
theme: '机械深海',
colorMood: '冷青色与暗金反光',
foregroundHints: '漂浮齿轮碎片',
midgroundComposition: '珊瑚状机械群落',
backgroundDepth: '深海远景光柱',
safePlayAreaHint: '中心区域留空',
spawnEdgeHint: '边缘暗流刷怪',
backgroundPromptSeed: 'mechanical deep sea arena',
},
runtimeParams: {
levelCount: 8,
mergeCountPerUpgrade: 3,
spawnTargetCount: 28,
leaderMoveSpeed: 1.2,
followerCatchUpSpeed: 1,
offscreenCullSeconds: 8,
preySpawnDeltaLevels: [-2, -1],
threatSpawnDeltaLevels: [1, 2],
winLevel: 8,
},
},
assetSlots: [],
assetCoverage: {
levelMainImageReadyCount: 0,
levelMotionReadyCount: 0,
backgroundReady: false,
requiredLevelCount: 8,
publishReady: false,
blockers: ['仍有主图、动作和背景未生成'],
},
messages: [
{
id: 'big-fish-message-1',
role: 'assistant',
kind: 'chat',
text: '先说说你想要什么样的大鱼生态。',
createdAt: '2026-04-22T12:00:00.000Z',
},
{
id: 'big-fish-message-2',
role: 'user',
kind: 'chat',
text: '我想做机械深海里微生物互相吞并进化。',
createdAt: '2026-04-22T12:01:00.000Z',
},
],
lastAssistantReply: '大鱼结果页草稿已经生成,可以补正式资产。',
publishReady: false,
updatedAt: '2026-04-22T12:10:00.000Z',
},
});
vi.mocked(createPuzzleAgentSession).mockResolvedValue({
session: {
sessionId: 'puzzle-session-1',
@@ -640,8 +789,170 @@ beforeEach(() => {
updatedAt: '2026-04-22T12:00:00.000Z',
},
});
vi.mocked(getPuzzleAgentSession).mockResolvedValue({
session: {
sessionId: 'puzzle-session-1',
currentTurn: 3,
progressPercent: 88,
stage: 'draft_ready',
anchorPack: {
themePromise: {
key: 'theme_promise',
label: '主题承诺',
value: '雨夜遗迹探索',
status: 'confirmed',
},
visualSubject: {
key: 'visual_subject',
label: '视觉主体',
value: '发光猫咪站在遗迹台阶上',
status: 'confirmed',
},
visualMood: {
key: 'visual_mood',
label: '视觉气质',
value: '潮湿、梦幻、轻悬疑',
status: 'confirmed',
},
compositionHooks: {
key: 'composition_hooks',
label: '构图钩子',
value: '台阶透视、倒影、门洞',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tags_and_forbidden',
label: '标签与禁区',
value: '雨夜、猫咪、遗迹;禁止文字水印',
status: 'confirmed',
},
},
draft: {
levelName: '雨夜猫塔',
summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
themeTags: ['雨夜', '猫咪', '遗迹'],
forbiddenDirectives: ['文字水印'],
creatorIntent: null,
anchorPack: {
themePromise: {
key: 'theme_promise',
label: '主题承诺',
value: '雨夜遗迹探索',
status: 'confirmed',
},
visualSubject: {
key: 'visual_subject',
label: '视觉主体',
value: '发光猫咪站在遗迹台阶上',
status: 'confirmed',
},
visualMood: {
key: 'visual_mood',
label: '视觉气质',
value: '潮湿、梦幻、轻悬疑',
status: 'confirmed',
},
compositionHooks: {
key: 'composition_hooks',
label: '构图钩子',
value: '台阶透视、倒影、门洞',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tags_and_forbidden',
label: '标签与禁区',
value: '雨夜、猫咪、遗迹;禁止文字水印',
status: 'confirmed',
},
},
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
},
messages: [
{
id: 'puzzle-message-1',
role: 'assistant',
kind: 'chat',
text: '先说一个你最想做成拼图的画面。',
createdAt: '2026-04-22T12:00:00.000Z',
},
{
id: 'puzzle-message-2',
role: 'user',
kind: 'chat',
text: '雨夜里有一只会发光的猫站在遗迹台阶上。',
createdAt: '2026-04-22T12:01:00.000Z',
},
],
lastAssistantReply: '拼图结果页草稿已经生成,可以开始出图并确认标签。',
publishedProfileId: null,
suggestedActions: [],
resultPreview: {
draft: {
levelName: '雨夜猫塔',
summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
themeTags: ['雨夜', '猫咪', '遗迹'],
forbiddenDirectives: ['文字水印'],
creatorIntent: null,
anchorPack: {
themePromise: {
key: 'theme_promise',
label: '主题承诺',
value: '雨夜遗迹探索',
status: 'confirmed',
},
visualSubject: {
key: 'visual_subject',
label: '视觉主体',
value: '发光猫咪站在遗迹台阶上',
status: 'confirmed',
},
visualMood: {
key: 'visual_mood',
label: '视觉气质',
value: '潮湿、梦幻、轻悬疑',
status: 'confirmed',
},
compositionHooks: {
key: 'composition_hooks',
label: '构图钩子',
value: '台阶透视、倒影、门洞',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tags_and_forbidden',
label: '标签与禁区',
value: '雨夜、猫咪、遗迹;禁止文字水印',
status: 'confirmed',
},
},
candidates: [],
selectedCandidateId: null,
coverImageSrc: null,
coverAssetId: null,
generationStatus: 'idle',
},
blockers: [
{
id: 'missing-cover-image',
code: 'MISSING_COVER_IMAGE',
message: '正式拼图图片尚未确定',
},
],
qualityFindings: [],
publishReady: false,
},
updatedAt: '2026-04-22T12:10:00.000Z',
},
});
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
vi.mocked(listRpgCreationWorks).mockResolvedValue([]);
vi.mocked(listBigFishWorks).mockResolvedValue({
items: [],
});
vi.mocked(listPuzzleWorks).mockResolvedValue({
items: [],
});
@@ -1054,6 +1365,106 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
expect(screen.queryByText(//u)).toBeNull();
});
test('puzzle draft card restores the bound agent session and opens the result view', async () => {
const user = userEvent.setup();
vi.mocked(listPuzzleWorks).mockResolvedValue({
items: [
{
workId: 'puzzle-work-session-1',
profileId: 'puzzle-profile-session-1',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-session-1',
authorDisplayName: '测试玩家',
levelName: '雨夜猫塔',
summary: '一张聚焦发光猫咪与遗迹台阶的雨夜拼图。',
themeTags: ['雨夜', '猫咪', '遗迹'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'draft',
updatedAt: '2026-04-22T12:10:00.000Z',
publishedAt: null,
playCount: 0,
publishReady: false,
},
],
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
expect(await screen.findByRole('button', { name: //u })).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
await waitFor(() => {
expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
});
expect(await screen.findByText('拼图结果页')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
expect(await screen.findByText('拼图玩法共创')).toBeTruthy();
expect(
screen.getByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
).toBeTruthy();
});
test('big fish draft card restores the bound agent session and opens the result view', async () => {
const user = userEvent.setup();
vi.mocked(listBigFishWorks).mockResolvedValue({
items: [
{
workId: 'big-fish-work-big-fish-session-1',
sourceSessionId: 'big-fish-session-1',
title: '机械深海 大鱼吃小鱼',
subtitle: '机械微生物吞并进化 · 偏爽快节奏',
summary: '机械微生物吞并进化',
coverImageSrc: null,
status: 'draft',
updatedAt: '2026-04-22T12:10:00.000Z',
publishReady: false,
levelCount: 8,
levelMainImageReadyCount: 0,
levelMotionReadyCount: 0,
backgroundReady: false,
},
],
});
render(<TestWrapper withAuth />);
await openCreationHub(user);
const title = await screen.findByText('机械深海 大鱼吃小鱼');
const card = title.closest('.platform-surface');
if (!(card instanceof HTMLElement)) {
throw new Error('Missing big fish draft card');
}
await user.click(within(card).getByRole('button', { name: //u }));
await waitFor(() => {
expect(getBigFishCreationSession).toHaveBeenCalledWith(
'big-fish-session-1',
);
});
expect(await screen.findByText('大鱼吃小鱼结果页')).toBeTruthy();
expect(screen.getByText('机械深海 大鱼吃小鱼')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '返回' }));
expect(
await screen.findByText('大鱼吃小鱼共创big-fish-session-1'),
).toBeTruthy();
expect(
screen.getByText('我想做机械深海里微生物互相吞并进化。'),
).toBeTruthy();
});
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
const user = userEvent.setup();
@@ -2198,7 +2609,7 @@ test('creation hub published work delete button removes the work directly from c
};
const publishedLibraryEntry = {
ownerUserId: 'user-1',
profileId: 'world-card-delete-1',
profileId: 'world-card-delete-1',,
profile: {
id: 'world-card-delete-1',
name: '潮雾列岛',