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 | null; setSelectedDetailEntry: ( entry: | CustomWorldLibraryEntry | null | (( current: CustomWorldLibraryEntry | null, ) => CustomWorldLibraryEntry | null), ) => void; savedCustomWorldEntries: CustomWorldLibraryEntry[]; setSavedCustomWorldEntries: ( entries: CustomWorldLibraryEntry[], ) => 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; refreshCustomWorldWorks: () => Promise; refreshPublishedGallery: () => Promise; persistAgentUiState: ( sessionId: string | null, operationId: string | null, ) => void; syncAgentSessionSnapshot: ( sessionId: string, ) => Promise; 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(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) => { 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) => { 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;