Auto-open draft result after foundation completes

This commit is contained in:
2026-04-25 10:52:39 +08:00
parent 35c2bce6f1
commit 03acbc5cb1
31 changed files with 36472 additions and 232 deletions

View File

@@ -1470,7 +1470,7 @@ test('big fish draft card restores the bound agent session and opens the result
throw new Error('Missing big fish draft card');
}
await user.click(within(card).getByRole('button', { name: //u }));
await user.click(card);
await waitFor(() => {
expect(getBigFishCreationSession).toHaveBeenCalledWith(
@@ -1522,6 +1522,70 @@ test('starting draft generation leaves the agent workspace and shows the generat
expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
});
test('refresh restores running draft generation progress instead of agent workspace', async () => {
window.history.replaceState(
null,
'',
'/?customWorldSessionId=custom-world-agent-session-1&customWorldOperationId=operation-draft-foundation-1&customWorldGenerationSource=agent-draft-foundation',
);
vi.mocked(getRpgCreationOperation).mockResolvedValue({
operationId: 'operation-draft-foundation-1',
type: 'draft_foundation',
status: 'running',
phaseLabel: '生成世界底稿',
phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
progress: 38,
error: null,
});
render(<TestWrapper withAuth />);
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
expect(screen.queryByText(/Agent/u)).toBeNull();
expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
});
test('failed draft work continues on generation progress view instead of agent workspace', async () => {
const user = userEvent.setup();
vi.mocked(listRpgCreationWorks).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: 'clarifying',
stageLabel: '生成失败待处理',
playableNpcCount: 0,
landmarkCount: 0,
roleVisualReadyCount: 0,
roleAnimationReadyCount: 0,
roleAssetSummaryLabel: null,
sessionId: 'custom-world-agent-session-1',
profileId: null,
canResume: true,
canEnterWorld: false,
},
]);
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
render(<TestWrapper withAuth />);
await openCreationHub(user);
expect(await screen.findByText('失败中的潮雾列岛')).toBeTruthy();
await user.click(await screen.findByRole('button', { name: //u }));
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
expect(screen.queryByText(/Agent/u)).toBeNull();
});
test('existing draft sessions open result page refinement instead of agent dialog', async () => {
const user = userEvent.setup();

View File

@@ -22,6 +22,7 @@ type UseRpgCreationAgentOperationPollingParams = {
persistAgentUiState: (
sessionId: string | null,
operationId: string | null,
generationSource?: 'agent-draft-foundation' | null,
) => void;
syncAgentSessionSnapshot: (
sessionId: string,
@@ -68,7 +69,15 @@ export function useRpgCreationAgentOperationPolling(
nextOperation.status === 'completed' ||
nextOperation.status === 'failed'
) {
persistAgentUiState(activeAgentSessionId, null);
persistAgentUiState(
activeAgentSessionId,
nextOperation.type === 'draft_foundation'
? activeAgentOperationId
: null,
nextOperation.type === 'draft_foundation'
? 'agent-draft-foundation'
: null,
);
await syncAgentSessionSnapshot(activeAgentSessionId).catch(
() => null,
);

View File

@@ -50,6 +50,7 @@ type UseRpgCreationResultAutosaveParams = {
persistAgentUiState: (
sessionId: string | null,
operationId: string | null,
generationSource?: 'agent-draft-foundation' | null,
) => void;
syncAgentSessionSnapshot: (
sessionId: string,

View File

@@ -51,6 +51,9 @@ type PendingAgentUserMessage = {
message: CustomWorldAgentSessionSnapshot['messages'][number];
};
const AGENT_DRAFT_RESULT_AUTO_OPEN_MAX_ATTEMPTS = 12;
const AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS = 900;
export function useRpgCreationSessionController(
params: UseRpgCreationSessionControllerParams,
) {
@@ -162,12 +165,17 @@ export function useRpgCreationSessionController(
);
const persistAgentUiState = useCallback(
(nextSessionId: string | null, nextOperationId: string | null) => {
(
nextSessionId: string | null,
nextOperationId: string | null,
nextGenerationSource: CustomWorldGenerationViewSource = null,
) => {
setActiveAgentSessionId(nextSessionId);
setActiveAgentOperationId(nextOperationId);
writeCustomWorldAgentUiState({
activeSessionId: nextSessionId,
activeOperationId: nextOperationId,
customWorldGenerationSource: nextGenerationSource,
// 工作区 session 是按 userId 持久化的,恢复指针必须绑定当前登录用户,
// 避免切换账号或复用旧 URL 时反复请求不属于当前用户的 session 产生 404。
ownerUserId: nextSessionId ? userId : null,
@@ -211,6 +219,16 @@ export function useRpgCreationSessionController(
if (!hasRequestedInitialAgentWorkspaceAuthRef.current) {
hasRequestedInitialAgentWorkspaceAuthRef.current = true;
openLoginModal?.(() => {
if (
initialAgentUiStateRef.current.activeOperationId &&
initialAgentUiStateRef.current.customWorldGenerationSource ===
'agent-draft-foundation'
) {
setCustomWorldGenerationViewSource('agent-draft-foundation');
setSelectionStage('custom-world-generating');
return;
}
setSelectionStage('agent-workspace');
});
}
@@ -228,6 +246,17 @@ export function useRpgCreationSessionController(
}
hasAppliedInitialAgentWorkspaceRef.current = true;
if (
initialAgentUiStateRef.current.activeOperationId &&
initialAgentUiStateRef.current.customWorldGenerationSource ===
'agent-draft-foundation'
) {
setCustomWorldGenerationViewSource('agent-draft-foundation');
setCustomWorldResultViewSource(null);
setSelectionStage('custom-world-generating');
return;
}
setSelectionStage('agent-workspace');
}, [enterCreateTab, openLoginModal, persistAgentUiState, setSelectionStage, userId]);
@@ -365,8 +394,23 @@ export function useRpgCreationSessionController(
}
let cancelled = false;
const timeoutId = window.setTimeout(() => {
void (async () => {
void (async () => {
for (
let attempt = 1;
attempt <= AGENT_DRAFT_RESULT_AUTO_OPEN_MAX_ATTEMPTS;
attempt += 1
) {
await new Promise((resolve) => {
window.setTimeout(
resolve,
AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS,
);
});
if (cancelled) {
return;
}
const latestSession = activeAgentSessionId
? await syncAgentSessionSnapshot(activeAgentSessionId).catch(
() => null,
@@ -382,10 +426,7 @@ export function useRpgCreationSessionController(
latestSession ?? agentSession,
);
if (!draftResultProfile) {
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage('agent-workspace');
return;
continue;
}
setGeneratedCustomWorldProfile(
@@ -395,12 +436,16 @@ export function useRpgCreationSessionController(
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('agent-draft');
setSelectionStage('custom-world-result');
})();
}, 900);
return;
}
if (!cancelled) {
setAgentDraftGenerationStartedAt(null);
}
})();
return () => {
cancelled = true;
window.clearTimeout(timeoutId);
};
}, [
activeAgentSessionId,
@@ -678,7 +723,11 @@ export function useRpgCreationSessionController(
payload,
);
setAgentOperation(operation);
persistAgentUiState(activeAgentSessionId, operation.operationId);
persistAgentUiState(
activeAgentSessionId,
operation.operationId,
isDraftFoundationAction ? 'agent-draft-foundation' : null,
);
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
@@ -694,7 +743,11 @@ export function useRpgCreationSessionController(
error: errorMessage,
}),
);
persistAgentUiState(activeAgentSessionId, null);
persistAgentUiState(
activeAgentSessionId,
null,
isDraftFoundationAction ? 'agent-draft-foundation' : null,
);
}
},
[activeAgentSessionId, persistAgentUiState, setSelectionStage],

View File

@@ -67,6 +67,7 @@ type UseRpgEntryLibraryDetailParams = {
persistAgentUiState: (
sessionId: string | null,
operationId: string | null,
generationSource?: 'agent-draft-foundation' | null,
) => void;
syncAgentSessionSnapshot: (
sessionId: string,
@@ -244,7 +245,30 @@ export function useRpgEntryLibraryDetail(
work.playableNpcCount <= 0 && work.landmarkCount <= 0;
try {
if (shouldOpenAgentWorkspace) {
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
const nextProfile = buildDraftResultProfile(latestSession);
const shouldResumeFailedGenerationView =
!nextProfile &&
//u.test(`${work.stageLabel ?? ''}${work.summary ?? ''}`);
if (shouldResumeFailedGenerationView) {
// 生成过程中失败的草稿要回到生成过程页承接错误处理,避免误回 Agent 对话。
suppressAgentDraftResultAutoOpen();
persistAgentUiState(
work.sessionId,
null,
'agent-draft-foundation',
);
setGeneratedCustomWorldProfile(null);
setCustomWorldGenerationViewSource('agent-draft-foundation');
setCustomWorldResultViewSource(null);
setPlatformTabToCreate();
setSelectionStage('custom-world-generating');
return;
}
if (shouldOpenAgentWorkspace && !nextProfile) {
// 仅八锚点未整理成底稿时才恢复 Agent 对话工作区。
suppressAgentDraftResultAutoOpen();
persistAgentUiState(work.sessionId, null);
@@ -256,13 +280,16 @@ export function useRpgEntryLibraryDetail(
}
releaseAgentDraftResultAutoOpenSuppression();
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
const nextProfile = buildDraftResultProfile(latestSession);
if (!nextProfile) {
persistAgentUiState(work.sessionId, null);
persistAgentUiState(
work.sessionId,
null,
'agent-draft-foundation',
);
setPlatformError('当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。');
setPlatformTabToCreate();
setSelectionStage('agent-workspace');
setCustomWorldGenerationViewSource('agent-draft-foundation');
setSelectionStage('custom-world-generating');
return;
}