fix rpg agent refresh restore route

This commit is contained in:
2026-04-26 23:01:42 +08:00
parent b7a507044f
commit 3198370089
6 changed files with 169 additions and 4 deletions

View File

@@ -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 工作区。

View File

@@ -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({

View File

@@ -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(<TestWrapper withAuth />);
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(<TestWrapper withAuth />);
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();

View File

@@ -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,

View File

@@ -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);
});

View File

@@ -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 {