import { useCallback, useEffect, useRef, useState } from 'react'; import type { CustomWorldAgentOperationRecord, CustomWorldAgentSessionSnapshot, } from '../../../packages/shared/src/contracts/customWorldAgent'; import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView'; import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary'; import { executeRpgCreationAction, getRpgCreationOperation, upsertRpgWorldProfile, } from '../../services/rpg-creation'; import type { CustomWorldProfile } from '../../types'; import { resolveRpgCreationErrorMessage, stringifyAgentBackedProfile, } from './rpgEntryShared'; import type { CustomWorldAutoSaveState, SelectionStage, SyncedAgentDraftResult, } from './rpgEntryTypes'; type UseRpgCreationResultAutosaveParams = { selectionStage: SelectionStage; activeAgentSessionId: string | null; generatedCustomWorldProfile: CustomWorldProfile | null; isAgentDraftResultView: boolean; userId: string | null | undefined; setGeneratedCustomWorldProfile: (profile: CustomWorldProfile | null) => void; setAgentOperation: ( operation: CustomWorldAgentOperationRecord | null, ) => void; setSavedCustomWorldEntries: ( entries: CustomWorldLibraryEntry[], ) => void; setSelectedDetailEntry: ( updater: | CustomWorldLibraryEntry | null | (( current: CustomWorldLibraryEntry | null, ) => CustomWorldLibraryEntry | null), ) => void; refreshCustomWorldWorks: () => Promise; persistAgentUiState: ( sessionId: string | null, operationId: string | null, generationSource?: 'agent-draft-foundation' | null, ) => void; syncAgentSessionSnapshot: ( sessionId: string, ) => Promise; syncAgentCreationResultView: ( sessionId: string, ) => Promise; buildDraftResultProfile: ( view: RpgCreationResultView | null, ) => CustomWorldProfile | null; }; /** * 协调结果页自动保存与 Agent 草稿同步。 * 这里统一维护去重签名、延时保存和“先同步 session 再落作品库”的顺序。 */ export function useRpgCreationResultAutosave( params: UseRpgCreationResultAutosaveParams, ) { const { selectionStage, activeAgentSessionId, generatedCustomWorldProfile, isAgentDraftResultView, userId, setGeneratedCustomWorldProfile, setAgentOperation, setSavedCustomWorldEntries, setSelectedDetailEntry, refreshCustomWorldWorks, persistAgentUiState, syncAgentSessionSnapshot, syncAgentCreationResultView, buildDraftResultProfile, } = params; const customWorldAutoSaveTimeoutRef = useRef(null); const lastAutoSavedProfileSignatureRef = useRef(null); const latestAutoSaveRequestIdRef = useRef(0); const latestAgentResultSyncSignatureRef = useRef(null); const isCustomWorldAutoSaveBusyRef = useRef(false); const [customWorldAutoSaveState, setCustomWorldAutoSaveState] = useState('idle'); const [customWorldAutoSaveError, setCustomWorldAutoSaveError] = useState< string | null >(null); const resetAutoSaveTrackingToIdle = useCallback(() => { if (customWorldAutoSaveTimeoutRef.current !== null) { window.clearTimeout(customWorldAutoSaveTimeoutRef.current); customWorldAutoSaveTimeoutRef.current = null; } lastAutoSavedProfileSignatureRef.current = null; latestAgentResultSyncSignatureRef.current = null; setCustomWorldAutoSaveState('idle'); setCustomWorldAutoSaveError(null); }, []); const markAutoSavedProfile = useCallback((profile: CustomWorldProfile) => { lastAutoSavedProfileSignatureRef.current = stringifyAgentBackedProfile(profile); }, []); const saveGeneratedCustomWorld = useCallback( async (profile = generatedCustomWorldProfile) => { if (!profile) { return null; } const requestId = latestAutoSaveRequestIdRef.current + 1; latestAutoSaveRequestIdRef.current = requestId; setCustomWorldAutoSaveState('saving'); setCustomWorldAutoSaveError(null); try { const mutation = await upsertRpgWorldProfile(profile, { sourceAgentSessionId: isAgentDraftResultView && activeAgentSessionId ? activeAgentSessionId : null, }); if (latestAutoSaveRequestIdRef.current !== requestId) { return mutation; } const canonicalProfile = normalizeCustomWorldProfileRecord(mutation.entry.profile) ?? mutation.entry.profile; // Agent 结果页的界面真相来自 result-view;作品库响应只用于列表与签名回写, // 避免旧兼容响应缺字段时覆盖当前完整编辑态。 lastAutoSavedProfileSignatureRef.current = stringifyAgentBackedProfile( isAgentDraftResultView ? profile : canonicalProfile, ); if (!isAgentDraftResultView) { setGeneratedCustomWorldProfile(canonicalProfile); } setSavedCustomWorldEntries(mutation.entries); if (userId) { void refreshCustomWorldWorks().catch(() => {}); } setSelectedDetailEntry((current) => { if (!current || current.profileId === mutation.entry.profileId) { return mutation.entry; } return current; }); setCustomWorldAutoSaveState('saved'); setCustomWorldAutoSaveError(null); return mutation; } catch (error) { if (latestAutoSaveRequestIdRef.current !== requestId) { return null; } setCustomWorldAutoSaveState('error'); setCustomWorldAutoSaveError( resolveRpgCreationErrorMessage(error, '保存自定义世界失败。'), ); return null; } }, [ activeAgentSessionId, generatedCustomWorldProfile, isAgentDraftResultView, refreshCustomWorldWorks, setSavedCustomWorldEntries, setSelectedDetailEntry, setGeneratedCustomWorldProfile, userId, ], ); const executeAgentActionAndWait = useCallback( async (action: Parameters[1]) => { if (!activeAgentSessionId) { throw new Error('当前世界草稿会话已失效,请返回创作页重新打开草稿。'); } const { operation } = await executeRpgCreationAction( activeAgentSessionId, action, ); setAgentOperation(operation); persistAgentUiState(activeAgentSessionId, operation.operationId); for (let attempt = 0; attempt < 60; attempt += 1) { const latestOperation = await getRpgCreationOperation( activeAgentSessionId, operation.operationId, ); setAgentOperation(latestOperation); if (latestOperation.status === 'failed') { throw new Error( latestOperation.error || latestOperation.phaseDetail || '执行共创操作失败。', ); } if (latestOperation.status === 'completed') { persistAgentUiState(activeAgentSessionId, null); return syncAgentSessionSnapshot(activeAgentSessionId); } await new Promise((resolve) => window.setTimeout(resolve, 200)); } throw new Error('执行共创操作超时。'); }, [ activeAgentSessionId, persistAgentUiState, setAgentOperation, syncAgentSessionSnapshot, ], ); const syncAgentDraftResultProfile = useCallback( async (profile: CustomWorldProfile) => { if (!activeAgentSessionId) { return { session: null, profile: null, } satisfies SyncedAgentDraftResult; } const profileSignature = stringifyAgentBackedProfile(profile); const currentView = await syncAgentCreationResultView(activeAgentSessionId); if (!currentView?.canSyncResultProfile) { const latestProfile = buildDraftResultProfile(currentView) ?? profile; if (latestProfile) { setGeneratedCustomWorldProfile(latestProfile); } latestAgentResultSyncSignatureRef.current = stringifyAgentBackedProfile(latestProfile); return { session: currentView?.session ?? null, profile: latestProfile, view: currentView, } satisfies SyncedAgentDraftResult; } if (latestAgentResultSyncSignatureRef.current !== profileSignature) { await executeAgentActionAndWait({ action: 'sync_result_profile', profile: profile as unknown as Record, }); latestAgentResultSyncSignatureRef.current = profileSignature; } const latestView = await syncAgentCreationResultView(activeAgentSessionId); const latestProfile = buildDraftResultProfile(latestView) ?? profile; if (latestProfile) { setGeneratedCustomWorldProfile(latestProfile); } latestAgentResultSyncSignatureRef.current = stringifyAgentBackedProfile(latestProfile); return { session: latestView?.session ?? null, profile: latestProfile, view: latestView, } satisfies SyncedAgentDraftResult; }, [ activeAgentSessionId, buildDraftResultProfile, executeAgentActionAndWait, setGeneratedCustomWorldProfile, syncAgentCreationResultView, ], ); useEffect( () => () => { if (customWorldAutoSaveTimeoutRef.current !== null) { window.clearTimeout(customWorldAutoSaveTimeoutRef.current); } }, [], ); useEffect(() => { if (!generatedCustomWorldProfile) { resetAutoSaveTrackingToIdle(); return; } if (selectionStage !== 'custom-world-result') { return; } if (isCustomWorldAutoSaveBusyRef.current) { return; } const nextSignature = stringifyAgentBackedProfile( generatedCustomWorldProfile, ); if (nextSignature === lastAutoSavedProfileSignatureRef.current) { return; } setCustomWorldAutoSaveState('saving'); if (customWorldAutoSaveTimeoutRef.current !== null) { window.clearTimeout(customWorldAutoSaveTimeoutRef.current); } const profileToSave = generatedCustomWorldProfile; customWorldAutoSaveTimeoutRef.current = window.setTimeout(() => { void (async () => { isCustomWorldAutoSaveBusyRef.current = true; try { let latestProfileToSave = profileToSave; if (isAgentDraftResultView) { const syncedResult = await syncAgentDraftResultProfile(profileToSave); if (syncedResult.view && !syncedResult.view.canAutosaveLibrary) { setCustomWorldAutoSaveState('idle'); return; } // 作品库自动保存优先落同步后 session 重编译出的结果,避免继续保存旧的前端内存态。 latestProfileToSave = syncedResult.profile ?? profileToSave; } await saveGeneratedCustomWorld(latestProfileToSave); } catch (error) { setCustomWorldAutoSaveState('error'); setCustomWorldAutoSaveError( resolveRpgCreationErrorMessage(error, '保存自定义世界失败。'), ); } finally { isCustomWorldAutoSaveBusyRef.current = false; } })(); customWorldAutoSaveTimeoutRef.current = null; }, 600); return () => { if (customWorldAutoSaveTimeoutRef.current !== null) { window.clearTimeout(customWorldAutoSaveTimeoutRef.current); customWorldAutoSaveTimeoutRef.current = null; } }; }, [ generatedCustomWorldProfile, isAgentDraftResultView, resetAutoSaveTrackingToIdle, saveGeneratedCustomWorld, selectionStage, syncAgentDraftResultProfile, ]); return { customWorldAutoSaveState, setCustomWorldAutoSaveState, customWorldAutoSaveError, setCustomWorldAutoSaveError, resetAutoSaveTrackingToIdle, markAutoSavedProfile, saveGeneratedCustomWorld, syncAgentDraftResultProfile, executeAgentActionAndWait, }; }