Auto-open draft result after foundation completes
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -50,6 +50,7 @@ type UseRpgCreationResultAutosaveParams = {
|
||||
persistAgentUiState: (
|
||||
sessionId: string | null,
|
||||
operationId: string | null,
|
||||
generationSource?: 'agent-draft-foundation' | null,
|
||||
) => void;
|
||||
syncAgentSessionSnapshot: (
|
||||
sessionId: string,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user