Files
Genarrative/src/components/rpg-entry/useRpgEntryBootstrap.ts
2026-05-09 18:24:08 +08:00

418 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
};
export function useRpgEntryBootstrap(
params: UseRpgEntryBootstrapParams,
) {
const {
user,
canAccessProtectedData = Boolean(user),
getProfileDashboard,
handleContinueGame,
hasInitialAgentSession,
} = params;
const isAuthenticated = Boolean(user);
const canReadProtectedData = Boolean(user) && canAccessProtectedData;
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
undefined,
);
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>('category');
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 () => {
setHistoryEntries([]);
setHistoryError(null);
setSaveError(null);
setIsLoadingPlatform(true);
setPlatformError(null);
setIsLoadingDashboard(canReadProtectedData);
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(isAuthenticated ? 'home' : 'category');
}
} finally {
if (isActive) {
setIsLoadingPlatform(false);
setIsLoadingDashboard(false);
}
}
})();
return () => {
isActive = false;
};
}, [
canReadProtectedData,
getProfileDashboard,
hasInitialAgentSession,
isAuthenticated,
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;