This commit is contained in:
2026-04-21 00:48:17 +08:00
parent 75944b1f1f
commit effe0355bd
19 changed files with 2897 additions and 180 deletions

View File

@@ -398,3 +398,30 @@ test('landmark tab uses first act image as scene card preview and keeps chapter
'/generated-custom-world-scenes/scene-act-1.png',
);
});
test('readOnly result view hides edit and create actions for agent preview mode', async () => {
const user = userEvent.setup();
render(
<CustomWorldResultView
profile={baseProfile}
previewCharacters={[]}
isGenerating={false}
progress={0}
progressLabel=""
error={null}
onBack={() => {}}
onProfileChange={() => {}}
readOnly
compactAgentResultMode
/>,
);
expect(screen.queryByRole('button', { name: /^$/u })).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
expect(screen.queryByRole('button', { name: '新增可扮演角色' })).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
expect(screen.queryByRole('button', { name: //u })).toBeNull();
});

View File

@@ -40,6 +40,7 @@ interface CustomWorldResultViewProps {
regenerateActionLabel?: string;
enterWorldActionLabel?: string;
autoSaveState?: 'idle' | 'saving' | 'saved' | 'error';
compactAgentResultMode?: boolean;
}
type EntityGenerationKind = 'playable' | 'story' | 'landmark';
@@ -372,6 +373,7 @@ export function CustomWorldResultView({
regenerateActionLabel = '重新生成',
enterWorldActionLabel = '进入世界',
autoSaveState = 'idle',
compactAgentResultMode = false,
}: CustomWorldResultViewProps) {
const [editorTarget, setEditorTarget] =
useState<CustomWorldEditorTarget | null>(null);
@@ -609,9 +611,11 @@ export function CustomWorldResultView({
onProfileChange={onProfileChange}
onDeleteStoryNpcs={handleDeleteStoryNpcs}
onDeleteLandmarks={handleDeleteLandmarks}
createActionLabel={readOnly ? undefined : createLabel}
createActionLabel={
readOnly || compactAgentResultMode ? undefined : createLabel
}
onCreateAction={
readOnly || !createTarget
readOnly || compactAgentResultMode || !createTarget
? undefined
: () => {
if (activeTab === 'playable') {

View File

@@ -135,12 +135,12 @@ export function CustomWorldCreationHub({
key={item.workId}
item={item}
onClick={() => {
if (item.status === 'draft' && item.sessionId) {
if (item.sourceType === 'agent_session' && item.sessionId) {
onResumeDraft(item.sessionId);
return;
}
if (item.status === 'published' && item.profileId) {
if (item.profileId) {
onEnterPublished(item.profileId);
}
}}

View File

@@ -6,14 +6,15 @@ import { useState } from 'react';
import { beforeEach, expect, test, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
createCustomWorldAgentSession,
executeCustomWorldAgentAction,
getCustomWorldAgentOperation,
getCustomWorldAgentSession,
listCustomWorldWorks,
streamCustomWorldAgentMessage,
} from '../../services/aiService';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
import {
clearProfileBrowseHistory,
@@ -54,12 +55,30 @@ async function clickFirstAsyncButtonByName(
await user.click(buttons[0]!);
}
async function openCreationHub(user: ReturnType<typeof userEvent.setup>) {
await clickFirstButtonByName(user, '创作');
expect(await screen.findByText('创作中心')).toBeTruthy();
}
async function openNewRpgCreation(
user: ReturnType<typeof userEvent.setup>,
) {
await openCreationHub(user);
const createButtons = await screen.findAllByRole('button', {
name: //u,
});
await user.click(createButtons.at(-1)!);
expect(screen.getByText('选择创作类型')).toBeTruthy();
await user.click(screen.getByRole('button', { name: / RPG/u }));
}
vi.mock('../../services/aiService', () => ({
createCustomWorldAgentSession: vi.fn(),
executeCustomWorldAgentAction: vi.fn(),
generateCustomWorldProfile: vi.fn(),
getCustomWorldAgentOperation: vi.fn(),
getCustomWorldAgentSession: vi.fn(),
listCustomWorldWorks: vi.fn(),
streamCustomWorldAgentMessage: vi.fn(),
}));
@@ -340,6 +359,7 @@ beforeEach(() => {
vi.mocked(createCustomWorldAgentSession).mockResolvedValue({
session: mockSession,
});
vi.mocked(listCustomWorldWorks).mockResolvedValue([]);
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
operation: {
operationId: 'operation-draft-foundation-1',
@@ -369,8 +389,11 @@ test('create tab opens game type modal, keeps AIRP and visual novel locked, and
render(<TestWrapper />);
await clickFirstButtonByName(user, '创作');
await clickFirstButtonByName(user, //u);
await openCreationHub(user);
const createButtons = await screen.findAllByRole('button', {
name: //u,
});
await user.click(createButtons.at(-1)!);
expect(screen.getByText('选择创作类型')).toBeTruthy();
@@ -393,6 +416,52 @@ test('create tab opens game type modal, keeps AIRP and visual novel locked, and
).toBeTruthy();
});
test('create tab uses unified creation hub and can resume an agent draft', async () => {
const user = userEvent.setup();
vi.mocked(listCustomWorldWorks).mockResolvedValue([
{
workId: 'draft:custom-world-agent-session-1',
sourceType: 'agent_session',
status: 'draft',
title: '潮雾列岛',
subtitle: '精修对象',
summary: '玩家是失职返乡的守灯人。',
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
updatedAt: '2026-04-20T10:00:00.000Z',
publishedAt: null,
stage: 'object_refining',
stageLabel: '精修对象',
playableNpcCount: 3,
landmarkCount: 4,
roleVisualReadyCount: 1,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: '沈砺 · 主图已生成',
sessionId: 'custom-world-agent-session-1',
profileId: null,
canResume: true,
canEnterWorld: false,
},
]);
render(<TestWrapper withAuth />);
await openCreationHub(user);
expect(
screen.getByRole('button', { name: //u }),
).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
await user.click(screen.getByRole('button', { name: //u }));
expect(
await screen.findByText('Agent工作区custom-world-agent-session-1'),
).toBeTruthy();
});
test('clicking a public work while logged out routes through requireAuth', async () => {
const user = userEvent.setup();
const requireAuth = vi.fn();
@@ -448,10 +517,7 @@ test('selecting RPG creation while logged out routes through requireAuth', async
/>,
);
await clickFirstButtonByName(user, '创作');
await clickFirstButtonByName(user, //u);
await user.click(screen.getByRole('button', { name: / RPG/u }));
await openNewRpgCreation(user);
expect(requireAuth).toHaveBeenCalledTimes(1);
expect(createCustomWorldAgentSession).not.toHaveBeenCalled();
});
@@ -461,9 +527,7 @@ test('starting draft generation leaves the agent workspace and shows the generat
render(<TestWrapper />);
await clickFirstButtonByName(user, '创作');
await clickFirstButtonByName(user, //u);
await user.click(screen.getByRole('button', { name: / RPG/u }));
await openNewRpgCreation(user);
expect(
await screen.findByText('Agent工作区custom-world-agent-session-1'),
@@ -490,7 +554,7 @@ test('starting draft generation leaves the agent workspace and shows the generat
expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
});
test('existing draft sessions enter the legacy result layout directly', async () => {
test('existing draft sessions enter the agent preview layout without opening legacy editor', async () => {
const user = userEvent.setup();
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
@@ -595,9 +659,7 @@ test('existing draft sessions enter the legacy result layout directly', async ()
render(<TestWrapper />);
await clickFirstButtonByName(user, '创作');
await clickFirstButtonByName(user, //u);
await user.click(screen.getByRole('button', { name: / RPG/u }));
await openNewRpgCreation(user);
await waitFor(
async () => {
@@ -611,13 +673,295 @@ test('existing draft sessions enter the legacy result layout directly', async ()
expect(screen.queryByText(/Agent/u)).toBeNull();
expect(screen.queryByRole('button', { name: /^/u })).toBeNull();
expect(screen.getByText(//u)).toBeTruthy();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
await user.click(screen.getByRole('button', { name: //u }));
await user.click(screen.getByRole('button', { name: //u }));
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.queryByText(//u)).toBeNull();
expect(screen.queryByRole('button', { name: /AI/u })).toBeNull();
expect(screen.queryByText('技能')).toBeNull();
});
expect(await screen.findByText(//u)).toBeTruthy();
expect(screen.getByRole('button', { name: /AI/u })).toBeTruthy();
expect(screen.getByText('技能')).toBeTruthy();
test('agent draft result back button returns to workspace without redundant sync when session is already latest', async () => {
const user = userEvent.setup();
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
operation: {
operationId: 'operation-sync-result-profile-1',
type: 'sync_result_profile',
status: 'queued',
phaseLabel: '同步结果页快照',
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
progress: 24,
error: null,
},
});
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
operationId: 'operation-sync-result-profile-1',
type: 'sync_result_profile',
status: 'completed',
phaseLabel: '结果页快照已同步',
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
progress: 100,
error: null,
});
const resultSession = {
...mockSession,
stage: 'object_refining' as const,
creatorIntent: {
sourceMode: 'card',
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
themeKeywords: ['海雾', '旧航路'],
toneDirectives: ['压抑', '悬疑'],
openingSituation: '首夜就有陌生船只闯入禁航区。',
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
keyFactions: [],
keyCharacters: [],
keyLandmarks: [],
iconicElements: ['会移动的海雾'],
forbiddenDirectives: [],
rawSettingText: '',
},
draftProfile: {
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [
{
id: 'playable-1',
name: '沈砺',
title: '旧航路引路人',
role: '关键同行者',
publicIdentity: '最熟悉旧航路的人。',
publicMask: '看上去像可靠旧友。',
currentPressure: '他必须在两股势力间站队。',
hiddenHook: '暗中替沉船商盟引路。',
relationToPlayer: '旧友兼潜在背叛者',
threadIds: ['thread-1'],
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
},
],
storyNpcs: [
{
id: 'story-1',
name: '顾潮音',
title: '守灯会值夜人',
role: '场景关键角色',
publicIdentity: '负责夜间巡灯与封锁。',
publicMask: '对外一直冷静克制。',
currentPressure: '她知道更多禁航区真相。',
hiddenHook: '曾亲眼见过失控海雾吞船。',
relationToPlayer: '最早愿意交换线索的人',
threadIds: ['thread-1'],
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
},
],
landmarks: [
{
id: 'landmark-1',
name: '回潮旧灯塔',
purpose: '观察雾潮与往来船只',
mood: '潮湿、压抑、风声不止',
importance: '开局核心场景',
characterIds: ['story-1'],
threadIds: ['thread-1'],
summary: '旧灯塔是整片群岛最先看见异动的地方。',
},
],
factions: [],
threads: [],
chapters: [],
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
openingSituation: '首夜就有陌生船只闯入禁航区。',
iconicElements: ['会移动的海雾'],
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
legacyResultProfile: {
id: 'agent-draft-custom-world-agent-session-1',
settingText: '被海雾吞没的旧航路群岛',
name: '潮雾列岛·同步后',
subtitle: '旧灯塔与失控航路',
summary: '同步后的结果页快照已经回写到 session。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
templateWorldType: 'WUXIA',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [],
storyNpcs: [],
items: [],
landmarks: [],
generationMode: 'full',
generationStatus: 'complete',
},
},
draftCards: [
{
id: 'world-foundation',
kind: 'world',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
status: 'warning',
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
warningCount: 0,
},
],
} satisfies CustomWorldAgentSessionSnapshot;
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(resultSession);
render(<TestWrapper />);
await openNewRpgCreation(user);
await waitFor(
async () => {
expect(await screen.findByText('世界档案')).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
},
{ timeout: 2500 },
);
await user.click(screen.getByRole('button', { name: //u }));
await waitFor(() => {
expect(
screen.getByText('Agent工作区custom-world-agent-session-1'),
).toBeTruthy();
});
expect(
vi.mocked(executeCustomWorldAgentAction).mock.calls.some(
([sessionId, payload]) =>
sessionId === 'custom-world-agent-session-1' &&
payload?.action === 'sync_result_profile',
),
).toBe(false);
expect(screen.queryByText('世界档案')).toBeNull();
});
test('agent draft result auto-save persists the latest profile rebuilt from synced session', async () => {
const user = userEvent.setup();
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
operation: {
operationId: 'operation-sync-result-profile-2',
type: 'sync_result_profile',
status: 'queued',
phaseLabel: '同步结果页快照',
phaseDetail: '正在把结果页里的最新世界结构写回当前草稿。',
progress: 24,
error: null,
},
});
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
operationId: 'operation-sync-result-profile-2',
type: 'sync_result_profile',
status: 'completed',
phaseLabel: '结果页快照已同步',
phaseDetail: '当前结果页编辑内容已经并回 Agent 草稿主链。',
progress: 100,
error: null,
});
const syncedSession = {
...mockSession,
stage: 'object_refining' as const,
creatorIntent: {
sourceMode: 'card',
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
themeKeywords: ['海雾', '旧航路'],
toneDirectives: ['压抑', '悬疑'],
openingSituation: '首夜就有陌生船只闯入禁航区。',
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
keyFactions: [],
keyCharacters: [],
keyLandmarks: [],
iconicElements: ['会移动的海雾'],
forbiddenDirectives: [],
rawSettingText: '',
},
draftProfile: {
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [],
storyNpcs: [],
landmarks: [],
factions: [],
threads: [],
chapters: [],
worldHook: '被海雾吞没的旧航路群岛',
playerPremise: '玩家回到群岛调查沉船真相。',
openingSituation: '首夜就有陌生船只闯入禁航区。',
iconicElements: ['会移动的海雾'],
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
legacyResultProfile: {
id: 'agent-draft-custom-world-agent-session-1',
settingText: '被海雾吞没的旧航路群岛',
name: '潮雾列岛·session最新版',
subtitle: '旧灯塔与失控航路',
summary: '作品库应该保存这份同步后的最新快照。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船与禁航区异动的真相。',
templateWorldType: 'WUXIA',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
playableNpcs: [],
storyNpcs: [],
items: [],
landmarks: [],
generationMode: 'full',
generationStatus: 'complete',
},
},
draftCards: [
{
id: 'world-foundation',
kind: 'world',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '第一版世界底稿已经整理完成。',
status: 'warning',
linkedIds: [],
warningCount: 0,
},
],
} satisfies CustomWorldAgentSessionSnapshot;
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(syncedSession);
render(<TestWrapper />);
await openNewRpgCreation(user);
await waitFor(
async () => {
expect(await screen.findByText('世界档案')).toBeTruthy();
expect(screen.getByText('已自动保存')).toBeTruthy();
},
{ timeout: 2500 },
);
await waitFor(() => {
expect(upsertCustomWorldProfile).toHaveBeenCalled();
});
const latestSavedProfile = vi.mocked(upsertCustomWorldProfile).mock.calls.at(-1)?.[0];
expect(latestSavedProfile?.name).toBe('潮雾列岛·session最新版');
expect(latestSavedProfile?.summary).toBe(
'作品库应该保存这份同步后的最新快照。',
);
});
test('authenticated users with save archives default into the saves tab', async () => {
@@ -697,42 +1041,71 @@ test('owned world detail can delete a work and return to the create tab list', a
const user = userEvent.setup();
vi.spyOn(window, 'confirm').mockReturnValue(true);
vi.mocked(listCustomWorldLibrary).mockResolvedValue([
{
ownerUserId: 'user-1',
profileId: 'world-delete-1',
profile: {
id: 'world-delete-1',
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '用于测试删除流程的作品。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清旧案。',
majorFactions: ['守灯会'],
coreConflicts: ['雾潮正在逼近港口'],
playableNpcs: [],
storyNpcs: [],
landmarks: [],
} as never,
visibility: 'draft',
publishedAt: null,
updatedAt: '2026-04-16T12:00:00.000Z',
authorDisplayName: '测试玩家',
worldName: '潮雾列岛',
const publishedWork = {
workId: 'published:world-delete-1',
sourceType: 'published_profile' as const,
status: 'published' as const,
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '用于测试删除流程的作品。',
coverImageSrc: null,
coverRenderMode: 'image' as const,
coverCharacterImageSrcs: [],
updatedAt: '2026-04-16T12:00:00.000Z',
publishedAt: '2026-04-16T12:00:00.000Z',
stage: null,
stageLabel: '已发布',
playableNpcCount: 0,
landmarkCount: 0,
roleVisualReadyCount: 0,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId: null,
profileId: 'world-delete-1',
canResume: false,
canEnterWorld: true,
};
const publishedLibraryEntry = {
ownerUserId: 'user-1',
profileId: 'world-delete-1',
profile: {
id: 'world-delete-1',
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '用于测试删除流程的作品。',
coverImageSrc: null,
themeMode: 'tide',
playableNpcCount: 0,
landmarkCount: 0,
},
]);
summary: '用于测试删除流程的作品。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清旧案。',
majorFactions: ['守灯会'],
coreConflicts: ['雾潮正在逼近港口'],
playableNpcs: [],
storyNpcs: [],
landmarks: [],
} as never,
visibility: 'published' as const,
publishedAt: '2026-04-16T12:00:00.000Z',
updatedAt: '2026-04-16T12:00:00.000Z',
authorDisplayName: '测试玩家',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '用于测试删除流程的作品。',
coverImageSrc: null,
themeMode: 'tide' as const,
playableNpcCount: 0,
landmarkCount: 0,
};
vi.mocked(listCustomWorldWorks)
.mockResolvedValueOnce([publishedWork])
.mockResolvedValue([]);
vi.mocked(listCustomWorldLibrary)
.mockResolvedValueOnce([publishedLibraryEntry])
.mockResolvedValue([]);
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
render(<TestWrapper withAuth />);
await clickFirstButtonByName(user, '创作');
await clickFirstAsyncButtonByName(user, //u);
await openCreationHub(user);
await user.click(screen.getByRole('button', { name: //u }));
await user.click(await screen.findByRole('button', { name: '删除作品' }));
await waitFor(() => {
@@ -742,8 +1115,77 @@ test('owned world detail can delete a work and return to the create tab list', a
await waitFor(() => {
expect(screen.queryByRole('button', { name: '删除作品' })).toBeNull();
});
expect(
screen.getAllByText('还没有保存任何自定义世界,先创建一个草稿开始吧。')
.length,
).toBeTruthy();
await waitFor(() => {
expect(screen.getByText('还没有作品')).toBeTruthy();
});
});
test('creation hub published work enters existing detail view', async () => {
const user = userEvent.setup();
vi.mocked(listCustomWorldWorks).mockResolvedValue([
{
workId: 'published:world-public-1',
sourceType: 'published_profile',
status: 'published',
title: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '已经发布的群岛世界作品。',
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
updatedAt: '2026-04-20T10:00:00.000Z',
publishedAt: '2026-04-20T10:00:00.000Z',
stage: null,
stageLabel: '已发布',
playableNpcCount: 3,
landmarkCount: 4,
roleVisualReadyCount: 1,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId: null,
profileId: 'world-public-1',
canResume: false,
canEnterWorld: true,
},
]);
vi.mocked(listCustomWorldLibrary).mockResolvedValue([
{
ownerUserId: 'user-1',
profileId: 'world-public-1',
profile: {
id: 'world-public-1',
name: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summary: '已经发布的群岛世界作品。',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清群岛旧案。',
majorFactions: ['守灯会'],
coreConflicts: ['假航灯正在扰乱航线'],
playableNpcs: [],
storyNpcs: [],
landmarks: [],
} as never,
visibility: 'published',
publishedAt: '2026-04-20T10:00:00.000Z',
updatedAt: '2026-04-20T10:00:00.000Z',
authorDisplayName: '测试玩家',
worldName: '潮雾列岛',
subtitle: '旧灯塔与失控航路',
summaryText: '已经发布的群岛世界作品。',
coverImageSrc: null,
themeMode: 'tide',
playableNpcCount: 3,
landmarkCount: 4,
},
]);
render(<TestWrapper withAuth />);
await openCreationHub(user);
await user.click(screen.getByRole('button', { name: //u }));
expect(await screen.findByText('世界信息')).toBeTruthy();
expect(screen.getByRole('button', { name: '开始游戏' })).toBeTruthy();
expect(screen.getByText('已发布')).toBeTruthy();
});

View File

@@ -14,6 +14,7 @@ import type {
CustomWorldAgentMessage,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
CustomWorldWorkSummary,
SendCustomWorldAgentMessageRequest,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type {
@@ -29,6 +30,7 @@ import {
executeCustomWorldAgentAction,
getCustomWorldAgentOperation,
getCustomWorldAgentSession,
listCustomWorldWorks,
streamCustomWorldAgentMessage,
} from '../../services/aiService';
import { buildCustomWorldProfileFromAgentDraft } from '../../services/customWorldAgentDraftResult';
@@ -69,6 +71,7 @@ import {
} from '../../services/storageService';
import { type CustomWorldProfile, type GameState } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { CustomWorldCreationHub } from '../custom-world-home/CustomWorldCreationHub';
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
import { type PlatformHomeTab, PlatformHomeView } from './PlatformHomeView';
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
@@ -107,6 +110,10 @@ type CustomWorldGenerationViewSource = 'agent-draft-foundation' | null;
type CustomWorldResultViewSource = 'saved-profile' | 'agent-draft' | null;
type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
type SyncedAgentDraftResult = {
session: CustomWorldAgentSessionSnapshot | null;
profile: CustomWorldProfile | null;
};
type PreGameSelectionFlowProps = {
selectionStage: SelectionStage;
@@ -164,6 +171,10 @@ function normalizeAgentBackedProfile(profile: CustomWorldProfile) {
} satisfies CustomWorldProfile;
}
function stringifyAgentBackedProfile(profile: CustomWorldProfile) {
return JSON.stringify(normalizeAgentBackedProfile(profile));
}
function LazyPanelFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
@@ -174,6 +185,37 @@ function LazyPanelFallback({ label }: { label: string }) {
);
}
function buildCreationHubFallbackItems(
entries: CustomWorldLibraryEntry<CustomWorldProfile>[],
): CustomWorldWorkSummary[] {
return entries
.filter((entry) => entry.visibility === 'published')
.map((entry) => ({
workId: `fallback:${entry.profileId}`,
sourceType: 'published_profile',
status: 'published',
title: entry.worldName,
subtitle: entry.subtitle || '已发布作品',
summary: entry.summaryText || '继续补完这个世界的设定与游玩入口。',
coverImageSrc: entry.coverImageSrc,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
updatedAt: entry.updatedAt,
publishedAt: entry.publishedAt,
stage: null,
stageLabel: '已发布',
playableNpcCount: entry.playableNpcCount,
landmarkCount: entry.landmarkCount,
roleVisualReadyCount: 0,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId: null,
profileId: entry.profileId,
canResume: false,
canEnterWorld: true,
}));
}
export function PreGameSelectionFlow({
selectionStage,
setSelectionStage,
@@ -191,6 +233,9 @@ export function PreGameSelectionFlow({
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
CustomWorldLibraryEntry<CustomWorldProfile>[]
>([]);
const [customWorldWorkEntries, setCustomWorldWorkEntries] = useState<
CustomWorldWorkSummary[]
>([]);
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
CustomWorldGalleryCard[]
>([]);
@@ -250,6 +295,10 @@ export function PreGameSelectionFlow({
const customWorldAutoSaveTimeoutRef = useRef<number | null>(null);
const lastAutoSavedProfileSignatureRef = useRef<string | null>(null);
const latestAutoSaveRequestIdRef = useRef(0);
const latestAgentResultSyncSignatureRef = useRef<string | null>(null);
// 用户手动返回工作区后,先抑制自动重开结果页,避免刚退出又被 session 快照顶回去。
const isAgentDraftResultAutoOpenSuppressedRef = useRef(false);
const isCustomWorldAutoSaveBusyRef = useRef(false);
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
undefined,
);
@@ -318,6 +367,17 @@ export function PreGameSelectionFlow({
}
}, [authUi?.user]);
const refreshCustomWorldWorks = useCallback(async () => {
if (!authUi?.user) {
setCustomWorldWorkEntries([]);
return [];
}
const nextItems = await listCustomWorldWorks();
setCustomWorldWorkEntries(nextItems);
return nextItems;
}, [authUi?.user]);
const appendBrowseHistoryEntry = useCallback(
async (entry: PlatformBrowseHistoryWriteEntry) => {
const nextEntries = writePlatformBrowseHistory(authUi?.user, entry);
@@ -380,6 +440,7 @@ export function PreGameSelectionFlow({
setDashboardError(null);
if (!isAuthenticated) {
setSavedCustomWorldEntries([]);
setCustomWorldWorkEntries([]);
setSaveEntries([]);
setProfileDashboard(null);
}
@@ -387,12 +448,14 @@ export function PreGameSelectionFlow({
try {
const [
libraryEntriesResult,
workEntriesResult,
galleryEntriesResult,
dashboardResult,
historyResult,
saveArchivesResult,
] = await Promise.allSettled([
isAuthenticated ? listCustomWorldLibrary() : Promise.resolve([]),
isAuthenticated ? listCustomWorldWorks() : Promise.resolve([]),
listCustomWorldGallery(),
isAuthenticated ? getProfileDashboard() : Promise.resolve(null),
isAuthenticated
@@ -423,6 +486,12 @@ export function PreGameSelectionFlow({
setSavedCustomWorldEntries([]);
}
if (workEntriesResult.status === 'fulfilled') {
setCustomWorldWorkEntries(workEntriesResult.value);
} else {
setCustomWorldWorkEntries([]);
}
if (galleryEntriesResult.status === 'fulfilled') {
setPublishedGalleryEntries(galleryEntriesResult.value);
} else {
@@ -431,11 +500,14 @@ export function PreGameSelectionFlow({
if (
(isAuthenticated && libraryEntriesResult.status === 'rejected') ||
(isAuthenticated && workEntriesResult.status === 'rejected') ||
galleryEntriesResult.status === 'rejected'
) {
const platformFailure =
libraryEntriesResult.status === 'rejected'
? libraryEntriesResult.reason
: workEntriesResult.status === 'rejected'
? workEntriesResult.reason
: galleryEntriesResult.status === 'rejected'
? galleryEntriesResult.reason
: null;
@@ -742,9 +814,14 @@ export function PreGameSelectionFlow({
return;
}
if (isAgentDraftResultAutoOpenSuppressedRef.current) {
return;
}
if (selectionStage === 'agent-workspace') {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
isAgentDraftResultAutoOpenSuppressedRef.current = false;
setSelectionStage('custom-world-result');
return;
}
@@ -755,10 +832,12 @@ export function PreGameSelectionFlow({
) {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
isAgentDraftResultAutoOpenSuppressedRef.current = false;
}
}, [
agentDraftResultProfile,
generatedCustomWorldProfile,
isAgentDraftResultAutoOpenSuppressedRef,
selectionStage,
setSelectionStage,
shouldAutoOpenAgentDraftResult,
@@ -776,6 +855,8 @@ export function PreGameSelectionFlow({
const isAgentDraftGenerationView =
customWorldGenerationViewSource === 'agent-draft-foundation';
const isAgentDraftResultView = customWorldResultViewSource === 'agent-draft';
const isAgentDraftResultEditingFrozen =
customWorldResultViewSource === 'agent-draft';
const activeGenerationSettingText = agentDraftSettingPreview;
const activeGenerationProgress = agentDraftGenerationProgress;
const isActiveGenerationRunning =
@@ -822,6 +903,7 @@ export function PreGameSelectionFlow({
setIsCreatingAgentSession(true);
setCreationTypeError(null);
isAgentDraftResultAutoOpenSuppressedRef.current = false;
try {
const { session } = await createCustomWorldAgentSession(
@@ -921,6 +1003,7 @@ export function PreGameSelectionFlow({
const isDraftFoundationAction = payload.action === 'draft_foundation';
if (isDraftFoundationAction) {
isAgentDraftResultAutoOpenSuppressedRef.current = false;
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
@@ -980,14 +1063,14 @@ export function PreGameSelectionFlow({
};
const leaveAgentDraftResult = () => {
isAgentDraftResultAutoOpenSuppressedRef.current = true;
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setPlatformTab('create');
setSelectionStage('platform');
setSelectionStage('agent-workspace');
};
const retryAgentDraftGeneration = () => {
@@ -1000,25 +1083,79 @@ export function PreGameSelectionFlow({
openCreationTypePicker();
};
const openLibraryDetail = (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => {
if (entry.visibility === 'published') {
void appendBrowseHistoryEntry({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
worldName: entry.worldName,
subtitle: entry.subtitle,
summaryText: entry.summaryText,
coverImageSrc: entry.coverImageSrc,
themeMode: entry.themeMode,
authorDisplayName: entry.authorDisplayName,
});
}
setSelectedDetailEntry(entry);
setDetailError(null);
setSelectionStage('detail');
};
const openLibraryDetail = useCallback(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
if (entry.visibility === 'published') {
void appendBrowseHistoryEntry({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
worldName: entry.worldName,
subtitle: entry.subtitle,
summaryText: entry.summaryText,
coverImageSrc: entry.coverImageSrc,
themeMode: entry.themeMode,
authorDisplayName: entry.authorDisplayName,
});
}
setSelectedDetailEntry(entry);
setDetailError(null);
setSelectionStage('detail');
},
[appendBrowseHistoryEntry, setSelectionStage],
);
const handleOpenCreationWork = useCallback(
async (work: CustomWorldWorkSummary) => {
if (work.status === 'draft' && work.sessionId) {
// 阶段二要求草稿优先回到 Agent 工作区,而不是再次自动顶回结果页。
isAgentDraftResultAutoOpenSuppressedRef.current = true;
persistAgentUiState(work.sessionId, null);
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setPlatformTab('create');
setSelectionStage('agent-workspace');
return;
}
if (!work.profileId) {
return;
}
try {
let matchedEntry = savedCustomWorldEntries.find(
(entry) => entry.profileId === work.profileId,
);
if (!matchedEntry && authUi?.user) {
const latestLibraryEntries = await listCustomWorldLibrary();
setSavedCustomWorldEntries(latestLibraryEntries);
matchedEntry = latestLibraryEntries.find(
(entry) => entry.profileId === work.profileId,
);
}
if (matchedEntry) {
openLibraryDetail(matchedEntry);
return;
}
setPlatformError('未找到对应作品,请刷新后重试。');
} catch (error) {
setPlatformError(resolveErrorMessage(error, '读取作品详情失败。'));
}
},
[
authUi?.user,
openLibraryDetail,
persistAgentUiState,
savedCustomWorldEntries,
setSelectionStage,
],
);
const openGalleryDetail = async (entry: CustomWorldGalleryCard) => {
setSelectionStage('detail');
@@ -1083,7 +1220,7 @@ export function PreGameSelectionFlow({
}
const normalizedProfile = normalizeAgentBackedProfile(profile);
const profileSignature = JSON.stringify(normalizedProfile);
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
const requestId = latestAutoSaveRequestIdRef.current + 1;
latestAutoSaveRequestIdRef.current = requestId;
setCustomWorldAutoSaveState('saving');
@@ -1097,6 +1234,9 @@ export function PreGameSelectionFlow({
lastAutoSavedProfileSignatureRef.current = profileSignature;
setSavedCustomWorldEntries(mutation.entries);
if (authUi?.user) {
void refreshCustomWorldWorks().catch(() => {});
}
setSelectedDetailEntry((current) => {
if (!current || current.profileId === mutation.entry.profileId) {
return mutation.entry;
@@ -1119,7 +1259,99 @@ export function PreGameSelectionFlow({
return null;
}
},
[generatedCustomWorldProfile],
[authUi?.user, generatedCustomWorldProfile, refreshCustomWorldWorks],
);
const syncAgentDraftResultProfile = useCallback(
async (profile: CustomWorldProfile) => {
if (!activeAgentSessionId) {
return {
session: null,
profile: null,
} satisfies SyncedAgentDraftResult;
}
const normalizedProfile = normalizeAgentBackedProfile(profile);
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
const latestSessionProfileSignature =
agentSession && buildCustomWorldProfileFromAgentDraft(agentSession)
? stringifyAgentBackedProfile(
buildCustomWorldProfileFromAgentDraft(agentSession)!,
)
: '';
if (latestSessionProfileSignature === profileSignature) {
latestAgentResultSyncSignatureRef.current = profileSignature;
return {
session: agentSession,
profile: normalizeAgentBackedProfile(
buildCustomWorldProfileFromAgentDraft(agentSession) ?? profile,
),
} satisfies SyncedAgentDraftResult;
}
if (latestAgentResultSyncSignatureRef.current === profileSignature) {
return {
session: agentSession,
profile: normalizeAgentBackedProfile(
buildCustomWorldProfileFromAgentDraft(agentSession) ?? profile,
),
} satisfies SyncedAgentDraftResult;
}
const { operation } = await executeCustomWorldAgentAction(
activeAgentSessionId,
{
action: 'sync_result_profile',
profile: normalizedProfile as unknown as Record<string, unknown>,
},
);
setAgentOperation(operation);
persistAgentUiState(activeAgentSessionId, operation.operationId);
for (let attempt = 0; attempt < 60; attempt += 1) {
const latestOperation = await getCustomWorldAgentOperation(
activeAgentSessionId,
operation.operationId,
);
setAgentOperation(latestOperation);
if (latestOperation.status === 'failed') {
throw new Error(
latestOperation.error ||
latestOperation.phaseDetail ||
'同步结果页世界快照失败。',
);
}
if (latestOperation.status === 'completed') {
persistAgentUiState(activeAgentSessionId, null);
const latestSession = await syncAgentSessionSnapshot(
activeAgentSessionId,
);
// 同步完成后统一从最新 session 重编译结果,保证结果页、作品库和进入世界吃同一份快照。
const latestProfile = normalizeAgentBackedProfile(
buildCustomWorldProfileFromAgentDraft(latestSession) ?? profile,
);
if (latestProfile) {
setGeneratedCustomWorldProfile(latestProfile);
}
latestAgentResultSyncSignatureRef.current = profileSignature;
return {
session: latestSession,
profile: latestProfile,
} satisfies SyncedAgentDraftResult;
}
await new Promise((resolve) => window.setTimeout(resolve, 200));
}
throw new Error('同步结果页世界快照超时。');
},
[
activeAgentSessionId,
agentSession,
persistAgentUiState,
syncAgentSessionSnapshot,
],
);
useEffect(() => {
@@ -1127,6 +1359,7 @@ export function PreGameSelectionFlow({
setCustomWorldAutoSaveState('idle');
setCustomWorldAutoSaveError(null);
lastAutoSavedProfileSignatureRef.current = null;
latestAgentResultSyncSignatureRef.current = null;
if (customWorldAutoSaveTimeoutRef.current !== null) {
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
customWorldAutoSaveTimeoutRef.current = null;
@@ -1138,7 +1371,11 @@ export function PreGameSelectionFlow({
return;
}
const nextSignature = JSON.stringify(generatedCustomWorldProfile);
if (isCustomWorldAutoSaveBusyRef.current) {
return;
}
const nextSignature = stringifyAgentBackedProfile(generatedCustomWorldProfile);
if (nextSignature === lastAutoSavedProfileSignatureRef.current) {
return;
}
@@ -1150,7 +1387,28 @@ export function PreGameSelectionFlow({
const profileToSave = generatedCustomWorldProfile;
customWorldAutoSaveTimeoutRef.current = window.setTimeout(() => {
void saveGeneratedCustomWorld(profileToSave);
void (async () => {
isCustomWorldAutoSaveBusyRef.current = true;
try {
let latestProfileToSave = normalizeAgentBackedProfile(profileToSave);
if (isAgentDraftResultView) {
const syncedResult =
await syncAgentDraftResultProfile(profileToSave);
// 作品库自动保存优先落同步后 session 重编译出的结果,避免继续保存旧的前端内存态。
latestProfileToSave = normalizeAgentBackedProfile(
syncedResult.profile ?? profileToSave,
);
}
await saveGeneratedCustomWorld(latestProfileToSave);
} catch (error) {
setCustomWorldAutoSaveState('error');
setCustomWorldAutoSaveError(
resolveErrorMessage(error, '保存自定义世界失败。'),
);
} finally {
isCustomWorldAutoSaveBusyRef.current = false;
}
})();
customWorldAutoSaveTimeoutRef.current = null;
}, 600);
@@ -1160,7 +1418,13 @@ export function PreGameSelectionFlow({
customWorldAutoSaveTimeoutRef.current = null;
}
};
}, [generatedCustomWorldProfile, saveGeneratedCustomWorld, selectionStage]);
}, [
generatedCustomWorldProfile,
isAgentDraftResultView,
saveGeneratedCustomWorld,
selectionStage,
syncAgentDraftResultProfile,
]);
const openSavedCustomWorldEditor = (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
@@ -1200,6 +1464,7 @@ export function PreGameSelectionFlow({
selectedDetailEntry.profileId,
);
setSavedCustomWorldEntries(mutation.entries);
await refreshCustomWorldWorks().catch(() => []);
setSelectedDetailEntry(mutation.entry);
setPublishedGalleryEntries(await listCustomWorldGallery());
} catch (error) {
@@ -1221,6 +1486,7 @@ export function PreGameSelectionFlow({
selectedDetailEntry.profileId,
);
setSavedCustomWorldEntries(mutation.entries);
await refreshCustomWorldWorks().catch(() => []);
setSelectedDetailEntry(mutation.entry);
setPublishedGalleryEntries(await listCustomWorldGallery());
} catch (error) {
@@ -1249,6 +1515,7 @@ export function PreGameSelectionFlow({
selectedDetailEntry.profileId,
);
setSavedCustomWorldEntries(entries);
await refreshCustomWorldWorks().catch(() => []);
setSelectedDetailEntry(null);
setPlatformTab('create');
setSelectionStage('platform');
@@ -1269,6 +1536,10 @@ export function PreGameSelectionFlow({
),
);
const resultViewError = customWorldAutoSaveError ?? customWorldError;
const creationHubItems =
customWorldWorkEntries.length > 0
? customWorldWorkEntries
: buildCreationHubFallbackItems(savedCustomWorldEntries);
return (
<>
@@ -1281,47 +1552,106 @@ export function PreGameSelectionFlow({
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PlatformHomeView
activeTab={platformTab}
onTabChange={setPlatformTab}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
saveEntries={saveEntries}
saveError={saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={publishedGalleryEntries}
myEntries={savedCustomWorldEntries}
historyEntries={historyEntries}
profileDashboard={profileDashboard}
isLoadingPlatform={isLoadingPlatform}
isLoadingDashboard={isLoadingDashboard}
isResumingSaveWorldKey={isResumingSaveWorldKey}
platformError={
isLoadingPlatform ? null : (platformError ?? creationTypeError)
}
dashboardError={isLoadingDashboard ? null : dashboardError}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
void handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCustomWorldCreator}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
runProtectedAction(() => {
void openGalleryDetail(entry);
});
}}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
openLibraryDetail(entry);
});
}}
onOpenProfileDashboardCard={() => {
if (dashboardError) {
void refreshProfileDashboard();
{platformTab === 'create' ? (
<CustomWorldCreationHub
items={creationHubItems}
loading={isLoadingPlatform}
error={isLoadingPlatform ? null : (platformError ?? creationTypeError)}
onBack={() => {
setPlatformTab('home');
}}
onRetry={() => {
setPlatformError(null);
void refreshCustomWorldWorks().catch((error) => {
setPlatformError(
resolveErrorMessage(error, '读取创作作品列表失败。'),
);
});
}}
onCreateNew={openCreationTypePicker}
onResumeDraft={(sessionId) => {
runProtectedAction(() => {
void handleOpenCreationWork({
workId: `draft:${sessionId}`,
sourceType: 'agent_session',
status: 'draft',
title: '',
subtitle: '',
summary: '',
coverImageSrc: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
updatedAt: new Date().toISOString(),
publishedAt: null,
stage: null,
stageLabel: '',
playableNpcCount: 0,
landmarkCount: 0,
roleVisualReadyCount: 0,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId,
profileId: null,
canResume: true,
canEnterWorld: false,
});
});
}}
onEnterPublished={(profileId) => {
runProtectedAction(() => {
const matchedWork = creationHubItems.find(
(entry) => entry.profileId === profileId,
);
if (!matchedWork) {
return;
}
void handleOpenCreationWork(matchedWork);
});
}}
/>
) : (
<PlatformHomeView
activeTab={platformTab}
onTabChange={setPlatformTab}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
saveEntries={saveEntries}
saveError={saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={publishedGalleryEntries}
myEntries={savedCustomWorldEntries}
historyEntries={historyEntries}
profileDashboard={profileDashboard}
isLoadingPlatform={isLoadingPlatform}
isLoadingDashboard={isLoadingDashboard}
isResumingSaveWorldKey={isResumingSaveWorldKey}
platformError={
isLoadingPlatform ? null : (platformError ?? creationTypeError)
}
}}
/>
dashboardError={isLoadingDashboard ? null : dashboardError}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
void handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCustomWorldCreator}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
runProtectedAction(() => {
void openGalleryDetail(entry);
});
}}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
openLibraryDetail(entry);
});
}}
onOpenProfileDashboardCard={() => {
if (dashboardError) {
void refreshProfileDashboard();
}
}}
/>
)}
</motion.div>
)}
@@ -1501,7 +1831,28 @@ export function PreGameSelectionFlow({
}}
onBack={
isAgentDraftResultView
? leaveAgentDraftResult
? () => {
void (async () => {
const currentProfile =
generatedCustomWorldProfile ??
buildCustomWorldProfileFromAgentDraft(
agentSession,
);
if (currentProfile && activeAgentSessionId) {
await syncAgentDraftResultProfile(currentProfile);
}
leaveAgentDraftResult();
})().catch((error) => {
setCustomWorldError(
resolveErrorMessage(
error,
'返回创作前同步草稿失败。',
),
);
});
}
: leaveCustomWorldResult
}
onEditSetting={undefined}
@@ -1509,10 +1860,40 @@ export function PreGameSelectionFlow({
onContinueExpand={undefined}
onEnterWorld={() => {
runProtectedAction(() => {
handleCustomWorldSelect(generatedCustomWorldProfile);
void (async () => {
if (!isAgentDraftResultView || !activeAgentSessionId) {
handleCustomWorldSelect(generatedCustomWorldProfile);
return;
}
const currentProfile =
generatedCustomWorldProfile ??
buildCustomWorldProfileFromAgentDraft(agentSession);
if (!currentProfile) {
return;
}
const latestResult = await syncAgentDraftResultProfile(
currentProfile,
);
const latestProfile = normalizeAgentBackedProfile(
buildCustomWorldProfileFromAgentDraft(
latestResult.session ?? agentSession,
) ??
latestResult.profile ??
currentProfile,
);
setGeneratedCustomWorldProfile(latestProfile);
handleCustomWorldSelect(latestProfile);
})().catch((error) => {
setCustomWorldError(
resolveErrorMessage(error, '进入世界前同步草稿失败。'),
);
});
});
}}
readOnly={false}
readOnly={isAgentDraftResultEditingFrozen}
compactAgentResultMode={isAgentDraftResultView}
backLabel={isAgentDraftResultView ? '返回创作' : undefined}
editActionLabel="去Agent调整设定"
enterWorldActionLabel="进入世界"