This commit is contained in:
379
src/components/rpg-entry/useRpgCreationResultAutosave.ts
Normal file
379
src/components/rpg-entry/useRpgCreationResultAutosave.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
executeRpgCreationAction,
|
||||
getRpgCreationOperation,
|
||||
upsertRpgWorldProfile,
|
||||
} from '../../services/rpg-creation';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import {
|
||||
normalizeAgentBackedProfile,
|
||||
resolveRpgCreationErrorMessage,
|
||||
stringifyAgentBackedProfile,
|
||||
} from './rpgEntryShared';
|
||||
import type {
|
||||
CustomWorldAutoSaveState,
|
||||
SelectionStage,
|
||||
SyncedAgentDraftResult,
|
||||
} from './rpgEntryTypes';
|
||||
|
||||
type UseRpgCreationResultAutosaveParams = {
|
||||
selectionStage: SelectionStage;
|
||||
activeAgentSessionId: string | null;
|
||||
agentSession: CustomWorldAgentSessionSnapshot | null;
|
||||
generatedCustomWorldProfile: CustomWorldProfile | null;
|
||||
isAgentDraftResultView: boolean;
|
||||
userId: string | null | undefined;
|
||||
setGeneratedCustomWorldProfile: (profile: CustomWorldProfile | null) => void;
|
||||
setAgentOperation: (
|
||||
operation: CustomWorldAgentOperationRecord | null,
|
||||
) => void;
|
||||
setSavedCustomWorldEntries: (
|
||||
entries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
||||
) => void;
|
||||
setSelectedDetailEntry: (
|
||||
updater:
|
||||
| CustomWorldLibraryEntry<CustomWorldProfile>
|
||||
| null
|
||||
| ((
|
||||
current: CustomWorldLibraryEntry<CustomWorldProfile> | null,
|
||||
) => CustomWorldLibraryEntry<CustomWorldProfile> | null),
|
||||
) => void;
|
||||
refreshCustomWorldWorks: () => 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 协调结果页自动保存与 Agent 草稿同步。
|
||||
* 这里统一维护去重签名、延时保存和“先同步 session 再落作品库”的顺序。
|
||||
*/
|
||||
export function useRpgCreationResultAutosave(
|
||||
params: UseRpgCreationResultAutosaveParams,
|
||||
) {
|
||||
const {
|
||||
selectionStage,
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
generatedCustomWorldProfile,
|
||||
isAgentDraftResultView,
|
||||
userId,
|
||||
setGeneratedCustomWorldProfile,
|
||||
setAgentOperation,
|
||||
setSavedCustomWorldEntries,
|
||||
setSelectedDetailEntry,
|
||||
refreshCustomWorldWorks,
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
buildDraftResultProfile,
|
||||
} = params;
|
||||
|
||||
const customWorldAutoSaveTimeoutRef = useRef<number | null>(null);
|
||||
const lastAutoSavedProfileSignatureRef = useRef<string | null>(null);
|
||||
const latestAutoSaveRequestIdRef = useRef(0);
|
||||
const latestAgentResultSyncSignatureRef = useRef<string | null>(null);
|
||||
const isCustomWorldAutoSaveBusyRef = useRef(false);
|
||||
|
||||
const [customWorldAutoSaveState, setCustomWorldAutoSaveState] =
|
||||
useState<CustomWorldAutoSaveState>('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 normalizedProfile = normalizeAgentBackedProfile(profile);
|
||||
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
|
||||
const requestId = latestAutoSaveRequestIdRef.current + 1;
|
||||
latestAutoSaveRequestIdRef.current = requestId;
|
||||
setCustomWorldAutoSaveState('saving');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
|
||||
try {
|
||||
const mutation =
|
||||
await upsertRpgWorldProfile(
|
||||
normalizedProfile,
|
||||
{
|
||||
sourceAgentSessionId:
|
||||
isAgentDraftResultView && activeAgentSessionId
|
||||
? activeAgentSessionId
|
||||
: null,
|
||||
},
|
||||
);
|
||||
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
||||
return mutation;
|
||||
}
|
||||
|
||||
lastAutoSavedProfileSignatureRef.current = profileSignature;
|
||||
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,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
const syncAgentDraftResultProfile = useCallback(
|
||||
async (profile: CustomWorldProfile) => {
|
||||
if (!activeAgentSessionId) {
|
||||
return {
|
||||
session: null,
|
||||
profile: null,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
const normalizedProfile = normalizeAgentBackedProfile(profile);
|
||||
const profileSignature = stringifyAgentBackedProfile(normalizedProfile);
|
||||
const latestSessionProfile = buildDraftResultProfile(agentSession);
|
||||
const latestSessionProfileSignature = latestSessionProfile
|
||||
? stringifyAgentBackedProfile(latestSessionProfile)
|
||||
: '';
|
||||
const shouldRefreshPublishGate = Boolean(
|
||||
agentSession?.resultPreview && !agentSession.resultPreview.publishReady,
|
||||
);
|
||||
|
||||
if (
|
||||
latestSessionProfileSignature === profileSignature &&
|
||||
!shouldRefreshPublishGate
|
||||
) {
|
||||
latestAgentResultSyncSignatureRef.current = profileSignature;
|
||||
return {
|
||||
session: agentSession,
|
||||
profile: normalizeAgentBackedProfile(latestSessionProfile ?? profile),
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
if (
|
||||
latestAgentResultSyncSignatureRef.current === profileSignature &&
|
||||
!shouldRefreshPublishGate
|
||||
) {
|
||||
return {
|
||||
session: agentSession,
|
||||
profile: normalizeAgentBackedProfile(latestSessionProfile ?? profile),
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
}
|
||||
|
||||
// Agent 结果页不再把前端 profile 回写到 session。
|
||||
// session.draftProfile 是真相源;这里只刷新后端最新快照,避免在采集/生成早期误触 sync_result_profile。
|
||||
const latestSession = await syncAgentSessionSnapshot(activeAgentSessionId);
|
||||
const latestProfile = normalizeAgentBackedProfile(
|
||||
buildDraftResultProfile(latestSession) ?? profile,
|
||||
);
|
||||
if (latestProfile) {
|
||||
setGeneratedCustomWorldProfile(latestProfile);
|
||||
}
|
||||
latestAgentResultSyncSignatureRef.current =
|
||||
stringifyAgentBackedProfile(latestProfile);
|
||||
|
||||
return {
|
||||
session: latestSession,
|
||||
profile: latestProfile,
|
||||
} satisfies SyncedAgentDraftResult;
|
||||
},
|
||||
[
|
||||
activeAgentSessionId,
|
||||
agentSession,
|
||||
buildDraftResultProfile,
|
||||
setGeneratedCustomWorldProfile,
|
||||
syncAgentSessionSnapshot,
|
||||
],
|
||||
);
|
||||
|
||||
const executeAgentActionAndWait = useCallback(
|
||||
async (action: Parameters<typeof executeRpgCreationAction>[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,
|
||||
],
|
||||
);
|
||||
|
||||
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 = normalizeAgentBackedProfile(profileToSave);
|
||||
if (isAgentDraftResultView) {
|
||||
const syncedResult =
|
||||
await syncAgentDraftResultProfile(profileToSave);
|
||||
// 作品库自动保存优先落同步后 session 重编译出的结果,避免继续保存旧的前端内存态。
|
||||
latestProfileToSave = normalizeAgentBackedProfile(
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user