From 3198370089b66b353850509393eb8f7c6b15e525 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 26 Apr 2026 23:01:42 +0800 Subject: [PATCH] fix rpg agent refresh restore route --- ...TEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md | 2 + .../PlatformEntryFlowShellImpl.tsx | 8 +++- ...gEntryFlowShell.agent.interaction.test.tsx | 41 ++++++++++++++++ .../useRpgCreationSessionController.ts | 32 ++++++++++++- src/services/customWorldAgentUiState.test.ts | 47 +++++++++++++++++++ src/services/customWorldAgentUiState.ts | 43 +++++++++++++++++ 6 files changed, 169 insertions(+), 4 deletions(-) diff --git a/docs/technical/FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md b/docs/technical/FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md index d418eb8c..57535d24 100644 --- a/docs/technical/FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md +++ b/docs/technical/FRONTEND_INDEPENDENT_PAGE_ROUTES_2026-04-25.md @@ -12,6 +12,7 @@ - 正式主应用内部页面路径由 `src/routing/appPageRoutes.ts` 统一维护,不在组件里散落硬编码字符串。 - `/puzzle` 与 `/big-fish` 保持为玩法调试直达入口;正式链路中的拼图和大鱼运行页使用 `/runtime/puzzle`、`/runtime/big-fish`,避免语义冲突。 - 独立路径先解决页面阶段语义和浏览器前进后退;依赖运行中内存对象的详情页、结果页和运行页直接刷新后仍允许回退到平台首页或展示现有恢复态,不在本轮扩展资源 ID 深链加载。 +- `sessionStorage` 里的 RPG Agent 恢复指针只能在当前路径属于 `/creation/rpg/*`,或 URL 显式携带 `customWorldSessionId / customWorldOperationId / customWorldGenerationSource` 时生效;刷新平台首页、分类页、作品详情页时不能被本地残留指针强制跳到 `/creation/rpg/agent`。 ## 页面路径表 @@ -46,3 +47,4 @@ 2. 从页面内切换到结果页、运行页或返回首页时,浏览器路径同步更新。 3. 浏览器后退/前进能驱动 `selectionStage` 回到对应页面。 4. `/puzzle` 与 `/big-fish` 仍进入原有玩法调试直达页。 +5. 仅有 `sessionStorage` 残留 RPG Agent 指针时,刷新 `/` 仍停留平台首页;刷新 `/creation/rpg/agent` 才恢复对应 Agent 工作区。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index ec717465..acedecd6 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -52,7 +52,10 @@ import { deleteBigFishWork, listBigFishWorks, } from '../../services/big-fish-works'; -import { readCustomWorldAgentUiState } from '../../services/customWorldAgentUiState'; +import { + readCustomWorldAgentUiState, + shouldRestoreCustomWorldAgentUiState, +} from '../../services/customWorldAgentUiState'; import { buildBigFishGenerationAnchorEntries, buildMiniGameDraftGenerationProgress, @@ -423,7 +426,8 @@ export function PlatformEntryFlowShellImpl({ >(null); const hadReadableProtectedDataRef = useRef(false); const hasInitialAgentSession = Boolean( - readCustomWorldAgentUiState().activeSessionId, + readCustomWorldAgentUiState().activeSessionId && + shouldRestoreCustomWorldAgentUiState(), ); const platformBootstrap = usePlatformEntryBootstrap({ diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 69125496..2b9674ca 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -1556,6 +1556,47 @@ test('restoring an agent workspace ignores a stored session owned by another use expect(window.location.search).toBe(''); }); +test('refreshing platform home ignores stored agent workspace pointer without explicit restore path', async () => { + window.sessionStorage.setItem( + 'genarrative.custom-world-agent-ui.v1', + JSON.stringify({ + activeSessionId: 'custom-world-agent-session-1', + activeOperationId: null, + ownerUserId: 'user-1', + }), + ); + + render(); + + expect(await screen.findByRole('button', { name: '创作' })).toBeTruthy(); + expect(screen.queryByText(/Agent工作区/u)).toBeNull(); + expect(getRpgCreationSession).not.toHaveBeenCalled(); + expect(window.location.pathname).toBe('/'); +}); + +test('refreshing RPG agent path restores stored agent workspace pointer', async () => { + window.history.replaceState(null, '', '/creation/rpg/agent'); + window.sessionStorage.setItem( + 'genarrative.custom-world-agent-ui.v1', + JSON.stringify({ + activeSessionId: 'custom-world-agent-session-1', + activeOperationId: null, + ownerUserId: 'user-1', + }), + ); + + render(); + + await waitFor(() => { + expect(getRpgCreationSession).toHaveBeenCalledWith( + 'custom-world-agent-session-1', + ); + }); + expect( + await screen.findByText('Agent工作区:custom-world-agent-session-1'), + ).toBeTruthy(); +}); + test('new creation entry maps raw bearer token errors to user-facing auth copy', async () => { const user = userEvent.setup(); diff --git a/src/components/rpg-entry/useRpgCreationSessionController.ts b/src/components/rpg-entry/useRpgCreationSessionController.ts index 29a04ed2..99940fe8 100644 --- a/src/components/rpg-entry/useRpgCreationSessionController.ts +++ b/src/components/rpg-entry/useRpgCreationSessionController.ts @@ -15,6 +15,7 @@ import { } from '../../services/customWorldAgentGenerationProgress'; import { readCustomWorldAgentUiState, + shouldRestoreCustomWorldAgentUiState, writeCustomWorldAgentUiState, } from '../../services/customWorldAgentUiState'; import { @@ -66,12 +67,16 @@ export function useRpgCreationSessionController( onSessionOpened, } = params; const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState()); + const shouldRestoreInitialAgentUiStateRef = useRef( + shouldRestoreCustomWorldAgentUiState(), + ); const isInitialAgentUiStateOwnedByCurrentUser = !initialAgentUiStateRef.current.ownerUserId || initialAgentUiStateRef.current.ownerUserId === userId; const isHydratingInitialAgentWorkspaceRef = useRef( Boolean( initialAgentUiStateRef.current.activeSessionId && + shouldRestoreInitialAgentUiStateRef.current && isInitialAgentUiStateOwnedByCurrentUser, ), ); @@ -88,6 +93,7 @@ export function useRpgCreationSessionController( const [activeAgentSessionId, setActiveAgentSessionId] = useState< string | null >(() => + shouldRestoreInitialAgentUiStateRef.current && isInitialAgentUiStateOwnedByCurrentUser ? (initialAgentUiStateRef.current.activeSessionId ?? null) : null, @@ -95,6 +101,7 @@ export function useRpgCreationSessionController( const [activeAgentOperationId, setActiveAgentOperationId] = useState< string | null >(() => + shouldRestoreInitialAgentUiStateRef.current && isInitialAgentUiStateOwnedByCurrentUser ? (initialAgentUiStateRef.current.activeOperationId ?? null) : null, @@ -209,7 +216,25 @@ export function useRpgCreationSessionController( useEffect(() => { const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId; - if (!initialAgentSessionId || hasAppliedInitialAgentWorkspaceRef.current) { + if ( + !initialAgentSessionId || + hasAppliedInitialAgentWorkspaceRef.current + ) { + return; + } + + if ( + initialAgentUiStateRef.current.ownerUserId && + userId && + initialAgentUiStateRef.current.ownerUserId !== userId + ) { + hasAppliedInitialAgentWorkspaceRef.current = true; + isHydratingInitialAgentWorkspaceRef.current = false; + persistAgentUiState(null, null); + return; + } + + if (!shouldRestoreInitialAgentUiStateRef.current) { return; } @@ -781,7 +806,10 @@ export function useRpgCreationSessionController( }, []); return { - initialAgentSessionId: initialAgentUiStateRef.current.activeSessionId ?? null, + initialAgentSessionId: + shouldRestoreInitialAgentUiStateRef.current + ? (initialAgentUiStateRef.current.activeSessionId ?? null) + : null, isCreatingAgentSession, activeAgentSessionId, activeAgentOperationId, diff --git a/src/services/customWorldAgentUiState.test.ts b/src/services/customWorldAgentUiState.test.ts index 72aeefb2..e9ce7d6f 100644 --- a/src/services/customWorldAgentUiState.test.ts +++ b/src/services/customWorldAgentUiState.test.ts @@ -3,6 +3,7 @@ import { expect, test } from 'vitest'; import { clearCustomWorldAgentUiState, readCustomWorldAgentUiState, + shouldRestoreCustomWorldAgentUiState, writeCustomWorldAgentUiState, } from './customWorldAgentUiState'; @@ -73,3 +74,49 @@ test('custom world agent ui state reads from query first and persists to session clearCustomWorldAgentUiState(env); expect(readCustomWorldAgentUiState(env)).toEqual({}); }); + +test('custom world agent ui state only auto restores stored pointers on RPG creation paths', () => { + const sessionStorage = createMemoryStorage(); + sessionStorage.setItem( + 'genarrative.custom-world-agent-ui.v1', + JSON.stringify({ + activeSessionId: 'session-1', + ownerUserId: 'user-1', + }), + ); + + expect( + shouldRestoreCustomWorldAgentUiState({ + location: { + pathname: '/', + search: '', + }, + history: null, + sessionStorage, + }), + ).toBe(false); + + expect( + shouldRestoreCustomWorldAgentUiState({ + location: { + pathname: '/creation/rpg/agent', + search: '', + }, + history: null, + sessionStorage, + }), + ).toBe(true); +}); + +test('custom world agent ui state restores explicit query pointers on any main path', () => { + expect( + shouldRestoreCustomWorldAgentUiState({ + location: { + pathname: '/', + search: '?customWorldSessionId=session-1', + }, + history: null, + sessionStorage: createMemoryStorage(), + }), + ).toBe(true); +}); diff --git a/src/services/customWorldAgentUiState.ts b/src/services/customWorldAgentUiState.ts index ac27041a..5300fcd3 100644 --- a/src/services/customWorldAgentUiState.ts +++ b/src/services/customWorldAgentUiState.ts @@ -56,6 +56,49 @@ function normalizeGenerationSource(value: unknown) { return value === 'agent-draft-foundation' ? value : null; } +function hasExplicitAgentUiStateQuery( + params: URLSearchParams, +) { + return ( + params.has(CUSTOM_WORLD_AGENT_SESSION_QUERY_KEY) || + params.has(CUSTOM_WORLD_AGENT_OPERATION_QUERY_KEY) || + params.has(CUSTOM_WORLD_GENERATION_SOURCE_QUERY_KEY) + ); +} + +function normalizePathname(value: string | undefined) { + const pathname = value?.trim().toLowerCase() ?? ''; + if (!pathname || pathname === '/') { + return '/'; + } + + return pathname.replace(/\/+$/u, ''); +} + +function isRpgCreationRestorePath(pathname: string | undefined) { + const normalizedPathname = normalizePathname(pathname); + return ( + normalizedPathname === '/creation/rpg' || + normalizedPathname.startsWith('/creation/rpg/') + ); +} + +export function shouldRestoreCustomWorldAgentUiState( + env?: CustomWorldAgentUiEnvironment, +) { + const resolved = resolveEnvironment(env); + const params = new URLSearchParams(resolved.location?.search ?? ''); + + // URL 显式恢复参数优先于当前路径,用于支持外部分享或登录回跳后的深链恢复。 + if (hasExplicitAgentUiStateQuery(params)) { + return true; + } + + // sessionStorage 里的残留指针只能在 RPG 创作页面生效, + // 避免刷新平台首页时被旧工作区状态强制带到 Agent 页面。 + return isRpgCreationRestorePath(resolved.location?.pathname); +} + export function readCustomWorldAgentUiState( env?: CustomWorldAgentUiEnvironment, ): CustomWorldAgentUiState {