477 lines
15 KiB
TypeScript
477 lines
15 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import type {
|
|
CustomWorldAgentSessionSnapshot,
|
|
CustomWorldWorkSummary,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import type {
|
|
CustomWorldGalleryCard,
|
|
CustomWorldLibraryEntry,
|
|
PlatformBrowseHistoryWriteEntry,
|
|
} from '../../../packages/shared/src/contracts/runtime';
|
|
import {
|
|
deleteRpgEntryWorldProfile,
|
|
getRpgEntryWorldGalleryDetail,
|
|
listRpgEntryWorldLibrary,
|
|
publishRpgEntryWorldProfile,
|
|
unpublishRpgEntryWorldProfile,
|
|
} from '../../services/rpg-entry';
|
|
import { ApiClientError } from '../../services/apiClient';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
import {
|
|
normalizeRpgEntryAgentBackedProfile,
|
|
resolveRpgEntryErrorMessage,
|
|
} from './rpgEntryShared';
|
|
import type {
|
|
CustomWorldAutoSaveState,
|
|
CustomWorldGenerationViewSource,
|
|
CustomWorldResultViewSource,
|
|
SelectionStage,
|
|
} from './rpgEntryTypes';
|
|
|
|
type UseRpgEntryLibraryDetailParams = {
|
|
userId: string | null | undefined;
|
|
selectedDetailEntry: CustomWorldLibraryEntry<CustomWorldProfile> | null;
|
|
setSelectedDetailEntry: (
|
|
entry:
|
|
| CustomWorldLibraryEntry<CustomWorldProfile>
|
|
| null
|
|
| ((
|
|
current: CustomWorldLibraryEntry<CustomWorldProfile> | null,
|
|
) => CustomWorldLibraryEntry<CustomWorldProfile> | null),
|
|
) => void;
|
|
savedCustomWorldEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
|
|
setSavedCustomWorldEntries: (
|
|
entries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
|
) => void;
|
|
setGeneratedCustomWorldProfile: (
|
|
profile: CustomWorldProfile | null,
|
|
) => void;
|
|
setCustomWorldError: (error: string | null) => void;
|
|
setCustomWorldAutoSaveError: (error: string | null) => void;
|
|
setCustomWorldAutoSaveState: (state: CustomWorldAutoSaveState) => void;
|
|
setCustomWorldGenerationViewSource: (
|
|
source: CustomWorldGenerationViewSource,
|
|
) => void;
|
|
setCustomWorldResultViewSource: (
|
|
source: CustomWorldResultViewSource,
|
|
) => void;
|
|
setSelectionStage: (stage: SelectionStage) => void;
|
|
setPlatformTabToCreate: () => void;
|
|
setPlatformError: (error: string | null) => void;
|
|
appendBrowseHistoryEntry: (
|
|
entry: PlatformBrowseHistoryWriteEntry,
|
|
) => Promise<void>;
|
|
refreshCustomWorldWorks: () => Promise<unknown>;
|
|
refreshPublishedGallery: () => Promise<unknown>;
|
|
persistAgentUiState: (
|
|
sessionId: string | null,
|
|
operationId: string | null,
|
|
) => void;
|
|
syncAgentSessionSnapshot: (
|
|
sessionId: string,
|
|
) => Promise<CustomWorldAgentSessionSnapshot | null>;
|
|
buildDraftResultProfile: (
|
|
session: CustomWorldAgentSessionSnapshot | null,
|
|
) => CustomWorldProfile | null;
|
|
suppressAgentDraftResultAutoOpen: () => void;
|
|
releaseAgentDraftResultAutoOpenSuppression: () => void;
|
|
resetAutoSaveTrackingToIdle: () => void;
|
|
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
|
|
};
|
|
|
|
function isMissingRpgEntryAgentSessionError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
error.status === 404 &&
|
|
error.code === 'NOT_FOUND'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 负责平台详情、创作作品入口和结果页打开路径。
|
|
* 平台壳层只消费“打开哪个面板”的结果,不再自己拼接恢复流程细节。
|
|
*/
|
|
export function useRpgEntryLibraryDetail(
|
|
params: UseRpgEntryLibraryDetailParams,
|
|
) {
|
|
const {
|
|
userId,
|
|
selectedDetailEntry,
|
|
setSelectedDetailEntry,
|
|
savedCustomWorldEntries,
|
|
setSavedCustomWorldEntries,
|
|
setGeneratedCustomWorldProfile,
|
|
setCustomWorldError,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setSelectionStage,
|
|
setPlatformTabToCreate,
|
|
setPlatformError,
|
|
appendBrowseHistoryEntry,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
persistAgentUiState,
|
|
syncAgentSessionSnapshot,
|
|
buildDraftResultProfile,
|
|
suppressAgentDraftResultAutoOpen,
|
|
releaseAgentDraftResultAutoOpenSuppression,
|
|
resetAutoSaveTrackingToIdle,
|
|
markAutoSavedProfile,
|
|
} = params;
|
|
|
|
const [detailError, setDetailError] = useState<string | null>(null);
|
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
|
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!selectedDetailEntry) {
|
|
return;
|
|
}
|
|
|
|
const nextOwnedEntry = savedCustomWorldEntries.find(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
);
|
|
if (nextOwnedEntry && nextOwnedEntry !== selectedDetailEntry) {
|
|
setSelectedDetailEntry(nextOwnedEntry);
|
|
}
|
|
}, [savedCustomWorldEntries, selectedDetailEntry, setSelectedDetailEntry]);
|
|
|
|
const openLibraryDetail = useCallback(
|
|
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
|
if (entry.visibility === 'published') {
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: entry.ownerUserId,
|
|
profileId: entry.profileId,
|
|
worldName: entry.worldName,
|
|
subtitle: entry.subtitle,
|
|
summaryText: entry.summaryText,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
themeMode: entry.themeMode,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
});
|
|
}
|
|
setSelectedDetailEntry(entry);
|
|
setDetailError(null);
|
|
setSelectionStage('detail');
|
|
},
|
|
[appendBrowseHistoryEntry, setSelectedDetailEntry, setSelectionStage],
|
|
);
|
|
|
|
const openGalleryDetail = useCallback(
|
|
async (entry: CustomWorldGalleryCard) => {
|
|
setSelectionStage('detail');
|
|
setIsDetailLoading(true);
|
|
setDetailError(null);
|
|
|
|
try {
|
|
const detailEntry = await getRpgEntryWorldGalleryDetail(
|
|
entry.ownerUserId,
|
|
entry.profileId,
|
|
);
|
|
setSelectedDetailEntry(detailEntry);
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: detailEntry.ownerUserId,
|
|
profileId: detailEntry.profileId,
|
|
worldName: detailEntry.worldName,
|
|
subtitle: detailEntry.subtitle,
|
|
summaryText: detailEntry.summaryText,
|
|
coverImageSrc: detailEntry.coverImageSrc,
|
|
themeMode: detailEntry.themeMode,
|
|
authorDisplayName: detailEntry.authorDisplayName,
|
|
});
|
|
} catch (error) {
|
|
setSelectedDetailEntry(null);
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
} finally {
|
|
setIsDetailLoading(false);
|
|
}
|
|
},
|
|
[appendBrowseHistoryEntry, setSelectedDetailEntry, setSelectionStage],
|
|
);
|
|
|
|
const openSavedCustomWorldEditor = useCallback(
|
|
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
|
setSelectedDetailEntry(entry);
|
|
const normalizedProfile = normalizeRpgEntryAgentBackedProfile(
|
|
entry.profile,
|
|
);
|
|
setGeneratedCustomWorldProfile(normalizedProfile);
|
|
markAutoSavedProfile(normalizedProfile);
|
|
setCustomWorldAutoSaveState('saved');
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource('saved-profile');
|
|
setSelectionStage('custom-world-result');
|
|
},
|
|
[
|
|
markAutoSavedProfile,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldError,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setGeneratedCustomWorldProfile,
|
|
setSelectedDetailEntry,
|
|
setSelectionStage,
|
|
],
|
|
);
|
|
|
|
const handleOpenCreationWork = useCallback(
|
|
async (work: CustomWorldWorkSummary) => {
|
|
if (work.status === 'draft' && work.sessionId) {
|
|
setCustomWorldError(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setCustomWorldGenerationViewSource(null);
|
|
resetAutoSaveTrackingToIdle();
|
|
|
|
const shouldOpenAgentWorkspace =
|
|
work.playableNpcCount <= 0 && work.landmarkCount <= 0;
|
|
|
|
try {
|
|
if (shouldOpenAgentWorkspace) {
|
|
// 仅八锚点未整理成底稿时才恢复 Agent 对话工作区。
|
|
suppressAgentDraftResultAutoOpen();
|
|
persistAgentUiState(work.sessionId, null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldResultViewSource(null);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('agent-workspace');
|
|
return;
|
|
}
|
|
|
|
releaseAgentDraftResultAutoOpenSuppression();
|
|
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
|
|
const nextProfile = buildDraftResultProfile(latestSession);
|
|
if (!nextProfile) {
|
|
persistAgentUiState(work.sessionId, null);
|
|
setPlatformError('当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。');
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('agent-workspace');
|
|
return;
|
|
}
|
|
|
|
persistAgentUiState(work.sessionId, null);
|
|
setGeneratedCustomWorldProfile(
|
|
normalizeRpgEntryAgentBackedProfile(nextProfile),
|
|
);
|
|
setCustomWorldResultViewSource('agent-draft');
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('custom-world-result');
|
|
return;
|
|
} catch (error) {
|
|
if (isMissingRpgEntryAgentSessionError(error)) {
|
|
// 失效会话不能继续保留在恢复状态里,否则刷新后会重复命中同一个坏 session。
|
|
persistAgentUiState(null, null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldResultViewSource(null);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setPlatformError(
|
|
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
|
|
);
|
|
} else {
|
|
setPlatformError(
|
|
resolveRpgEntryErrorMessage(error, '读取创作草稿失败。'),
|
|
);
|
|
}
|
|
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('platform');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!work.profileId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let matchedEntry = savedCustomWorldEntries.find(
|
|
(entry) => entry.profileId === work.profileId,
|
|
);
|
|
|
|
if (!matchedEntry && userId) {
|
|
const latestLibraryEntries = await listRpgEntryWorldLibrary();
|
|
setSavedCustomWorldEntries(latestLibraryEntries);
|
|
matchedEntry = latestLibraryEntries.find(
|
|
(entry) => entry.profileId === work.profileId,
|
|
);
|
|
}
|
|
|
|
if (matchedEntry) {
|
|
openLibraryDetail(matchedEntry);
|
|
return;
|
|
}
|
|
|
|
setPlatformError('未找到对应作品,请刷新后重试。');
|
|
} catch (error) {
|
|
setPlatformError(
|
|
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
|
|
);
|
|
}
|
|
},
|
|
[
|
|
buildDraftResultProfile,
|
|
openLibraryDetail,
|
|
persistAgentUiState,
|
|
releaseAgentDraftResultAutoOpenSuppression,
|
|
resetAutoSaveTrackingToIdle,
|
|
savedCustomWorldEntries,
|
|
setCustomWorldAutoSaveError,
|
|
setCustomWorldAutoSaveState,
|
|
setCustomWorldError,
|
|
setCustomWorldGenerationViewSource,
|
|
setCustomWorldResultViewSource,
|
|
setGeneratedCustomWorldProfile,
|
|
setPlatformError,
|
|
setPlatformTabToCreate,
|
|
setSavedCustomWorldEntries,
|
|
setSelectionStage,
|
|
suppressAgentDraftResultAutoOpen,
|
|
syncAgentSessionSnapshot,
|
|
userId,
|
|
],
|
|
);
|
|
|
|
const handlePublishSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await publishRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '发布自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
]);
|
|
|
|
const handleUnpublishSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await unpublishRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '下架自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
]);
|
|
|
|
const handleDeleteSelectedWorld = useCallback(async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
const confirmed = window.confirm(
|
|
`确认删除作品《${selectedDetailEntry.worldName}》吗?删除后会从你的作品列表和公开广场中移除。`,
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const entries = await deleteRpgEntryWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(entries);
|
|
await refreshCustomWorldWorks().catch(() => []);
|
|
setSelectedDetailEntry(null);
|
|
setPlatformTabToCreate();
|
|
setSelectionStage('platform');
|
|
await refreshPublishedGallery().catch(() => []);
|
|
} catch (error) {
|
|
setDetailError(
|
|
resolveRpgEntryErrorMessage(error, '删除自定义世界失败。'),
|
|
);
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
}, [
|
|
isMutatingDetail,
|
|
refreshCustomWorldWorks,
|
|
refreshPublishedGallery,
|
|
selectedDetailEntry,
|
|
setPlatformTabToCreate,
|
|
setSavedCustomWorldEntries,
|
|
setSelectedDetailEntry,
|
|
setSelectionStage,
|
|
]);
|
|
|
|
const isSelectedWorldOwned = Boolean(
|
|
selectedDetailEntry &&
|
|
savedCustomWorldEntries.some(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
),
|
|
);
|
|
|
|
return {
|
|
detailError,
|
|
setDetailError,
|
|
isDetailLoading,
|
|
isMutatingDetail,
|
|
isSelectedWorldOwned,
|
|
openLibraryDetail,
|
|
openGalleryDetail,
|
|
openSavedCustomWorldEditor,
|
|
handleOpenCreationWork,
|
|
handlePublishSelectedWorld,
|
|
handleUnpublishSelectedWorld,
|
|
handleDeleteSelectedWorld,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
|
|
*/
|
|
export const useRpgCreationDetailNavigation = useRpgEntryLibraryDetail;
|