1
This commit is contained in:
@@ -8,6 +8,7 @@ import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { ApiClientError } from '../../services/apiClient';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
createRpgCreationSession,
|
||||
executeRpgCreationAction,
|
||||
getRpgCreationOperation,
|
||||
getRpgCreationResultView,
|
||||
getRpgCreationSession,
|
||||
listRpgCreationWorks,
|
||||
streamRpgCreationMessage,
|
||||
@@ -101,6 +103,7 @@ vi.mock('../../services/rpg-creation', () => ({
|
||||
createRpgCreationSession: vi.fn(),
|
||||
executeRpgCreationAction: vi.fn(),
|
||||
getRpgCreationOperation: vi.fn(),
|
||||
getRpgCreationResultView: vi.fn(),
|
||||
getRpgCreationSession: vi.fn(),
|
||||
listRpgCreationWorks: vi.fn(),
|
||||
streamRpgCreationMessage: vi.fn(),
|
||||
@@ -523,6 +526,48 @@ const compiledAgentDraftSession: CustomWorldAgentSessionSnapshot = {
|
||||
},
|
||||
};
|
||||
|
||||
function buildResultViewForSession(
|
||||
session: CustomWorldAgentSessionSnapshot,
|
||||
): RpgCreationResultView {
|
||||
const profile = session.resultPreview?.preview ?? null;
|
||||
const isResultStage =
|
||||
session.stage === 'object_refining' ||
|
||||
session.stage === 'visual_refining' ||
|
||||
session.stage === 'long_tail_review' ||
|
||||
session.stage === 'ready_to_publish' ||
|
||||
session.stage === 'published';
|
||||
|
||||
return {
|
||||
session,
|
||||
profile,
|
||||
profileSource: profile ? 'result_preview' : 'none',
|
||||
targetStage: profile && isResultStage
|
||||
? 'custom-world-result'
|
||||
: session.stage === 'error'
|
||||
? 'custom-world-generating'
|
||||
: 'agent-workspace',
|
||||
generationViewSource: session.stage === 'error'
|
||||
? 'agent-draft-foundation'
|
||||
: null,
|
||||
resultViewSource: profile && isResultStage ? 'agent-draft' : null,
|
||||
canAutosaveLibrary: Boolean(profile && isResultStage),
|
||||
canSyncResultProfile:
|
||||
session.stage === 'object_refining' ||
|
||||
session.stage === 'visual_refining' ||
|
||||
session.stage === 'long_tail_review' ||
|
||||
session.stage === 'ready_to_publish',
|
||||
publishReady: Boolean(session.resultPreview?.publishReady),
|
||||
canEnterWorld: Boolean(session.resultPreview?.canEnterWorld),
|
||||
blockerCount: session.resultPreview?.blockers?.length ?? 0,
|
||||
recoveryAction: profile && isResultStage
|
||||
? 'open_result'
|
||||
: session.stage === 'error'
|
||||
? 'resume_generation'
|
||||
: 'continue_agent',
|
||||
recoveryReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
type TestAuthValue = {
|
||||
user: AuthUser | null;
|
||||
canAccessProtectedData: boolean;
|
||||
@@ -573,8 +618,11 @@ function TestWrapper({
|
||||
onContinueGame?: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
||||
onSelectWorld?: RpgEntryFlowShellProps['handleCustomWorldSelect'];
|
||||
} = {}) {
|
||||
const [selectionStage, setSelectionStage] =
|
||||
useState<SelectionStage>('platform');
|
||||
const [selectionStage, setSelectionStage] = useState<SelectionStage>(() =>
|
||||
window.location.pathname === '/creation/rpg/agent'
|
||||
? 'agent-workspace'
|
||||
: 'platform',
|
||||
);
|
||||
|
||||
const content = (
|
||||
<RpgEntryFlowShell
|
||||
@@ -600,7 +648,7 @@ function TestWrapper({
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
window.history.replaceState(null, '', '/');
|
||||
window.sessionStorage.clear();
|
||||
window.localStorage.clear();
|
||||
@@ -667,6 +715,9 @@ beforeEach(() => {
|
||||
vi.mocked(createRpgCreationSession).mockResolvedValue({
|
||||
session: mockSession,
|
||||
});
|
||||
vi.mocked(getRpgCreationResultView).mockImplementation(async () =>
|
||||
buildResultViewForSession(mockSession),
|
||||
);
|
||||
vi.mocked(createBigFishCreationSession).mockResolvedValue({
|
||||
session: {
|
||||
sessionId: 'big-fish-session-1',
|
||||
@@ -757,9 +808,17 @@ beforeEach(() => {
|
||||
level: 1,
|
||||
name: '微光孢子',
|
||||
oneLineFantasy: '像发光尘埃一样在深海漂浮。',
|
||||
textDescription:
|
||||
'微光孢子是机械深海生态中的起始个体,体型最小,会先漂浮试探并寻找可吞并目标。',
|
||||
silhouetteDirection: '圆润微型机械球',
|
||||
sizeRatio: 1,
|
||||
visualDescription:
|
||||
'带有浅色发光核心的微型机械鱼苗或孢子体,轮廓圆润,表现出弱小但灵动的初始形象。',
|
||||
visualPromptSeed: 'deep sea glowing mechanical spore',
|
||||
idleMotionDescription:
|
||||
'待机时轻轻漂浮,身体和尾部做小幅摆动,像在适应深海水流。',
|
||||
moveMotionDescription:
|
||||
'移动时核心前探,尾部快速摆动推进,带出轻盈的游动轨迹。',
|
||||
motionPromptSeed: 'soft floating mechanical spore',
|
||||
mergeSourceLevel: null,
|
||||
preyWindow: [1],
|
||||
@@ -1168,6 +1227,9 @@ test('create tab opens compiled agent draft in result refinement page', async ()
|
||||
},
|
||||
]);
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(compiledAgentDraftSession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -1271,6 +1333,13 @@ test('create tab resumes agent workspace when session has no draft profile even
|
||||
stage: 'clarifying',
|
||||
draftProfile: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession({
|
||||
...mockSession,
|
||||
stage: 'clarifying',
|
||||
draftProfile: null,
|
||||
}),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -1316,13 +1385,13 @@ test('opening a compiled draft with a missing agent session falls back to create
|
||||
])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
vi.mocked(getRpgCreationSession).mockRejectedValueOnce(
|
||||
new ApiClientError({
|
||||
message: 'custom world agent session not found',
|
||||
status: 404,
|
||||
code: 'NOT_FOUND',
|
||||
}),
|
||||
);
|
||||
const missingSessionError = new ApiClientError({
|
||||
message: 'custom world agent session not found',
|
||||
status: 404,
|
||||
code: 'NOT_FOUND',
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockRejectedValueOnce(missingSessionError);
|
||||
vi.mocked(getRpgCreationResultView).mockRejectedValueOnce(missingSessionError);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -1699,6 +1768,22 @@ test('restoring an agent workspace ignores a stored session owned by another use
|
||||
expect(window.location.search).toBe('');
|
||||
});
|
||||
|
||||
test('restoring an agent workspace ignores explicit session pointer without local owner after login', async () => {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/?customWorldSessionId=custom-world-agent-session-legacy',
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.search).toBe('');
|
||||
});
|
||||
|
||||
expect(getRpgCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('refreshing platform home ignores stored agent workspace pointer without explicit restore path', async () => {
|
||||
window.sessionStorage.setItem(
|
||||
'genarrative.custom-world-agent-ui.v1',
|
||||
@@ -2243,6 +2328,15 @@ test('failed draft work continues on generation progress view instead of agent w
|
||||
},
|
||||
]);
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue({
|
||||
...buildResultViewForSession({
|
||||
...mockSession,
|
||||
stage: 'error',
|
||||
}),
|
||||
targetStage: 'custom-world-generating',
|
||||
generationViewSource: 'agent-draft-foundation',
|
||||
recoveryAction: 'resume_generation',
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2267,6 +2361,9 @@ test('existing draft sessions open result page refinement instead of agent dialo
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(compiledAgentDraftSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(compiledAgentDraftSession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2306,7 +2403,7 @@ test('agent result view shows publish blocker dialog before publish action when
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue({
|
||||
const blockedSession = {
|
||||
...compiledAgentDraftSession,
|
||||
resultPreview: {
|
||||
...compiledAgentDraftSession.resultPreview!,
|
||||
@@ -2319,7 +2416,11 @@ test('agent result view shows publish blocker dialog before publish action when
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(blockedSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(blockedSession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2424,6 +2525,11 @@ test('agent draft result publishes to gallery from publish panel', async () => {
|
||||
vi.mocked(getRpgCreationSession).mockImplementation(async () =>
|
||||
hasPublishedWorld ? publishedSession : publishReadyDraftSession,
|
||||
);
|
||||
vi.mocked(getRpgCreationResultView).mockImplementation(async () =>
|
||||
buildResultViewForSession(
|
||||
hasPublishedWorld ? publishedSession : publishReadyDraftSession,
|
||||
),
|
||||
);
|
||||
|
||||
function PublishFlowWrapper() {
|
||||
const [selectionStage, setSelectionStage] =
|
||||
@@ -2482,7 +2588,7 @@ test('agent draft result test button enters current draft without publish gate',
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue({
|
||||
const testDraftSession = {
|
||||
...compiledAgentDraftSession,
|
||||
stage: 'ready_to_publish',
|
||||
resultPreview: {
|
||||
@@ -2497,7 +2603,11 @@ test('agent draft result test button enters current draft without publish gate',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(testDraftSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(testDraftSession),
|
||||
);
|
||||
|
||||
function TestDraftWrapper() {
|
||||
const [selectionStage, setSelectionStage] =
|
||||
@@ -2582,7 +2692,7 @@ test('agent result view does not keep legacy publish blockers when preview uses
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue({
|
||||
const publishGateSession = {
|
||||
...compiledAgentDraftSession,
|
||||
stage: 'ready_to_publish',
|
||||
resultPreview: {
|
||||
@@ -2650,7 +2760,11 @@ test('agent result view does not keep legacy publish blockers when preview uses
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(publishGateSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(publishGateSession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2809,6 +2923,9 @@ test('agent draft result back button returns to creation hub without syncing res
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(resultSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(resultSession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2840,7 +2957,7 @@ test('agent draft result back button returns to creation hub without syncing res
|
||||
expect(screen.queryByText('世界档案')).toBeNull();
|
||||
});
|
||||
|
||||
test('agent draft result auto-save persists the latest profile from session draft without result sync action', async () => {
|
||||
test('agent draft result auto-save syncs result profile before persisting backend result view', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const syncedSession = {
|
||||
@@ -2940,6 +3057,35 @@ test('agent draft result auto-save persists the latest profile from session draf
|
||||
},
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(syncedSession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(syncedSession),
|
||||
);
|
||||
vi.mocked(executeRpgCreationAction).mockImplementation(async (_, payload) => ({
|
||||
operation: {
|
||||
operationId:
|
||||
payload.action === 'sync_result_profile'
|
||||
? 'operation-sync-result-profile-1'
|
||||
: 'operation-draft-foundation-1',
|
||||
type: payload.action,
|
||||
status: 'queued',
|
||||
phaseLabel: '已接收请求',
|
||||
phaseDetail:
|
||||
payload.action === 'sync_result_profile'
|
||||
? '正在同步结果页档案。'
|
||||
: '正在准备生成世界底稿。',
|
||||
progress: 10,
|
||||
error: null,
|
||||
},
|
||||
}));
|
||||
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
||||
operationId: 'operation-sync-result-profile-1',
|
||||
type: 'sync_result_profile',
|
||||
status: 'completed',
|
||||
phaseLabel: '结果页档案已同步',
|
||||
phaseDetail: '服务端已根据最新结果页档案刷新会话预览。',
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
@@ -2978,7 +3124,7 @@ test('agent draft result auto-save persists the latest profile from session draf
|
||||
sessionId === 'custom-world-agent-session-1' &&
|
||||
payload?.action === 'sync_result_profile',
|
||||
),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('agent draft result can open from server result preview without embedded legacyResultProfile', async () => {
|
||||
@@ -3021,6 +3167,9 @@ test('agent draft result can open from server result preview without embedded le
|
||||
} satisfies CustomWorldAgentSessionSnapshot;
|
||||
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(previewOnlySession);
|
||||
vi.mocked(getRpgCreationResultView).mockResolvedValue(
|
||||
buildResultViewForSession(previewOnlySession),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
|
||||
40
src/components/rpg-entry/rpgEntryShared.test.ts
Normal file
40
src/components/rpg-entry/rpgEntryShared.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
normalizeRpgEntryAgentBackedProfile,
|
||||
stringifyRpgEntryAgentBackedProfile,
|
||||
} from './rpgEntryShared';
|
||||
|
||||
describe('rpgEntryShared profile save boundary', () => {
|
||||
it('does not rewrite settingText from creatorIntent on the frontend', () => {
|
||||
const profile = {
|
||||
id: 'cwprof_test',
|
||||
settingText: '结果页用户正在编辑的草稿文案',
|
||||
creatorIntent: {
|
||||
worldHook: '海图会在午夜改写群岛航路',
|
||||
playerPremise: '玩家是失忆领航员',
|
||||
openingSituation: '正在禁航区醒来',
|
||||
themeKeywords: ['海雾'],
|
||||
toneDirectives: ['悬疑'],
|
||||
coreConflicts: ['议会隐瞒沉船真相'],
|
||||
keyCharacters: [
|
||||
{
|
||||
name: '顾潮音',
|
||||
role: '守灯人',
|
||||
relationToPlayer: '旧识',
|
||||
hiddenHook: '掌握伪造海图',
|
||||
},
|
||||
],
|
||||
iconicElements: ['会说谎的罗盘'],
|
||||
},
|
||||
} as CustomWorldProfile;
|
||||
|
||||
expect(normalizeRpgEntryAgentBackedProfile(profile)).toBe(profile);
|
||||
expect(
|
||||
JSON.parse(stringifyRpgEntryAgentBackedProfile(profile)),
|
||||
).toMatchObject({
|
||||
settingText: '结果页用户正在编辑的草稿文案',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,9 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { ApiClientError, isTimeoutError } from '../../services/apiClient';
|
||||
import { buildCustomWorldCreatorIntentFoundationText } from '../../services/customWorldCreatorIntent';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
export function resolveRpgEntryErrorMessage(
|
||||
error: unknown,
|
||||
fallback: string,
|
||||
) {
|
||||
export function resolveRpgEntryErrorMessage(error: unknown, fallback: string) {
|
||||
if (isTimeoutError(error)) {
|
||||
if (/拼图/u.test(fallback)) {
|
||||
return '开启拼图创作工作台超时,请确认运行时后端已启动后重试。';
|
||||
@@ -68,24 +64,15 @@ export function buildOptimisticRpgEntryAgentMessage(
|
||||
export function normalizeRpgEntryAgentBackedProfile(
|
||||
profile: CustomWorldProfile,
|
||||
) {
|
||||
const foundationText = buildCustomWorldCreatorIntentFoundationText(
|
||||
profile.creatorIntent,
|
||||
).trim();
|
||||
|
||||
if (!foundationText || foundationText === profile.settingText.trim()) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
return {
|
||||
...profile,
|
||||
settingText: foundationText,
|
||||
} satisfies CustomWorldProfile;
|
||||
// 中文注释:保存前 canonicalize 已迁到 server-rs;
|
||||
// 这里保留透传函数只为了兼容旧导入,不再改写正式 profile 字段。
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function stringifyRpgEntryAgentBackedProfile(
|
||||
profile: CustomWorldProfile,
|
||||
) {
|
||||
return JSON.stringify(normalizeRpgEntryAgentBackedProfile(profile));
|
||||
return JSON.stringify(profile);
|
||||
}
|
||||
|
||||
export function buildRpgEntryCreationHubFallbackItems(
|
||||
@@ -123,13 +110,9 @@ export function buildRpgEntryCreationHubFallbackItems(
|
||||
* 兼容创作链工作包已经接入的旧 helper 命名,避免本轮迁移波及其他并行改动。
|
||||
*/
|
||||
export const resolveRpgCreationErrorMessage = resolveRpgEntryErrorMessage;
|
||||
export const createFailedAgentOperation =
|
||||
createFailedRpgEntryAgentOperation;
|
||||
export const buildOptimisticAgentMessage =
|
||||
buildOptimisticRpgEntryAgentMessage;
|
||||
export const normalizeAgentBackedProfile =
|
||||
normalizeRpgEntryAgentBackedProfile;
|
||||
export const stringifyAgentBackedProfile =
|
||||
stringifyRpgEntryAgentBackedProfile;
|
||||
export const createFailedAgentOperation = createFailedRpgEntryAgentOperation;
|
||||
export const buildOptimisticAgentMessage = buildOptimisticRpgEntryAgentMessage;
|
||||
export const normalizeAgentBackedProfile = normalizeRpgEntryAgentBackedProfile;
|
||||
export const stringifyAgentBackedProfile = stringifyRpgEntryAgentBackedProfile;
|
||||
export const buildCreationHubFallbackItems =
|
||||
buildRpgEntryCreationHubFallbackItems;
|
||||
|
||||
@@ -115,11 +115,6 @@ function buildSession(): CustomWorldAgentSessionSnapshot {
|
||||
|
||||
describe('useRpgCreationEnterWorld', () => {
|
||||
it('Agent 草稿测试进入游戏时优先使用结果页当前 profile,而不是回退到会话快照', async () => {
|
||||
const staleResultProfile = buildProfile({
|
||||
id: 'session-profile',
|
||||
name: '会话旧快照',
|
||||
imageSrc: '/template/old-role.png',
|
||||
});
|
||||
const resultProfile = buildProfile({
|
||||
id: 'draft-profile',
|
||||
name: '结果页真相源',
|
||||
@@ -128,16 +123,21 @@ describe('useRpgCreationEnterWorld', () => {
|
||||
const handleCustomWorldSelect = vi.fn();
|
||||
const setGeneratedCustomWorldProfile = vi.fn();
|
||||
const executePublishWorld = vi.fn(async () => buildSession());
|
||||
const syncAgentCreationResultView = vi.fn();
|
||||
const syncAgentDraftResultProfile = vi.fn(async () => ({
|
||||
profile: resultProfile,
|
||||
view: null,
|
||||
}));
|
||||
|
||||
function Harness() {
|
||||
const { enterWorldForTestFromCurrentResult } = useRpgCreationEnterWorld({
|
||||
isAgentDraftResultView: true,
|
||||
activeAgentSessionId: 'session-1',
|
||||
generatedCustomWorldProfile: resultProfile,
|
||||
agentSessionProfile: staleResultProfile,
|
||||
agentSession: buildSession(),
|
||||
handleCustomWorldSelect,
|
||||
syncAgentDraftResultProfile,
|
||||
executePublishWorld,
|
||||
syncAgentCreationResultView,
|
||||
setGeneratedCustomWorldProfile,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
import type { CustomWorldRuntimeLaunchOptions } from '../platform-entry/platformEntryTypes';
|
||||
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
@@ -9,13 +9,17 @@ type UseRpgCreationEnterWorldParams = {
|
||||
isAgentDraftResultView: boolean;
|
||||
activeAgentSessionId: string | null;
|
||||
generatedCustomWorldProfile: CustomWorldProfile | null;
|
||||
agentSessionProfile: CustomWorldProfile | null;
|
||||
agentSession: CustomWorldAgentSessionSnapshot | null;
|
||||
handleCustomWorldSelect: (
|
||||
customWorldProfile: CustomWorldProfile,
|
||||
options?: CustomWorldRuntimeLaunchOptions,
|
||||
) => void;
|
||||
executePublishWorld: () => Promise<CustomWorldAgentSessionSnapshot | null>;
|
||||
syncAgentDraftResultProfile: (
|
||||
profile: CustomWorldProfile,
|
||||
) => Promise<{ profile: CustomWorldProfile | null; view?: RpgCreationResultView | null }>;
|
||||
executePublishWorld: () => Promise<unknown>;
|
||||
syncAgentCreationResultView: (
|
||||
sessionId: string,
|
||||
) => Promise<RpgCreationResultView | null>;
|
||||
setGeneratedCustomWorldProfile: (profile: CustomWorldProfile | null) => void;
|
||||
};
|
||||
|
||||
@@ -30,10 +34,10 @@ export function useRpgCreationEnterWorld(
|
||||
isAgentDraftResultView,
|
||||
activeAgentSessionId,
|
||||
generatedCustomWorldProfile,
|
||||
agentSessionProfile,
|
||||
agentSession,
|
||||
handleCustomWorldSelect,
|
||||
syncAgentDraftResultProfile,
|
||||
executePublishWorld,
|
||||
syncAgentCreationResultView,
|
||||
setGeneratedCustomWorldProfile,
|
||||
} = params;
|
||||
|
||||
@@ -44,7 +48,7 @@ export function useRpgCreationEnterWorld(
|
||||
|
||||
// 中文注释:作品测试必须复用“结果页当前真相源”。
|
||||
// 用户在结果页看到并可能继续编辑的是 generatedCustomWorldProfile;
|
||||
// 如果这里又回退成会话里的 agentSessionProfile,就会出现
|
||||
// 如果这里又回退成 session 里的旧 preview,就会出现
|
||||
// “结果页看起来已经是新版,但作品测试实际进入的是旧版快照”的错位。
|
||||
if (isAgentDraftResultView && activeAgentSessionId) {
|
||||
setGeneratedCustomWorldProfile(generatedCustomWorldProfile);
|
||||
@@ -73,36 +77,47 @@ export function useRpgCreationEnterWorld(
|
||||
return generatedCustomWorldProfile;
|
||||
}
|
||||
|
||||
if (!agentSessionProfile) {
|
||||
const syncedResult = await syncAgentDraftResultProfile(
|
||||
generatedCustomWorldProfile,
|
||||
);
|
||||
const latestProfile =
|
||||
syncedResult.profile ??
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultView(syncedResult.view);
|
||||
|
||||
if (!latestProfile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setGeneratedCustomWorldProfile(agentSessionProfile);
|
||||
setGeneratedCustomWorldProfile(latestProfile);
|
||||
|
||||
const latestSession = agentSession;
|
||||
const canEnterPublishedWorld =
|
||||
latestSession?.stage === 'published' &&
|
||||
latestSession.resultPreview?.canEnterWorld;
|
||||
syncedResult.view?.session.stage === 'published' &&
|
||||
syncedResult.view.canEnterWorld;
|
||||
|
||||
if (canEnterPublishedWorld) {
|
||||
return agentSessionProfile;
|
||||
const latestView = await syncAgentCreationResultView(activeAgentSessionId);
|
||||
return (
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultView(latestView) ??
|
||||
latestProfile
|
||||
);
|
||||
}
|
||||
|
||||
const publishedSession = await executePublishWorld();
|
||||
await executePublishWorld();
|
||||
const latestView = await syncAgentCreationResultView(activeAgentSessionId);
|
||||
const publishedProfile =
|
||||
rpgCreationPreviewAdapter.buildPreviewFromSession(publishedSession) ??
|
||||
agentSessionProfile;
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultView(latestView) ??
|
||||
latestProfile;
|
||||
|
||||
setGeneratedCustomWorldProfile(publishedProfile);
|
||||
return publishedProfile;
|
||||
}, [
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
agentSessionProfile,
|
||||
executePublishWorld,
|
||||
generatedCustomWorldProfile,
|
||||
isAgentDraftResultView,
|
||||
setGeneratedCustomWorldProfile,
|
||||
syncAgentDraftResultProfile,
|
||||
syncAgentCreationResultView,
|
||||
]);
|
||||
|
||||
const enterWorldFromCurrentResult = useCallback(async () => {
|
||||
|
||||
@@ -4,9 +4,9 @@ import type {
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
|
||||
import {
|
||||
executeRpgCreationAction,
|
||||
getRpgCreationOperation,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from '../../services/rpg-creation';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
normalizeAgentBackedProfile,
|
||||
resolveRpgCreationErrorMessage,
|
||||
stringifyAgentBackedProfile,
|
||||
} from './rpgEntryShared';
|
||||
@@ -27,7 +26,6 @@ import type {
|
||||
type UseRpgCreationResultAutosaveParams = {
|
||||
selectionStage: SelectionStage;
|
||||
activeAgentSessionId: string | null;
|
||||
agentSession: CustomWorldAgentSessionSnapshot | null;
|
||||
generatedCustomWorldProfile: CustomWorldProfile | null;
|
||||
isAgentDraftResultView: boolean;
|
||||
userId: string | null | undefined;
|
||||
@@ -55,8 +53,11 @@ type UseRpgCreationResultAutosaveParams = {
|
||||
syncAgentSessionSnapshot: (
|
||||
sessionId: string,
|
||||
) => Promise<CustomWorldAgentSessionSnapshot | null>;
|
||||
syncAgentCreationResultView: (
|
||||
sessionId: string,
|
||||
) => Promise<RpgCreationResultView | null>;
|
||||
buildDraftResultProfile: (
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
view: RpgCreationResultView | null,
|
||||
) => CustomWorldProfile | null;
|
||||
};
|
||||
|
||||
@@ -70,7 +71,6 @@ export function useRpgCreationResultAutosave(
|
||||
const {
|
||||
selectionStage,
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
generatedCustomWorldProfile,
|
||||
isAgentDraftResultView,
|
||||
userId,
|
||||
@@ -81,6 +81,7 @@ export function useRpgCreationResultAutosave(
|
||||
refreshCustomWorldWorks,
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
syncAgentCreationResultView,
|
||||
buildDraftResultProfile,
|
||||
} = params;
|
||||
|
||||
@@ -118,29 +119,33 @@ export function useRpgCreationResultAutosave(
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedProfile = normalizeAgentBackedProfile(profile);
|
||||
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
|
||||
const requestId = latestAutoSaveRequestIdRef.current + 1;
|
||||
latestAutoSaveRequestIdRef.current = requestId;
|
||||
setCustomWorldAutoSaveState('saving');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
|
||||
try {
|
||||
const mutation =
|
||||
await upsertRpgWorldProfile(
|
||||
normalizedProfile,
|
||||
{
|
||||
sourceAgentSessionId:
|
||||
isAgentDraftResultView && activeAgentSessionId
|
||||
? activeAgentSessionId
|
||||
: null,
|
||||
},
|
||||
);
|
||||
const mutation = await upsertRpgWorldProfile(profile, {
|
||||
sourceAgentSessionId:
|
||||
isAgentDraftResultView && activeAgentSessionId
|
||||
? activeAgentSessionId
|
||||
: null,
|
||||
});
|
||||
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
||||
return mutation;
|
||||
}
|
||||
|
||||
lastAutoSavedProfileSignatureRef.current = profileSignature;
|
||||
const canonicalProfile =
|
||||
normalizeCustomWorldProfileRecord(mutation.entry.profile) ??
|
||||
mutation.entry.profile;
|
||||
// Agent 结果页的界面真相来自 result-view;作品库响应只用于列表与签名回写,
|
||||
// 避免旧兼容响应缺字段时覆盖当前完整编辑态。
|
||||
lastAutoSavedProfileSignatureRef.current = stringifyAgentBackedProfile(
|
||||
isAgentDraftResultView ? profile : canonicalProfile,
|
||||
);
|
||||
if (!isAgentDraftResultView) {
|
||||
setGeneratedCustomWorldProfile(canonicalProfile);
|
||||
}
|
||||
setSavedCustomWorldEntries(mutation.entries);
|
||||
if (userId) {
|
||||
void refreshCustomWorldWorks().catch(() => {});
|
||||
@@ -174,73 +179,8 @@ export function useRpgCreationResultAutosave(
|
||||
refreshCustomWorldWorks,
|
||||
setSavedCustomWorldEntries,
|
||||
setSelectedDetailEntry,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
const syncAgentDraftResultProfile = useCallback(
|
||||
async (profile: CustomWorldProfile) => {
|
||||
if (!activeAgentSessionId) {
|
||||
return {
|
||||
session: null,
|
||||
profile: null,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
const normalizedProfile = normalizeAgentBackedProfile(profile);
|
||||
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
|
||||
const latestSessionProfile = buildDraftResultProfile(agentSession);
|
||||
const latestSessionProfileSignature = latestSessionProfile
|
||||
? stringifyAgentBackedProfile(latestSessionProfile)
|
||||
: '';
|
||||
const shouldRefreshPublishGate = Boolean(
|
||||
agentSession?.resultPreview && !agentSession.resultPreview.publishReady,
|
||||
);
|
||||
|
||||
if (
|
||||
latestSessionProfileSignature === profileSignature &&
|
||||
!shouldRefreshPublishGate
|
||||
) {
|
||||
latestAgentResultSyncSignatureRef.current = profileSignature;
|
||||
return {
|
||||
session: agentSession,
|
||||
profile: normalizeAgentBackedProfile(latestSessionProfile ?? profile),
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
if (
|
||||
latestAgentResultSyncSignatureRef.current === profileSignature &&
|
||||
!shouldRefreshPublishGate
|
||||
) {
|
||||
return {
|
||||
session: agentSession,
|
||||
profile: normalizeAgentBackedProfile(latestSessionProfile ?? profile),
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
// Agent 结果页不再把前端 profile 回写到 session。
|
||||
// 这里只刷新后端结果页快照,避免在采集/生成早期误触 sync_result_profile。
|
||||
const latestSession = await syncAgentSessionSnapshot(activeAgentSessionId);
|
||||
const latestProfile = normalizeAgentBackedProfile(
|
||||
buildDraftResultProfile(latestSession) ?? profile,
|
||||
);
|
||||
if (latestProfile) {
|
||||
setGeneratedCustomWorldProfile(latestProfile);
|
||||
}
|
||||
latestAgentResultSyncSignatureRef.current =
|
||||
stringifyAgentBackedProfile(latestProfile);
|
||||
|
||||
return {
|
||||
session: latestSession,
|
||||
profile: latestProfile,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
},
|
||||
[
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
buildDraftResultProfile,
|
||||
setGeneratedCustomWorldProfile,
|
||||
syncAgentSessionSnapshot,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -290,6 +230,65 @@ export function useRpgCreationResultAutosave(
|
||||
],
|
||||
);
|
||||
|
||||
const syncAgentDraftResultProfile = useCallback(
|
||||
async (profile: CustomWorldProfile) => {
|
||||
if (!activeAgentSessionId) {
|
||||
return {
|
||||
session: null,
|
||||
profile: null,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
const profileSignature = stringifyAgentBackedProfile(profile);
|
||||
const currentView =
|
||||
await syncAgentCreationResultView(activeAgentSessionId);
|
||||
if (!currentView?.canSyncResultProfile) {
|
||||
const latestProfile = buildDraftResultProfile(currentView) ?? profile;
|
||||
if (latestProfile) {
|
||||
setGeneratedCustomWorldProfile(latestProfile);
|
||||
}
|
||||
latestAgentResultSyncSignatureRef.current =
|
||||
stringifyAgentBackedProfile(latestProfile);
|
||||
|
||||
return {
|
||||
session: currentView?.session ?? null,
|
||||
profile: latestProfile,
|
||||
view: currentView,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
if (latestAgentResultSyncSignatureRef.current !== profileSignature) {
|
||||
await executeAgentActionAndWait({
|
||||
action: 'sync_result_profile',
|
||||
profile: profile as unknown as Record<string, unknown>,
|
||||
});
|
||||
latestAgentResultSyncSignatureRef.current = profileSignature;
|
||||
}
|
||||
|
||||
const latestView =
|
||||
await syncAgentCreationResultView(activeAgentSessionId);
|
||||
const latestProfile = buildDraftResultProfile(latestView) ?? profile;
|
||||
if (latestProfile) {
|
||||
setGeneratedCustomWorldProfile(latestProfile);
|
||||
}
|
||||
latestAgentResultSyncSignatureRef.current =
|
||||
stringifyAgentBackedProfile(latestProfile);
|
||||
|
||||
return {
|
||||
session: latestView?.session ?? null,
|
||||
profile: latestProfile,
|
||||
view: latestView,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
},
|
||||
[
|
||||
activeAgentSessionId,
|
||||
buildDraftResultProfile,
|
||||
executeAgentActionAndWait,
|
||||
setGeneratedCustomWorldProfile,
|
||||
syncAgentCreationResultView,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
||||
@@ -313,7 +312,9 @@ export function useRpgCreationResultAutosave(
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSignature = stringifyAgentBackedProfile(generatedCustomWorldProfile);
|
||||
const nextSignature = stringifyAgentBackedProfile(
|
||||
generatedCustomWorldProfile,
|
||||
);
|
||||
if (nextSignature === lastAutoSavedProfileSignatureRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -328,14 +329,16 @@ export function useRpgCreationResultAutosave(
|
||||
void (async () => {
|
||||
isCustomWorldAutoSaveBusyRef.current = true;
|
||||
try {
|
||||
let latestProfileToSave = normalizeAgentBackedProfile(profileToSave);
|
||||
let latestProfileToSave = profileToSave;
|
||||
if (isAgentDraftResultView) {
|
||||
const syncedResult =
|
||||
await syncAgentDraftResultProfile(profileToSave);
|
||||
if (syncedResult.view && !syncedResult.view.canAutosaveLibrary) {
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
return;
|
||||
}
|
||||
// 作品库自动保存优先落同步后 session 重编译出的结果,避免继续保存旧的前端内存态。
|
||||
latestProfileToSave = normalizeAgentBackedProfile(
|
||||
syncedResult.profile ?? profileToSave,
|
||||
);
|
||||
latestProfileToSave = syncedResult.profile ?? profileToSave;
|
||||
}
|
||||
await saveGeneratedCustomWorld(latestProfileToSave);
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
createRpgCreationSession,
|
||||
executeRpgCreationAction,
|
||||
getRpgCreationResultView,
|
||||
getRpgCreationSession,
|
||||
streamRpgCreationMessage,
|
||||
} from '../../services/rpg-creation';
|
||||
@@ -29,7 +30,6 @@ import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
buildOptimisticAgentMessage,
|
||||
createFailedAgentOperation,
|
||||
normalizeAgentBackedProfile,
|
||||
resolveRpgCreationErrorMessage,
|
||||
} from './rpgEntryShared';
|
||||
import type {
|
||||
@@ -40,7 +40,9 @@ import type {
|
||||
|
||||
type UseRpgCreationSessionControllerParams = {
|
||||
userId: string | null | undefined;
|
||||
openLoginModal?: ((postLoginAction?: (() => void) | null) => void) | undefined;
|
||||
openLoginModal?:
|
||||
| ((postLoginAction?: (() => void) | null) => void)
|
||||
| undefined;
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
enterCreateTab?: (() => void) | undefined;
|
||||
@@ -70,12 +72,23 @@ export function useRpgCreationSessionController(
|
||||
const shouldRestoreInitialAgentUiStateRef = useRef(
|
||||
shouldRestoreCustomWorldAgentUiState(),
|
||||
);
|
||||
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
|
||||
const isInitialAgentGenerationRestore =
|
||||
Boolean(initialAgentUiStateRef.current.activeOperationId) &&
|
||||
initialAgentUiStateRef.current.customWorldGenerationSource ===
|
||||
'agent-draft-foundation';
|
||||
const canResolveInitialAgentSessionOwner =
|
||||
!initialAgentSessionId ||
|
||||
!userId ||
|
||||
Boolean(initialAgentUiStateRef.current.ownerUserId) ||
|
||||
isInitialAgentGenerationRestore;
|
||||
const isInitialAgentUiStateOwnedByCurrentUser =
|
||||
!initialAgentUiStateRef.current.ownerUserId ||
|
||||
initialAgentUiStateRef.current.ownerUserId === userId;
|
||||
canResolveInitialAgentSessionOwner &&
|
||||
(!initialAgentUiStateRef.current.ownerUserId ||
|
||||
initialAgentUiStateRef.current.ownerUserId === userId);
|
||||
const isHydratingInitialAgentWorkspaceRef = useRef(
|
||||
Boolean(
|
||||
initialAgentUiStateRef.current.activeSessionId &&
|
||||
initialAgentSessionId &&
|
||||
shouldRestoreInitialAgentUiStateRef.current &&
|
||||
isInitialAgentUiStateOwnedByCurrentUser,
|
||||
),
|
||||
@@ -115,9 +128,12 @@ export function useRpgCreationSessionController(
|
||||
const [pendingAgentUserMessage, setPendingAgentUserMessage] =
|
||||
useState<PendingAgentUserMessage | null>(null);
|
||||
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
|
||||
const [creationTypeError, setCreationTypeError] = useState<string | null>(null);
|
||||
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] =
|
||||
useState<string | null>(null);
|
||||
const [creationTypeError, setCreationTypeError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
useState<CustomWorldProfile | null>(null);
|
||||
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
|
||||
@@ -127,7 +143,10 @@ export function useRpgCreationSessionController(
|
||||
useState<CustomWorldResultViewSource>(null);
|
||||
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
||||
useState<number | null>(null);
|
||||
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(null);
|
||||
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(
|
||||
null,
|
||||
);
|
||||
const latestAgentResultViewOpenRequestIdRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
currentAgentSessionIdRef.current = agentSession?.sessionId ?? null;
|
||||
@@ -191,35 +210,54 @@ export function useRpgCreationSessionController(
|
||||
[userId],
|
||||
);
|
||||
|
||||
const syncAgentSessionSnapshot = useCallback(async (sessionId: string) => {
|
||||
const requestId = latestAgentSessionSyncRequestIdRef.current + 1;
|
||||
latestAgentSessionSyncRequestIdRef.current = requestId;
|
||||
const nextSession = await getRpgCreationSession(sessionId);
|
||||
const mergedSession = mergePendingAgentUserMessageIntoSession(nextSession);
|
||||
const syncAgentSessionSnapshot = useCallback(
|
||||
async (sessionId: string) => {
|
||||
const requestId = latestAgentSessionSyncRequestIdRef.current + 1;
|
||||
latestAgentSessionSyncRequestIdRef.current = requestId;
|
||||
const nextSession = await getRpgCreationSession(sessionId);
|
||||
const mergedSession =
|
||||
mergePendingAgentUserMessageIntoSession(nextSession);
|
||||
|
||||
if (latestAgentSessionSyncRequestIdRef.current === requestId) {
|
||||
setAgentSession(mergedSession);
|
||||
const currentPendingAgentUserMessage = pendingAgentUserMessageRef.current;
|
||||
const hasServerEchoedPendingMessage =
|
||||
currentPendingAgentUserMessage?.sessionId === nextSession.sessionId &&
|
||||
nextSession.messages.some(
|
||||
(message) => message.id === currentPendingAgentUserMessage.message.id,
|
||||
);
|
||||
if (hasServerEchoedPendingMessage) {
|
||||
setPendingAgentUserMessage(null);
|
||||
if (latestAgentSessionSyncRequestIdRef.current === requestId) {
|
||||
setAgentSession(mergedSession);
|
||||
const currentPendingAgentUserMessage =
|
||||
pendingAgentUserMessageRef.current;
|
||||
const hasServerEchoedPendingMessage =
|
||||
currentPendingAgentUserMessage?.sessionId === nextSession.sessionId &&
|
||||
nextSession.messages.some(
|
||||
(message) =>
|
||||
message.id === currentPendingAgentUserMessage.message.id,
|
||||
);
|
||||
if (hasServerEchoedPendingMessage) {
|
||||
setPendingAgentUserMessage(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSession;
|
||||
}, [mergePendingAgentUserMessageIntoSession]);
|
||||
return mergedSession;
|
||||
},
|
||||
[mergePendingAgentUserMessageIntoSession],
|
||||
);
|
||||
|
||||
const syncAgentCreationResultView = useCallback(
|
||||
async (sessionId: string) => {
|
||||
const resultView = await getRpgCreationResultView(sessionId);
|
||||
const mergedSession = mergePendingAgentUserMessageIntoSession(
|
||||
resultView.session,
|
||||
);
|
||||
setAgentSession(mergedSession);
|
||||
return {
|
||||
...resultView,
|
||||
session: mergedSession ?? resultView.session,
|
||||
};
|
||||
},
|
||||
[mergePendingAgentUserMessageIntoSession],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
|
||||
const initialAgentSessionId =
|
||||
initialAgentUiStateRef.current.activeSessionId;
|
||||
|
||||
if (
|
||||
!initialAgentSessionId ||
|
||||
hasAppliedInitialAgentWorkspaceRef.current
|
||||
) {
|
||||
if (!initialAgentSessionId || hasAppliedInitialAgentWorkspaceRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,6 +298,20 @@ export function useRpgCreationSessionController(
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!initialAgentUiStateRef.current.ownerUserId &&
|
||||
!(
|
||||
initialAgentUiStateRef.current.activeOperationId &&
|
||||
initialAgentUiStateRef.current.customWorldGenerationSource ===
|
||||
'agent-draft-foundation'
|
||||
)
|
||||
) {
|
||||
hasAppliedInitialAgentWorkspaceRef.current = true;
|
||||
isHydratingInitialAgentWorkspaceRef.current = false;
|
||||
persistAgentUiState(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
initialAgentUiStateRef.current.ownerUserId &&
|
||||
initialAgentUiStateRef.current.ownerUserId !== userId
|
||||
@@ -283,7 +335,13 @@ export function useRpgCreationSessionController(
|
||||
}
|
||||
|
||||
setSelectionStage('agent-workspace');
|
||||
}, [enterCreateTab, openLoginModal, persistAgentUiState, setSelectionStage, userId]);
|
||||
}, [
|
||||
enterCreateTab,
|
||||
openLoginModal,
|
||||
persistAgentUiState,
|
||||
setSelectionStage,
|
||||
userId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -365,7 +423,10 @@ export function useRpgCreationSessionController(
|
||||
setAgentWorkspaceRestoreError(null);
|
||||
} else {
|
||||
setAgentWorkspaceRestoreError(
|
||||
resolveRpgCreationErrorMessage(error, '读取 Agent 共创工作区失败。'),
|
||||
resolveRpgCreationErrorMessage(
|
||||
error,
|
||||
'读取 Agent 共创工作区失败。',
|
||||
),
|
||||
);
|
||||
}
|
||||
setAgentSession(null);
|
||||
@@ -426,37 +487,32 @@ export function useRpgCreationSessionController(
|
||||
attempt += 1
|
||||
) {
|
||||
await new Promise((resolve) => {
|
||||
window.setTimeout(
|
||||
resolve,
|
||||
AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS,
|
||||
);
|
||||
window.setTimeout(resolve, AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS);
|
||||
});
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestSession = activeAgentSessionId
|
||||
? await syncAgentSessionSnapshot(activeAgentSessionId).catch(
|
||||
const latestResultView = activeAgentSessionId
|
||||
? await syncAgentCreationResultView(activeAgentSessionId).catch(
|
||||
() => null,
|
||||
)
|
||||
: agentSession;
|
||||
: null;
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draftResultProfile =
|
||||
rpgCreationPreviewAdapter.buildPreviewFromSession(
|
||||
latestSession ?? agentSession,
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultView(
|
||||
latestResultView,
|
||||
);
|
||||
if (!draftResultProfile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setGeneratedCustomWorldProfile(
|
||||
normalizeAgentBackedProfile(draftResultProfile),
|
||||
);
|
||||
setGeneratedCustomWorldProfile(draftResultProfile);
|
||||
setAgentDraftGenerationStartedAt(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
@@ -479,7 +535,7 @@ export function useRpgCreationSessionController(
|
||||
customWorldGenerationViewSource,
|
||||
selectionStage,
|
||||
setSelectionStage,
|
||||
syncAgentSessionSnapshot,
|
||||
syncAgentCreationResultView,
|
||||
]);
|
||||
|
||||
const agentDraftSettingPreview = useMemo(
|
||||
@@ -490,25 +546,6 @@ export function useRpgCreationSessionController(
|
||||
() => buildAgentDraftFoundationAnchorEntries(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
const agentDraftResultProfile = useMemo(
|
||||
() => rpgCreationPreviewAdapter.buildPreviewFromSession(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
const shouldAutoOpenAgentDraftResult = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
agentDraftResultProfile &&
|
||||
agentSession &&
|
||||
(agentSession.stage === 'object_refining' ||
|
||||
agentSession.stage === 'visual_refining' ||
|
||||
agentSession.stage === 'long_tail_review' ||
|
||||
agentSession.stage === 'ready_to_publish' ||
|
||||
agentSession.stage === 'published') &&
|
||||
agentSession.draftCards.length > 0,
|
||||
),
|
||||
[agentDraftResultProfile, agentSession],
|
||||
);
|
||||
|
||||
const agentDraftGenerationProgress = useMemo(
|
||||
() =>
|
||||
buildAgentDraftFoundationGenerationProgress(
|
||||
@@ -530,7 +567,11 @@ export function useRpgCreationSessionController(
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoOpenAgentDraftResult || !agentDraftResultProfile) {
|
||||
if (
|
||||
!agentSession ||
|
||||
!activeAgentSessionId ||
|
||||
agentSession.draftCards.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -538,28 +579,63 @@ export function useRpgCreationSessionController(
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionStage === 'agent-workspace') {
|
||||
setGeneratedCustomWorldProfile(agentDraftResultProfile);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
||||
setSelectionStage('custom-world-result');
|
||||
if (
|
||||
selectionStage !== 'agent-workspace' &&
|
||||
selectionStage !== 'custom-world-result'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectionStage === 'custom-world-result' &&
|
||||
!generatedCustomWorldProfile
|
||||
generatedCustomWorldProfile
|
||||
) {
|
||||
setGeneratedCustomWorldProfile(agentDraftResultProfile);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = latestAgentResultViewOpenRequestIdRef.current + 1;
|
||||
latestAgentResultViewOpenRequestIdRef.current = requestId;
|
||||
let cancelled = false;
|
||||
|
||||
void syncAgentCreationResultView(activeAgentSessionId)
|
||||
.then((resultView) => {
|
||||
if (
|
||||
cancelled ||
|
||||
latestAgentResultViewOpenRequestIdRef.current !== requestId ||
|
||||
isAgentDraftResultAutoOpenSuppressedRef.current ||
|
||||
resultView.targetStage !== 'custom-world-result'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultProfile =
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultView(resultView);
|
||||
if (!resultProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneratedCustomWorldProfile(resultProfile);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(
|
||||
resultView.resultViewSource ?? 'agent-draft',
|
||||
);
|
||||
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
||||
if (selectionStage === 'agent-workspace') {
|
||||
setSelectionStage('custom-world-result');
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
agentDraftResultProfile,
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
generatedCustomWorldProfile,
|
||||
selectionStage,
|
||||
setSelectionStage,
|
||||
shouldAutoOpenAgentDraftResult,
|
||||
syncAgentCreationResultView,
|
||||
]);
|
||||
|
||||
const openRpgAgentWorkspace = useCallback(
|
||||
@@ -689,26 +765,26 @@ export function useRpgCreationSessionController(
|
||||
kind: 'warning',
|
||||
text: errorMessage,
|
||||
});
|
||||
setAgentSession((current) =>
|
||||
{
|
||||
const mergedCurrentSession = mergePendingAgentUserMessageIntoSession(
|
||||
current,
|
||||
pendingMessagePayload,
|
||||
);
|
||||
return mergedCurrentSession
|
||||
? {
|
||||
...mergedCurrentSession,
|
||||
messages: [...mergedCurrentSession.messages, warningMessage],
|
||||
updatedAt: warningMessage.createdAt,
|
||||
}
|
||||
: current;
|
||||
},
|
||||
);
|
||||
setAgentSession((current) => {
|
||||
const mergedCurrentSession = mergePendingAgentUserMessageIntoSession(
|
||||
current,
|
||||
pendingMessagePayload,
|
||||
);
|
||||
return mergedCurrentSession
|
||||
? {
|
||||
...mergedCurrentSession,
|
||||
messages: [...mergedCurrentSession.messages, warningMessage],
|
||||
updatedAt: warningMessage.createdAt,
|
||||
}
|
||||
: current;
|
||||
});
|
||||
setPendingAgentUserMessage(null);
|
||||
setStreamingAgentReplyText('');
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
} finally {
|
||||
if (activeAgentReplyAbortControllerRef.current === replyAbortController) {
|
||||
if (
|
||||
activeAgentReplyAbortControllerRef.current === replyAbortController
|
||||
) {
|
||||
activeAgentReplyAbortControllerRef.current = null;
|
||||
}
|
||||
if (!replyAbortController.signal.aborted) {
|
||||
@@ -780,9 +856,7 @@ export function useRpgCreationSessionController(
|
||||
|
||||
const setNormalizedGeneratedCustomWorldProfile = useCallback(
|
||||
(profile: CustomWorldProfile | null) => {
|
||||
setGeneratedCustomWorldProfile(
|
||||
profile ? normalizeAgentBackedProfile(profile) : null,
|
||||
);
|
||||
setGeneratedCustomWorldProfile(profile);
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -807,7 +881,8 @@ export function useRpgCreationSessionController(
|
||||
|
||||
return {
|
||||
initialAgentSessionId:
|
||||
shouldRestoreInitialAgentUiStateRef.current
|
||||
shouldRestoreInitialAgentUiStateRef.current &&
|
||||
isInitialAgentUiStateOwnedByCurrentUser
|
||||
? (initialAgentUiStateRef.current.activeSessionId ?? null)
|
||||
: null,
|
||||
isCreatingAgentSession,
|
||||
@@ -837,7 +912,10 @@ export function useRpgCreationSessionController(
|
||||
setAgentDraftGenerationStartedAt,
|
||||
agentDraftSettingPreview,
|
||||
agentDraftAnchorPreviewEntries,
|
||||
agentDraftResultProfile,
|
||||
agentDraftResultProfile:
|
||||
rpgCreationPreviewAdapter.buildPreviewFromResultPreview(
|
||||
agentSession?.resultPreview,
|
||||
),
|
||||
agentDraftGenerationProgress,
|
||||
isAgentDraftGenerationView,
|
||||
isAgentDraftResultView,
|
||||
@@ -848,6 +926,7 @@ export function useRpgCreationSessionController(
|
||||
releaseAgentDraftResultAutoOpenSuppression,
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
syncAgentCreationResultView,
|
||||
openRpgAgentWorkspace,
|
||||
submitAgentMessage,
|
||||
executeAgentAction,
|
||||
|
||||
@@ -5,9 +5,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
import { WorldType, type CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
executeRpgCreationAction,
|
||||
getRpgCreationOperation,
|
||||
upsertRpgWorldProfile,
|
||||
} from '../../services/rpg-creation';
|
||||
import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave';
|
||||
@@ -109,16 +111,42 @@ function buildSession(
|
||||
};
|
||||
}
|
||||
|
||||
function buildResultView(
|
||||
overrides: Partial<RpgCreationResultView> = {},
|
||||
): RpgCreationResultView {
|
||||
const session = overrides.session ?? buildSession();
|
||||
return {
|
||||
session,
|
||||
profile: null,
|
||||
profileSource: 'none',
|
||||
targetStage: 'agent-workspace',
|
||||
generationViewSource: null,
|
||||
resultViewSource: null,
|
||||
canAutosaveLibrary: false,
|
||||
canSyncResultProfile: false,
|
||||
publishReady: false,
|
||||
canEnterWorld: false,
|
||||
blockerCount: 0,
|
||||
recoveryAction: 'continue_agent',
|
||||
recoveryReason: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('RPG Agent 草稿恢复', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('作品摘要已有对象数量但 session 没有 draftProfile 时恢复 Agent 页面', async () => {
|
||||
const syncAgentSessionSnapshot = vi.fn(async () =>
|
||||
buildSession({
|
||||
stage: 'clarifying',
|
||||
draftProfile: null,
|
||||
const syncAgentCreationResultView = vi.fn(async () =>
|
||||
buildResultView({
|
||||
session: buildSession({
|
||||
stage: 'clarifying',
|
||||
draftProfile: null,
|
||||
}),
|
||||
targetStage: 'agent-workspace',
|
||||
recoveryAction: 'continue_agent',
|
||||
}),
|
||||
);
|
||||
const setSelectionStage = vi.fn();
|
||||
@@ -150,9 +178,9 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
refreshCustomWorldWorks: vi.fn(async () => []),
|
||||
refreshPublishedGallery: vi.fn(async () => []),
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
buildDraftResultProfile: (session) =>
|
||||
(session?.draftProfile as CustomWorldProfile | null) ?? null,
|
||||
syncAgentCreationResultView,
|
||||
buildDraftResultProfile: (view) =>
|
||||
(view?.profile as CustomWorldProfile | null) ?? null,
|
||||
suppressAgentDraftResultAutoOpen,
|
||||
releaseAgentDraftResultAutoOpenSuppression: vi.fn(),
|
||||
resetAutoSaveTrackingToIdle: vi.fn(),
|
||||
@@ -183,7 +211,7 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
|
||||
expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
|
||||
expect(suppressAgentDraftResultAutoOpen).toHaveBeenCalled();
|
||||
expect(persistAgentUiState).toHaveBeenCalledWith('agent-session-1', null);
|
||||
expect(setGeneratedCustomWorldProfile).toHaveBeenLastCalledWith(null);
|
||||
@@ -192,7 +220,7 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
expect(setSelectionStage).not.toHaveBeenCalledWith('custom-world-result');
|
||||
});
|
||||
|
||||
it('Agent 结果页自动保存只刷新 session draftProfile,不触发 sync_result_profile', async () => {
|
||||
it('Agent 结果页自动保存先回写 session,再保存后端 result-view profile', async () => {
|
||||
const oldProfile = buildProfile('旧前端快照');
|
||||
const latestProfile = {
|
||||
...buildProfile('服务端草稿快照'),
|
||||
@@ -203,6 +231,36 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
draftProfile: latestProfile as unknown as Record<string, unknown>,
|
||||
});
|
||||
const syncAgentSessionSnapshot = vi.fn(async () => latestSession);
|
||||
const syncAgentCreationResultView = vi.fn(async () =>
|
||||
buildResultView({
|
||||
session: latestSession,
|
||||
profile: latestProfile,
|
||||
profileSource: 'result_preview',
|
||||
targetStage: 'custom-world-result',
|
||||
resultViewSource: 'agent-draft',
|
||||
canAutosaveLibrary: true,
|
||||
canSyncResultProfile: true,
|
||||
recoveryAction: 'open_result',
|
||||
}),
|
||||
);
|
||||
vi.mocked(executeRpgCreationAction).mockResolvedValue({
|
||||
operation: {
|
||||
operationId: 'operation-sync-result',
|
||||
type: 'sync_result_profile',
|
||||
status: 'running',
|
||||
phaseLabel: '结果页同步中',
|
||||
phaseDetail: '正在同步结果页。',
|
||||
progress: 50,
|
||||
},
|
||||
});
|
||||
vi.mocked(getRpgCreationOperation).mockResolvedValue({
|
||||
operationId: 'operation-sync-result',
|
||||
type: 'sync_result_profile',
|
||||
status: 'completed',
|
||||
phaseLabel: '结果页已同步',
|
||||
phaseDetail: '结果页已同步。',
|
||||
progress: 100,
|
||||
});
|
||||
|
||||
vi.mocked(upsertRpgWorldProfile).mockResolvedValue({
|
||||
entry: {
|
||||
@@ -230,16 +288,6 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
useRpgCreationResultAutosave({
|
||||
selectionStage: 'custom-world-result',
|
||||
activeAgentSessionId: 'agent-session-1',
|
||||
agentSession: buildSession({
|
||||
stage: 'object_refining',
|
||||
draftProfile: oldProfile as unknown as Record<string, unknown>,
|
||||
resultPreview: {
|
||||
publishReady: false,
|
||||
blockers: [],
|
||||
qualityFindings: [],
|
||||
sourceLabel: '旧预览',
|
||||
} as never,
|
||||
}),
|
||||
generatedCustomWorldProfile: oldProfile,
|
||||
isAgentDraftResultView: true,
|
||||
userId: 'user-1',
|
||||
@@ -250,8 +298,9 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
refreshCustomWorldWorks: vi.fn(async () => []),
|
||||
persistAgentUiState: vi.fn(),
|
||||
syncAgentSessionSnapshot,
|
||||
buildDraftResultProfile: (session) =>
|
||||
(session?.draftProfile as CustomWorldProfile | null) ?? null,
|
||||
syncAgentCreationResultView,
|
||||
buildDraftResultProfile: (view) =>
|
||||
(view?.profile as CustomWorldProfile | null) ?? null,
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -266,13 +315,23 @@ describe('RPG Agent 草稿恢复', () => {
|
||||
vi.useRealTimers();
|
||||
|
||||
expect(syncAgentSessionSnapshot).toHaveBeenCalledWith('agent-session-1');
|
||||
expect(upsertRpgWorldProfile).toHaveBeenCalledWith(latestProfile, {
|
||||
sourceAgentSessionId: 'agent-session-1',
|
||||
expect(syncAgentCreationResultView).toHaveBeenCalledWith('agent-session-1');
|
||||
expect(executeRpgCreationAction).toHaveBeenCalledWith('agent-session-1', {
|
||||
action: 'sync_result_profile',
|
||||
profile: expect.objectContaining({
|
||||
id: oldProfile.id,
|
||||
name: oldProfile.name,
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
vi.mocked(executeRpgCreationAction).mock.calls.some(
|
||||
([, payload]) => payload?.action === 'sync_result_profile',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(upsertRpgWorldProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: latestProfile.id,
|
||||
name: latestProfile.name,
|
||||
summary: latestProfile.summary,
|
||||
}),
|
||||
{
|
||||
sourceAgentSessionId: 'agent-session-1',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
@@ -22,10 +20,7 @@ import {
|
||||
unpublishRpgEntryWorldProfile,
|
||||
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
normalizeRpgEntryAgentBackedProfile,
|
||||
resolveRpgEntryErrorMessage,
|
||||
} from './rpgEntryShared';
|
||||
import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
|
||||
import type {
|
||||
CustomWorldAutoSaveState,
|
||||
CustomWorldGenerationViewSource,
|
||||
@@ -48,18 +43,14 @@ type UseRpgEntryLibraryDetailParams = {
|
||||
setSavedCustomWorldEntries: (
|
||||
entries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
||||
) => void;
|
||||
setGeneratedCustomWorldProfile: (
|
||||
profile: CustomWorldProfile | null,
|
||||
) => void;
|
||||
setGeneratedCustomWorldProfile: (profile: CustomWorldProfile | null) => void;
|
||||
setCustomWorldError: (error: string | null) => void;
|
||||
setCustomWorldAutoSaveError: (error: string | null) => void;
|
||||
setCustomWorldAutoSaveState: (state: CustomWorldAutoSaveState) => void;
|
||||
setCustomWorldGenerationViewSource: (
|
||||
source: CustomWorldGenerationViewSource,
|
||||
) => void;
|
||||
setCustomWorldResultViewSource: (
|
||||
source: CustomWorldResultViewSource,
|
||||
) => void;
|
||||
setCustomWorldResultViewSource: (source: CustomWorldResultViewSource) => void;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
setPlatformTabToCreate: () => void;
|
||||
setPlatformError: (error: string | null) => void;
|
||||
@@ -73,11 +64,11 @@ type UseRpgEntryLibraryDetailParams = {
|
||||
operationId: string | null,
|
||||
generationSource?: 'agent-draft-foundation' | null,
|
||||
) => void;
|
||||
syncAgentSessionSnapshot: (
|
||||
syncAgentCreationResultView: (
|
||||
sessionId: string,
|
||||
) => Promise<CustomWorldAgentSessionSnapshot | null>;
|
||||
) => Promise<RpgCreationResultView | null>;
|
||||
buildDraftResultProfile: (
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
view: RpgCreationResultView | null,
|
||||
) => CustomWorldProfile | null;
|
||||
suppressAgentDraftResultAutoOpen: () => void;
|
||||
releaseAgentDraftResultAutoOpenSuppression: () => void;
|
||||
@@ -85,14 +76,6 @@ type UseRpgEntryLibraryDetailParams = {
|
||||
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
|
||||
};
|
||||
|
||||
const AGENT_RESULT_STAGES = new Set([
|
||||
'object_refining',
|
||||
'visual_refining',
|
||||
'long_tail_review',
|
||||
'ready_to_publish',
|
||||
'published',
|
||||
]);
|
||||
|
||||
function isMissingRpgEntryAgentSessionError(error: unknown) {
|
||||
return (
|
||||
error instanceof ApiClientError &&
|
||||
@@ -127,7 +110,7 @@ export function useRpgEntryLibraryDetail(
|
||||
refreshCustomWorldWorks,
|
||||
refreshPublishedGallery,
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
syncAgentCreationResultView,
|
||||
buildDraftResultProfile,
|
||||
suppressAgentDraftResultAutoOpen,
|
||||
releaseAgentDraftResultAutoOpenSuppression,
|
||||
@@ -225,11 +208,8 @@ export function useRpgEntryLibraryDetail(
|
||||
setCustomWorldResultViewSource(null);
|
||||
setCustomWorldError(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
const normalizedProfile = normalizeRpgEntryAgentBackedProfile(
|
||||
entry.profile,
|
||||
);
|
||||
setGeneratedCustomWorldProfile(normalizedProfile);
|
||||
markAutoSavedProfile(normalizedProfile);
|
||||
setGeneratedCustomWorldProfile(entry.profile);
|
||||
markAutoSavedProfile(entry.profile);
|
||||
setCustomWorldAutoSaveState('saved');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldError(null);
|
||||
@@ -262,34 +242,28 @@ export function useRpgEntryLibraryDetail(
|
||||
resetAutoSaveTrackingToIdle();
|
||||
|
||||
try {
|
||||
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
|
||||
const nextProfile = buildDraftResultProfile(latestSession);
|
||||
const shouldOpenAgentWorkspace =
|
||||
!latestSession?.draftProfile ||
|
||||
!latestSession.stage ||
|
||||
!AGENT_RESULT_STAGES.has(latestSession.stage);
|
||||
const resultView = await syncAgentCreationResultView(work.sessionId);
|
||||
const nextProfile = buildDraftResultProfile(resultView);
|
||||
|
||||
const shouldResumeFailedGenerationView =
|
||||
!nextProfile &&
|
||||
/失败/u.test(`${work.stageLabel ?? ''}${work.summary ?? ''}`);
|
||||
|
||||
if (shouldResumeFailedGenerationView) {
|
||||
if (resultView?.targetStage === 'custom-world-generating') {
|
||||
// 生成过程中失败的草稿要回到生成过程页承接错误处理,避免误回 Agent 对话。
|
||||
suppressAgentDraftResultAutoOpen();
|
||||
persistAgentUiState(
|
||||
work.sessionId,
|
||||
null,
|
||||
'agent-draft-foundation',
|
||||
resultView.generationViewSource ?? 'agent-draft-foundation',
|
||||
);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
||||
setCustomWorldGenerationViewSource(
|
||||
resultView.generationViewSource ?? 'agent-draft-foundation',
|
||||
);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('custom-world-generating');
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldOpenAgentWorkspace) {
|
||||
if (resultView?.targetStage === 'agent-workspace') {
|
||||
// 还没有服务端草稿真相源时只能恢复 Agent,对象数量等摘要字段不能决定结果页入口。
|
||||
suppressAgentDraftResultAutoOpen();
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
@@ -302,12 +276,10 @@ export function useRpgEntryLibraryDetail(
|
||||
|
||||
releaseAgentDraftResultAutoOpenSuppression();
|
||||
if (!nextProfile) {
|
||||
persistAgentUiState(
|
||||
work.sessionId,
|
||||
null,
|
||||
'agent-draft-foundation',
|
||||
persistAgentUiState(work.sessionId, null, 'agent-draft-foundation');
|
||||
setPlatformError(
|
||||
'当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。',
|
||||
);
|
||||
setPlatformError('当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。');
|
||||
setPlatformTabToCreate();
|
||||
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
||||
setSelectionStage('custom-world-generating');
|
||||
@@ -315,12 +287,12 @@ export function useRpgEntryLibraryDetail(
|
||||
}
|
||||
|
||||
persistAgentUiState(work.sessionId, null);
|
||||
setGeneratedCustomWorldProfile(
|
||||
normalizeRpgEntryAgentBackedProfile(nextProfile),
|
||||
setGeneratedCustomWorldProfile(nextProfile);
|
||||
setCustomWorldResultViewSource(
|
||||
resultView?.resultViewSource ?? 'agent-draft',
|
||||
);
|
||||
setCustomWorldResultViewSource('agent-draft');
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('custom-world-result');
|
||||
setSelectionStage(resultView?.targetStage ?? 'custom-world-result');
|
||||
return;
|
||||
} catch (error) {
|
||||
if (isMissingRpgEntryAgentSessionError(error)) {
|
||||
@@ -391,7 +363,7 @@ export function useRpgEntryLibraryDetail(
|
||||
setSavedCustomWorldEntries,
|
||||
setSelectionStage,
|
||||
suppressAgentDraftResultAutoOpen,
|
||||
syncAgentSessionSnapshot,
|
||||
syncAgentCreationResultView,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user