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;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,4 +42,35 @@ describe('normalizeCustomWorldProfileRecord role asset descriptions', () => {
|
||||
expect(profile?.storyNpcs[0]?.actionDescription).toContain('印信');
|
||||
expect(profile?.storyNpcs[0]?.sceneVisualDescription).toContain('议会厅');
|
||||
});
|
||||
|
||||
it('保留 Agent 发布门槛需要的顶层 worldHook 和 playerPremise', () => {
|
||||
const profile = normalizeCustomWorldProfileRecord({
|
||||
name: '雾港归航',
|
||||
settingText: '海雾旧案',
|
||||
summary: '海雾会吞掉记错航线的人。',
|
||||
worldHook: '在失真的海图上追查一场被篡改的沉船事故。',
|
||||
playerPremise: '玩家是返乡调查旧案的守灯人。',
|
||||
sceneChapterBlueprints: [
|
||||
{
|
||||
id: 'scene-chapter-1',
|
||||
sceneId: 'landmark-1',
|
||||
title: '失灯港',
|
||||
acts: [
|
||||
{
|
||||
id: 'act-1',
|
||||
title: '第一幕',
|
||||
summary: '玩家在雾港发现灯册被改写。',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(profile?.worldHook).toBe(
|
||||
'在失真的海图上追查一场被篡改的沉船事故。',
|
||||
);
|
||||
expect(profile?.playerPremise).toBe('玩家是返乡调查旧案的守灯人。');
|
||||
expect(profile?.sceneChapterBlueprints?.[0]?.acts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1050,6 +1050,17 @@ function normalizeProfile(value: unknown): CustomWorldProfile | null {
|
||||
const summary = toText(value.summary);
|
||||
const tone = toText(value.tone);
|
||||
const playerGoal = toText(value.playerGoal);
|
||||
const creatorIntentRecord = isRecord(value.creatorIntent)
|
||||
? value.creatorIntent
|
||||
: null;
|
||||
const worldHook = toText(
|
||||
value.worldHook,
|
||||
toText(creatorIntentRecord?.worldHook, toText(value.summary, settingText || name)),
|
||||
);
|
||||
const playerPremise = toText(
|
||||
value.playerPremise,
|
||||
toText(creatorIntentRecord?.playerPremise, playerGoal),
|
||||
);
|
||||
const majorFactions = toStringArray(value.majorFactions);
|
||||
const coreConflicts = toStringArray(value.coreConflicts);
|
||||
const resolvedCoreConflicts =
|
||||
@@ -1093,6 +1104,8 @@ function normalizeProfile(value: unknown): CustomWorldProfile | null {
|
||||
summary,
|
||||
tone,
|
||||
playerGoal,
|
||||
worldHook,
|
||||
playerPremise,
|
||||
templateWorldType,
|
||||
compatibilityTemplateWorldType,
|
||||
majorFactions,
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
buildCustomWorldRoleBatchPrompt,
|
||||
buildCustomWorldRoleOutlineBatchJsonRepairPrompt,
|
||||
buildCustomWorldRoleOutlineBatchPrompt,
|
||||
buildCustomWorldSceneImagePrompt,
|
||||
buildCustomWorldStoryGraphJsonRepairPrompt,
|
||||
buildCustomWorldStoryGraphPrompt,
|
||||
buildCustomWorldThemePackJsonRepairPrompt,
|
||||
@@ -1951,11 +1950,7 @@ export async function generateCustomWorldSceneImage({
|
||||
size = '1280*720',
|
||||
referenceImageSrc,
|
||||
}: CustomWorldSceneImageRequest): Promise<CustomWorldSceneImageResult> {
|
||||
const resolvedPrompt =
|
||||
prompt?.trim() ||
|
||||
buildCustomWorldSceneImagePrompt(profile, landmark, userPrompt, {
|
||||
hasReferenceImage: Boolean(referenceImageSrc?.trim()),
|
||||
});
|
||||
const resolvedPrompt = prompt?.trim() || userPrompt?.trim() || '';
|
||||
const resolvedNegativePrompt =
|
||||
negativePrompt?.trim() || DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT;
|
||||
const controller = new AbortController();
|
||||
@@ -1975,9 +1970,25 @@ export async function generateCustomWorldSceneImage({
|
||||
worldName: profile.name,
|
||||
landmarkId: landmark.id,
|
||||
landmarkName: landmark.name,
|
||||
prompt: resolvedPrompt,
|
||||
...(prompt?.trim() ? { prompt: prompt.trim() } : {}),
|
||||
userPrompt: resolvedPrompt,
|
||||
negativePrompt: resolvedNegativePrompt,
|
||||
size,
|
||||
profile: {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
subtitle: profile.subtitle,
|
||||
summary: profile.summary,
|
||||
tone: profile.tone,
|
||||
playerGoal: profile.playerGoal,
|
||||
settingText: profile.settingText,
|
||||
},
|
||||
landmark: {
|
||||
id: landmark.id,
|
||||
name: landmark.name,
|
||||
description: landmark.description,
|
||||
dangerLevel: landmark.dangerLevel,
|
||||
},
|
||||
...(referenceImageSrc?.trim()
|
||||
? { referenceImageSrc: referenceImageSrc.trim() }
|
||||
: {}),
|
||||
|
||||
@@ -45,6 +45,7 @@ test('custom world agent ui state reads from query first and persists to session
|
||||
{
|
||||
activeSessionId: 'session-1',
|
||||
activeOperationId: 'operation-1',
|
||||
customWorldGenerationSource: 'agent-draft-foundation',
|
||||
ownerUserId: 'user-1',
|
||||
},
|
||||
env,
|
||||
@@ -52,15 +53,20 @@ test('custom world agent ui state reads from query first and persists to session
|
||||
|
||||
expect(currentUrl).toContain('customWorldSessionId=session-1');
|
||||
expect(currentUrl).toContain('customWorldOperationId=operation-1');
|
||||
expect(currentUrl).toContain(
|
||||
'customWorldGenerationSource=agent-draft-foundation',
|
||||
);
|
||||
expect(readCustomWorldAgentUiState(env)).toEqual({
|
||||
activeSessionId: 'session-1',
|
||||
activeOperationId: 'operation-1',
|
||||
customWorldGenerationSource: 'agent-draft-foundation',
|
||||
});
|
||||
|
||||
currentUrl = '/play';
|
||||
expect(readCustomWorldAgentUiState(env)).toEqual({
|
||||
activeSessionId: 'session-1',
|
||||
activeOperationId: 'operation-1',
|
||||
customWorldGenerationSource: 'agent-draft-foundation',
|
||||
ownerUserId: 'user-1',
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { CustomWorldAgentUiState } from '../types';
|
||||
|
||||
export const CUSTOM_WORLD_AGENT_SESSION_QUERY_KEY = 'customWorldSessionId';
|
||||
export const CUSTOM_WORLD_AGENT_OPERATION_QUERY_KEY = 'customWorldOperationId';
|
||||
export const CUSTOM_WORLD_GENERATION_SOURCE_QUERY_KEY =
|
||||
'customWorldGenerationSource';
|
||||
export const CUSTOM_WORLD_AGENT_UI_STATE_STORAGE_KEY =
|
||||
'genarrative.custom-world-agent-ui.v1';
|
||||
|
||||
@@ -50,6 +52,10 @@ function normalizeValue(value: unknown) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function normalizeGenerationSource(value: unknown) {
|
||||
return value === 'agent-draft-foundation' ? value : null;
|
||||
}
|
||||
|
||||
export function readCustomWorldAgentUiState(
|
||||
env?: CustomWorldAgentUiEnvironment,
|
||||
): CustomWorldAgentUiState {
|
||||
@@ -62,9 +68,16 @@ export function readCustomWorldAgentUiState(
|
||||
activeOperationId: normalizeValue(
|
||||
params.get(CUSTOM_WORLD_AGENT_OPERATION_QUERY_KEY),
|
||||
),
|
||||
customWorldGenerationSource: normalizeGenerationSource(
|
||||
params.get(CUSTOM_WORLD_GENERATION_SOURCE_QUERY_KEY),
|
||||
),
|
||||
};
|
||||
|
||||
if (stateFromQuery.activeSessionId || stateFromQuery.activeOperationId) {
|
||||
if (
|
||||
stateFromQuery.activeSessionId ||
|
||||
stateFromQuery.activeOperationId ||
|
||||
stateFromQuery.customWorldGenerationSource
|
||||
) {
|
||||
return stateFromQuery;
|
||||
}
|
||||
|
||||
@@ -80,6 +93,9 @@ export function readCustomWorldAgentUiState(
|
||||
return {
|
||||
activeSessionId: normalizeValue(parsed.activeSessionId),
|
||||
activeOperationId: normalizeValue(parsed.activeOperationId),
|
||||
customWorldGenerationSource: normalizeGenerationSource(
|
||||
parsed.customWorldGenerationSource,
|
||||
),
|
||||
ownerUserId: normalizeValue(parsed.ownerUserId),
|
||||
};
|
||||
} catch {
|
||||
@@ -95,10 +111,14 @@ export function writeCustomWorldAgentUiState(
|
||||
const resolved = resolveEnvironment(env);
|
||||
const activeSessionId = normalizeValue(state.activeSessionId);
|
||||
const activeOperationId = normalizeValue(state.activeOperationId);
|
||||
const customWorldGenerationSource = normalizeGenerationSource(
|
||||
state.customWorldGenerationSource,
|
||||
);
|
||||
const ownerUserId = normalizeValue(state.ownerUserId);
|
||||
const nextState: CustomWorldAgentUiState = {
|
||||
activeSessionId,
|
||||
activeOperationId,
|
||||
customWorldGenerationSource,
|
||||
ownerUserId,
|
||||
};
|
||||
|
||||
@@ -116,6 +136,15 @@ export function writeCustomWorldAgentUiState(
|
||||
params.delete(CUSTOM_WORLD_AGENT_OPERATION_QUERY_KEY);
|
||||
}
|
||||
|
||||
if (customWorldGenerationSource) {
|
||||
params.set(
|
||||
CUSTOM_WORLD_GENERATION_SOURCE_QUERY_KEY,
|
||||
customWorldGenerationSource,
|
||||
);
|
||||
} else {
|
||||
params.delete(CUSTOM_WORLD_GENERATION_SOURCE_QUERY_KEY);
|
||||
}
|
||||
|
||||
const search = params.toString();
|
||||
const nextUrl = search
|
||||
? `${resolved.location.pathname}?${search}`
|
||||
@@ -124,7 +153,7 @@ export function writeCustomWorldAgentUiState(
|
||||
}
|
||||
|
||||
if (resolved.sessionStorage) {
|
||||
if (activeSessionId || activeOperationId) {
|
||||
if (activeSessionId || activeOperationId || customWorldGenerationSource) {
|
||||
resolved.sessionStorage.setItem(
|
||||
CUSTOM_WORLD_AGENT_UI_STATE_STORAGE_KEY,
|
||||
JSON.stringify(nextState),
|
||||
|
||||
@@ -29,6 +29,7 @@ export type CustomWorldCoverSourceType = 'default' | 'uploaded' | 'generated';
|
||||
export type CustomWorldAgentUiState = {
|
||||
activeSessionId?: string | null;
|
||||
activeOperationId?: string | null;
|
||||
customWorldGenerationSource?: 'agent-draft-foundation' | null;
|
||||
ownerUserId?: string | null;
|
||||
};
|
||||
|
||||
@@ -397,6 +398,16 @@ export interface CustomWorldProfile {
|
||||
summary: string;
|
||||
tone: string;
|
||||
playerGoal: string;
|
||||
/**
|
||||
* 发布门槛直接读取的世界一句话钩子。
|
||||
* Agent 结果页回写 session 时需要保留该字段,避免只剩 UI 归一化字段导致后端误判缺失。
|
||||
*/
|
||||
worldHook?: string | null;
|
||||
/**
|
||||
* 发布门槛直接读取的玩家身份与切入前提。
|
||||
* 即使 creatorIntent / anchorContent 中已有结构化信息,也要保留顶层字段作为 SpacetimeDB 发布快照的稳定兼容槽位。
|
||||
*/
|
||||
playerPremise?: string | null;
|
||||
cover?: CustomWorldCoverProfile | null;
|
||||
templateWorldType: WorldTemplateType;
|
||||
compatibilityTemplateWorldType?: WorldTemplateType | null;
|
||||
|
||||
Reference in New Issue
Block a user