This commit is contained in:
519
src/components/rpg-entry/useRpgEntryLibraryDetail.ts
Normal file
519
src/components/rpg-entry/useRpgEntryLibraryDetail.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
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/rpgEntryLibraryClient';
|
||||
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,
|
||||
generationSource?: 'agent-draft-foundation' | null,
|
||||
) => void;
|
||||
syncAgentSessionSnapshot: (
|
||||
sessionId: string,
|
||||
) => Promise<CustomWorldAgentSessionSnapshot | null>;
|
||||
buildDraftResultProfile: (
|
||||
session: CustomWorldAgentSessionSnapshot | null,
|
||||
) => CustomWorldProfile | null;
|
||||
suppressAgentDraftResultAutoOpen: () => void;
|
||||
releaseAgentDraftResultAutoOpenSuppression: () => void;
|
||||
resetAutoSaveTrackingToIdle: () => void;
|
||||
markAutoSavedProfile: (profile: CustomWorldProfile) => void;
|
||||
};
|
||||
|
||||
const AGENT_RESULT_STAGES = new Set([
|
||||
'object_refining',
|
||||
'visual_refining',
|
||||
'long_tail_review',
|
||||
'ready_to_publish',
|
||||
'published',
|
||||
]);
|
||||
|
||||
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);
|
||||
resetAutoSaveTrackingToIdle();
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setCustomWorldError(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
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');
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
resetAutoSaveTrackingToIdle();
|
||||
|
||||
try {
|
||||
const latestSession = await syncAgentSessionSnapshot(work.sessionId);
|
||||
const nextProfile = buildDraftResultProfile(latestSession);
|
||||
const shouldOpenAgentWorkspace =
|
||||
!latestSession?.draftProfile ||
|
||||
!latestSession.stage ||
|
||||
!AGENT_RESULT_STAGES.has(latestSession.stage);
|
||||
|
||||
const shouldResumeFailedGenerationView =
|
||||
!nextProfile &&
|
||||
/失败/u.test(`${work.stageLabel ?? ''}${work.summary ?? ''}`);
|
||||
|
||||
if (shouldResumeFailedGenerationView) {
|
||||
// 生成过程中失败的草稿要回到生成过程页承接错误处理,避免误回 Agent 对话。
|
||||
suppressAgentDraftResultAutoOpen();
|
||||
persistAgentUiState(
|
||||
work.sessionId,
|
||||
null,
|
||||
'agent-draft-foundation',
|
||||
);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
||||
setCustomWorldResultViewSource(null);
|
||||
setPlatformTabToCreate();
|
||||
setSelectionStage('custom-world-generating');
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldOpenAgentWorkspace) {
|
||||
// 还没有服务端草稿真相源时只能恢复 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(
|
||||
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;
|
||||
Reference in New Issue
Block a user