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 { countCustomWorldProfileAssetSlots, countCustomWorldProfileDetailSlots, countCustomWorldProfileStructuredSlots, } from './rpgProfileCompleteness'; import { 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; setPlatformTabToDraft: () => void; setPlatformError: (error: string | null) => void; appendBrowseHistoryEntry: ( entry: PlatformBrowseHistoryWriteEntry, ) => Promise; refreshCustomWorldWorks: () => Promise; refreshPublishedGallery: () => Promise; persistAgentUiState: ( sessionId: string | null, operationId: string | null, generationSource?: 'agent-draft-foundation' | null, ) => void; syncAgentCreationResultView: ( sessionId: string, ) => Promise; 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' ); } function shouldKeepSelectedDetailProfile( selectedEntry: CustomWorldLibraryEntry, nextOwnedEntry: CustomWorldLibraryEntry, ) { if ( selectedEntry.ownerUserId !== nextOwnedEntry.ownerUserId || selectedEntry.profileId !== nextOwnedEntry.profileId ) { return false; } const selectedDetailCount = countCustomWorldProfileDetailSlots( selectedEntry.profile, ); const nextDetailCount = countCustomWorldProfileDetailSlots( nextOwnedEntry.profile, ); const selectedAssetSlotCount = countCustomWorldProfileAssetSlots( selectedEntry.profile, ); const nextAssetSlotCount = countCustomWorldProfileAssetSlots( nextOwnedEntry.profile, ); const selectedStructuredSlotCount = countCustomWorldProfileStructuredSlots(selectedEntry.profile); const nextStructuredSlotCount = countCustomWorldProfileStructuredSlots( nextOwnedEntry.profile, ); const expectedRuntimeCount = nextOwnedEntry.playableNpcCount + nextOwnedEntry.landmarkCount; // 作品架列表只保证卡片摘要,不能在详情接口已经拿到完整运行态字段后覆盖详情。 return ( (selectedDetailCount > nextDetailCount && expectedRuntimeCount > nextDetailCount) || selectedAssetSlotCount > nextAssetSlotCount || selectedStructuredSlotCount > nextStructuredSlotCount ); } /** * 负责平台详情、创作作品入口和结果页打开路径。 * 平台壳层只消费“打开哪个面板”的结果,不再自己拼接恢复流程细节。 */ 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(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) { if (shouldKeepSelectedDetailProfile(selectedDetailEntry, nextOwnedEntry)) { return; } setSelectedDetailEntry(nextOwnedEntry); } }, [savedCustomWorldEntries, selectedDetailEntry, setSelectedDetailEntry]); const openLibraryDetail = useCallback( async (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'); 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) => { 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;