init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View 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,
};
}