Files
Genarrative/src/components/rpg-entry/useRpgEntryLibraryDetail.ts
2026-05-08 11:44:42 +08:00

553 lines
18 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, useState } from 'react';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
PlatformBrowseHistoryWriteEntry,
} from '../../../packages/shared/src/contracts/runtime';
import {
buildPublicWorkDetailPath,
pushAppHistoryPath,
} from '../../routing/appPageRoutes';
import { ApiClientError } from '../../services/apiClient';
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldLibraryDetail,
getRpgEntryWorldGalleryDetail,
listRpgEntryWorldLibrary,
publishRpgEntryWorldProfile,
unpublishRpgEntryWorldProfile,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import type { CustomWorldProfile } from '../../types';
import { 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;
setPlatformTabToDraft: () => void;
setPlatformError: (error: string | null) => void;
appendBrowseHistoryEntry: (
entry: PlatformBrowseHistoryWriteEntry,
) => Promise<void>;
refreshCustomWorldWorks: () => Promise<unknown>;
refreshPublishedGallery: () => Promise<unknown>;
persistAgentUiState: (
sessionId: string | null,
operationId: string | null,
generationSource?: 'agent-draft-foundation' | null,
) => void;
syncAgentCreationResultView: (
sessionId: string,
) => Promise<RpgCreationResultView | null>;
buildDraftResultProfile: (
view: RpgCreationResultView | 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,
setPlatformTabToDraft,
setPlatformError,
appendBrowseHistoryEntry,
refreshCustomWorldWorks,
refreshPublishedGallery,
persistAgentUiState,
syncAgentCreationResultView,
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(
async (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');
if (entry.publicWorkCode?.trim()) {
pushAppHistoryPath(buildPublicWorkDetailPath(entry.publicWorkCode));
}
if (!userId || entry.ownerUserId !== userId) {
return;
}
setIsDetailLoading(true);
try {
const detailEntry = await getRpgEntryWorldLibraryDetail(entry.profileId);
setSelectedDetailEntry(detailEntry);
} catch (error) {
setDetailError(
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
);
} finally {
setIsDetailLoading(false);
}
},
[
appendBrowseHistoryEntry,
setSelectedDetailEntry,
setSelectionStage,
userId,
],
);
const loadGalleryDetailEntry = useCallback(
async (entry: CustomWorldGalleryCard) => {
const detailEntry = await getRpgEntryWorldGalleryDetail(
entry.ownerUserId,
entry.profileId,
);
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,
});
return detailEntry;
},
[appendBrowseHistoryEntry],
);
const openGalleryDetail = useCallback(
async (entry: CustomWorldGalleryCard) => {
setSelectionStage('detail');
setIsDetailLoading(true);
setDetailError(null);
try {
const detailEntry = await loadGalleryDetailEntry(entry);
setSelectedDetailEntry(detailEntry);
if (detailEntry.publicWorkCode?.trim()) {
pushAppHistoryPath(
buildPublicWorkDetailPath(detailEntry.publicWorkCode),
);
}
} catch (error) {
setSelectedDetailEntry(null);
setDetailError(
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
);
} finally {
setIsDetailLoading(false);
}
},
[
loadGalleryDetailEntry,
setSelectedDetailEntry,
setSelectionStage,
],
);
const openSavedCustomWorldEditor = useCallback(
async (entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
try {
const detailEntry =
userId && entry.ownerUserId === userId
? await getRpgEntryWorldLibraryDetail(entry.profileId)
: entry;
setSelectedDetailEntry(detailEntry);
resetAutoSaveTrackingToIdle();
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setCustomWorldError(null);
setGeneratedCustomWorldProfile(null);
setGeneratedCustomWorldProfile(detailEntry.profile);
markAutoSavedProfile(detailEntry.profile);
setCustomWorldAutoSaveState('saved');
setCustomWorldAutoSaveError(null);
setCustomWorldError(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('saved-profile');
setSelectionStage('custom-world-result');
} catch (error) {
setDetailError(
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
);
}
},
[
markAutoSavedProfile,
setCustomWorldAutoSaveError,
setCustomWorldAutoSaveState,
setCustomWorldError,
setCustomWorldGenerationViewSource,
setCustomWorldResultViewSource,
setGeneratedCustomWorldProfile,
setSelectedDetailEntry,
setSelectionStage,
userId,
],
);
const handleOpenCreationWork = useCallback(
async (work: CustomWorldWorkSummary) => {
if (work.status === 'draft' && work.sessionId) {
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setGeneratedCustomWorldProfile(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
resetAutoSaveTrackingToIdle();
try {
const resultView = await syncAgentCreationResultView(work.sessionId);
const nextProfile = buildDraftResultProfile(resultView);
if (resultView?.targetStage === 'custom-world-generating') {
// 生成过程中失败的草稿要回到生成过程页承接错误处理,避免误回 Agent 对话。
suppressAgentDraftResultAutoOpen();
persistAgentUiState(
work.sessionId,
null,
resultView.generationViewSource ?? 'agent-draft-foundation',
);
setGeneratedCustomWorldProfile(null);
setCustomWorldGenerationViewSource(
resultView.generationViewSource ?? 'agent-draft-foundation',
);
setCustomWorldResultViewSource(null);
setPlatformTabToCreate();
setSelectionStage('custom-world-generating');
return;
}
if (resultView?.targetStage === 'agent-workspace') {
// 还没有服务端草稿真相源时只能恢复 Agent对象数量等摘要字段不能决定结果页入口。
suppressAgentDraftResultAutoOpen();
persistAgentUiState(work.sessionId, null);
setGeneratedCustomWorldProfile(null);
setCustomWorldResultViewSource(null);
setPlatformTabToCreate();
setSelectionStage('agent-workspace');
return;
}
releaseAgentDraftResultAutoOpenSuppression();
if (!nextProfile) {
persistAgentUiState(work.sessionId, null, 'agent-draft-foundation');
setPlatformError(
'当前草稿还没有可编辑的结果页数据,请先继续补齐锚点。',
);
setPlatformTabToCreate();
setCustomWorldGenerationViewSource('agent-draft-foundation');
setSelectionStage('custom-world-generating');
return;
}
persistAgentUiState(work.sessionId, null);
setGeneratedCustomWorldProfile(nextProfile);
setCustomWorldResultViewSource(
resultView?.resultViewSource ?? 'agent-draft',
);
setPlatformTabToCreate();
setSelectionStage(resultView?.targetStage ?? '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, '读取创作草稿失败。'),
);
}
setPlatformTabToDraft();
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) {
void openLibraryDetail(matchedEntry);
return;
}
setPlatformError('未找到对应作品,请刷新后重试。');
} catch (error) {
setPlatformError(
resolveRpgEntryErrorMessage(error, '读取作品详情失败。'),
);
}
},
[
buildDraftResultProfile,
openLibraryDetail,
persistAgentUiState,
releaseAgentDraftResultAutoOpenSuppression,
resetAutoSaveTrackingToIdle,
savedCustomWorldEntries,
setCustomWorldAutoSaveError,
setCustomWorldAutoSaveState,
setCustomWorldError,
setCustomWorldGenerationViewSource,
setCustomWorldResultViewSource,
setGeneratedCustomWorldProfile,
setPlatformError,
setPlatformTabToCreate,
setPlatformTabToDraft,
setSavedCustomWorldEntries,
setSelectionStage,
suppressAgentDraftResultAutoOpen,
syncAgentCreationResultView,
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,
loadGalleryDetailEntry,
openSavedCustomWorldEditor,
handleOpenCreationWork,
handlePublishSelectedWorld,
handleUnpublishSelectedWorld,
handleDeleteSelectedWorld,
};
}
/**
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
*/
export const useRpgCreationDetailNavigation = useRpgEntryLibraryDetail;