Files
Genarrative/src/components/rpg-entry/useRpgEntryBootstrap.ts
T
AIGameCreator App 4ef3dbd677 合并主分支最新改动
同步 master 的最新功能与修复
保留 AI 游戏创作客户端分支现有实现

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
#	scripts/dev.mjs
#	server-rs/crates/api-server/src/editor_screen_background_decision.rs
#	server-rs/crates/api-server/src/modules/admin.rs
#	src/components/rpg-entry/RpgEntryHomeView.tsx
2026-07-14 13:05:45 +08:00

426 lines
13 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import type {
CustomWorldWorkSummary,
} from '../../../packages/shared/src/contracts/customWorldAgent';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
PlatformBrowseHistoryEntry,
PlatformBrowseHistoryWriteEntry,
ProfileDashboardSummary,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
import { listRpgCreationWorks } from '../../services/rpg-creation/index';
import {
listRpgEntryWorldGallery,
listRpgEntryWorldLibrary,
listRpgProfileBrowseHistory,
listRpgProfileSaveArchives,
resumeRpgProfileSaveArchive,
upsertRpgProfileBrowseHistory,
} from '../../services/rpg-entry';
import {
RUNTIME_BACKGROUND_AUTH_OPTIONS,
type RuntimeRequestOptions,
} from '../../services/rpg-runtime/rpgRuntimeRequest';
import type { CustomWorldProfile } from '../../types';
import type { PlatformHomeTab } from './RpgEntryHomeView';
import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
const PLATFORM_BOOTSTRAP_AUTH_OPTIONS = RUNTIME_BACKGROUND_AUTH_OPTIONS;
type UseRpgEntryBootstrapParams = {
user: AuthUser | null | undefined;
canAccessProtectedData?: boolean | undefined;
getProfileDashboard: (
options?: RuntimeRequestOptions,
) => Promise<ProfileDashboardSummary | null>;
handleContinueGame: (
snapshot?: HydratedSavedGameSnapshot | null,
) => void;
hasInitialAgentSession: boolean;
initialPlatformTab?: PlatformHomeTab;
};
export function useRpgEntryBootstrap(
params: UseRpgEntryBootstrapParams,
) {
const {
user,
canAccessProtectedData = Boolean(user),
getProfileDashboard,
handleContinueGame,
hasInitialAgentSession,
initialPlatformTab = 'home',
} = params;
const isAuthenticated = Boolean(user);
const canReadProtectedData = Boolean(user) && canAccessProtectedData;
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
undefined,
);
const hasCompletedPlatformBootstrapRef = useRef(false);
const hasExplicitPlatformTabSelectionRef = useRef(false);
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
CustomWorldLibraryEntry<CustomWorldProfile>[]
>([]);
const [customWorldWorkEntries, setCustomWorldWorkEntries] = useState<
CustomWorldWorkSummary[]
>([]);
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
CustomWorldGalleryCard[]
>([]);
const [historyEntries, setHistoryEntries] = useState<
PlatformBrowseHistoryEntry[]
>([]);
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
const [platformTab, setPlatformTabState] =
useState<PlatformHomeTab>(initialPlatformTab);
const [platformError, setPlatformError] = useState<string | null>(null);
const [dashboardError, setDashboardError] = useState<string | null>(null);
const [historyError, setHistoryError] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const [isLoadingPlatform, setIsLoadingPlatform] = useState(false);
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
const [isResumingSaveWorldKey, setIsResumingSaveWorldKey] = useState<
string | null
>(null);
const [profileDashboard, setProfileDashboard] =
useState<ProfileDashboardSummary | null>(null);
const setPlatformTab = useCallback((nextTab: PlatformHomeTab) => {
// 区分“平台首屏默认落点”和“用户/流程显式切换”。
// 一旦显式切过 Tab,就不能再被首屏异步请求回刷成首页或存档。
hasExplicitPlatformTabSelectionRef.current = true;
setPlatformTabState(nextTab);
}, []);
const refreshProfileDashboard = useCallback(async () => {
if (!user || !canReadProtectedData) {
setProfileDashboard(null);
setDashboardError(null);
setIsLoadingDashboard(false);
return;
}
setIsLoadingDashboard(true);
setDashboardError(null);
try {
setProfileDashboard(
await getProfileDashboard(PLATFORM_BOOTSTRAP_AUTH_OPTIONS),
);
} catch (error) {
setDashboardError(
resolveRpgEntryErrorMessage(error, '读取个人数据看板失败。'),
);
} finally {
setIsLoadingDashboard(false);
}
}, [canReadProtectedData, getProfileDashboard, user]);
const refreshCustomWorldWorks = useCallback(async () => {
if (!user || !canReadProtectedData) {
setCustomWorldWorkEntries([]);
return [];
}
const nextItems = await listRpgCreationWorks(
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
);
setCustomWorldWorkEntries(nextItems);
return nextItems;
}, [canReadProtectedData, user]);
const refreshPublishedGallery = useCallback(async () => {
const nextEntries = await listRpgEntryWorldGallery();
setPublishedGalleryEntries(nextEntries);
return nextEntries;
}, []);
const refreshSavedCustomWorldLibrary = useCallback(async () => {
if (!user || !canReadProtectedData) {
setSavedCustomWorldEntries([]);
return [];
}
const nextEntries = await listRpgEntryWorldLibrary(
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
);
setSavedCustomWorldEntries(nextEntries);
return nextEntries;
}, [canReadProtectedData, user]);
const refreshSaveArchives = useCallback(async () => {
if (!user || !canReadProtectedData) {
setSaveEntries([]);
setSaveError(null);
return [];
}
setSaveError(null);
try {
const nextEntries = await listRpgProfileSaveArchives(
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
);
setSaveEntries(nextEntries);
return nextEntries;
} catch (error) {
setSaveError(resolveRpgEntryErrorMessage(error, '读取存档列表失败。'));
return [];
}
}, [canReadProtectedData, user]);
const appendBrowseHistoryEntry = useCallback(
async (entry: PlatformBrowseHistoryWriteEntry) => {
setHistoryError(null);
try {
const syncedEntries = await upsertRpgProfileBrowseHistory(
entry,
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
);
setHistoryEntries(syncedEntries);
} catch (error) {
setHistoryError(
resolveRpgEntryErrorMessage(error, '写入浏览历史失败。'),
);
}
},
[],
);
const handleResumeSaveEntry = useCallback(
async (entry: ProfileSaveArchiveSummary) => {
if (!user || !canReadProtectedData || isResumingSaveWorldKey) {
return;
}
setIsResumingSaveWorldKey(entry.worldKey);
setSaveError(null);
try {
const resumedArchive = await resumeRpgProfileSaveArchive(entry.worldKey);
setSaveEntries((currentEntries) =>
currentEntries.map((currentEntry) =>
currentEntry.worldKey === resumedArchive.entry.worldKey
? resumedArchive.entry
: currentEntry,
),
);
handleContinueGame(resumedArchive.snapshot);
} catch (error) {
setSaveError(resolveRpgEntryErrorMessage(error, '恢复存档失败。'));
} finally {
setIsResumingSaveWorldKey(null);
}
},
[canReadProtectedData, handleContinueGame, isResumingSaveWorldKey, user],
);
useEffect(() => {
let isActive = true;
const nextPlatformBootstrapUserId = user?.id ?? null;
const shouldApplyInitialPlatformTab =
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId;
if (shouldApplyInitialPlatformTab) {
// 在请求发出前先占位,避免首屏请求未完成时用户切了 Tab,
// 返回结果又被误判成“还没初始化过”并强制跳回默认页。
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
}
void (async () => {
const shouldShowBlockingLoading =
!hasCompletedPlatformBootstrapRef.current;
setHistoryEntries([]);
setHistoryError(null);
setSaveError(null);
setIsLoadingPlatform(shouldShowBlockingLoading);
setPlatformError(null);
setIsLoadingDashboard(
canReadProtectedData && shouldShowBlockingLoading,
);
setDashboardError(null);
if (!canReadProtectedData) {
setSavedCustomWorldEntries([]);
setCustomWorldWorkEntries([]);
setSaveEntries([]);
setProfileDashboard(null);
}
try {
const [
libraryEntriesResult,
workEntriesResult,
galleryEntriesResult,
dashboardResult,
historyResult,
saveArchivesResult,
] = await Promise.allSettled([
canReadProtectedData
? listRpgEntryWorldLibrary(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
: Promise.resolve([]),
canReadProtectedData
? listRpgCreationWorks(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
: Promise.resolve([]),
listRpgEntryWorldGallery(),
canReadProtectedData
? getProfileDashboard(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
: Promise.resolve(null),
canReadProtectedData
? listRpgProfileBrowseHistory(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
: Promise.resolve([]),
canReadProtectedData
? listRpgProfileSaveArchives(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
: Promise.resolve([]),
]);
if (!isActive) {
return;
}
if (libraryEntriesResult.status === 'fulfilled') {
setSavedCustomWorldEntries(libraryEntriesResult.value);
} else {
setSavedCustomWorldEntries([]);
}
if (workEntriesResult.status === 'fulfilled') {
setCustomWorldWorkEntries(workEntriesResult.value);
} else {
setCustomWorldWorkEntries([]);
}
if (galleryEntriesResult.status === 'fulfilled') {
setPublishedGalleryEntries(galleryEntriesResult.value);
} else {
// 中文注释:公开广场只影响首页展示,失败时降级为空列表;
// 私有作品库和创作作品列表的受保护失败才需要阻塞提示。
setPublishedGalleryEntries([]);
}
if (
(canReadProtectedData &&
libraryEntriesResult.status === 'rejected') ||
(canReadProtectedData &&
workEntriesResult.status === 'rejected')
) {
const platformFailure =
libraryEntriesResult.status === 'rejected'
? libraryEntriesResult.reason
: workEntriesResult.status === 'rejected'
? workEntriesResult.reason
: null;
setPlatformError(
resolveRpgEntryErrorMessage(platformFailure, '读取平台数据失败。'),
);
}
if (dashboardResult.status === 'fulfilled') {
setProfileDashboard(dashboardResult.value);
} else if (canReadProtectedData) {
setProfileDashboard(null);
setDashboardError(
resolveRpgEntryErrorMessage(
dashboardResult.reason,
'读取个人数据看板失败。',
),
);
}
if (historyResult.status === 'fulfilled') {
setHistoryEntries(historyResult.value);
} else if (canReadProtectedData) {
setHistoryError(
resolveRpgEntryErrorMessage(historyResult.reason, '读取浏览历史失败。'),
);
}
if (saveArchivesResult.status === 'fulfilled') {
setSaveEntries(saveArchivesResult.value);
} else if (canReadProtectedData) {
setSaveEntries([]);
setSaveError(
resolveRpgEntryErrorMessage(
saveArchivesResult.reason,
'读取存档列表失败。',
),
);
}
if (
shouldApplyInitialPlatformTab &&
!hasInitialAgentSession &&
!hasExplicitPlatformTabSelectionRef.current
) {
// 中文注释:新用户也先进入推荐页;真正受保护的动作再单独做登录门禁。
setPlatformTabState(initialPlatformTab);
}
} finally {
if (isActive) {
hasCompletedPlatformBootstrapRef.current = true;
setIsLoadingPlatform(false);
setIsLoadingDashboard(false);
}
}
})();
return () => {
isActive = false;
};
}, [
canReadProtectedData,
getProfileDashboard,
hasInitialAgentSession,
initialPlatformTab,
user,
]);
return {
isAuthenticated,
canReadProtectedData,
platformTab,
setPlatformTab,
savedCustomWorldEntries,
setSavedCustomWorldEntries,
customWorldWorkEntries,
setCustomWorldWorkEntries,
publishedGalleryEntries,
setPublishedGalleryEntries,
historyEntries,
setHistoryEntries,
saveEntries,
setSaveEntries,
platformError,
setPlatformError,
dashboardError,
setDashboardError,
historyError,
setHistoryError,
saveError,
setSaveError,
isLoadingPlatform,
isLoadingDashboard,
isResumingSaveWorldKey,
profileDashboard,
setProfileDashboard,
refreshProfileDashboard,
refreshCustomWorldWorks,
refreshPublishedGallery,
refreshSavedCustomWorldLibrary,
refreshSaveArchives,
appendBrowseHistoryEntry,
handleResumeSaveEntry,
};
}
/**
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
*/
export const useRpgCreationPlatformBootstrap = useRpgEntryBootstrap;