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
426 lines
13 KiB
TypeScript
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;
|