This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -21,6 +21,7 @@ import {
import {
createRpgCreationSession,
executeRpgCreationAction,
getRpgCreationResultView,
getRpgCreationSession,
streamRpgCreationMessage,
} from '../../services/rpg-creation';
@@ -29,7 +30,6 @@ import type { CustomWorldProfile } from '../../types';
import {
buildOptimisticAgentMessage,
createFailedAgentOperation,
normalizeAgentBackedProfile,
resolveRpgCreationErrorMessage,
} from './rpgEntryShared';
import type {
@@ -40,7 +40,9 @@ import type {
type UseRpgCreationSessionControllerParams = {
userId: string | null | undefined;
openLoginModal?: ((postLoginAction?: (() => void) | null) => void) | undefined;
openLoginModal?:
| ((postLoginAction?: (() => void) | null) => void)
| undefined;
selectionStage: SelectionStage;
setSelectionStage: (stage: SelectionStage) => void;
enterCreateTab?: (() => void) | undefined;
@@ -70,12 +72,23 @@ export function useRpgCreationSessionController(
const shouldRestoreInitialAgentUiStateRef = useRef(
shouldRestoreCustomWorldAgentUiState(),
);
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
const isInitialAgentGenerationRestore =
Boolean(initialAgentUiStateRef.current.activeOperationId) &&
initialAgentUiStateRef.current.customWorldGenerationSource ===
'agent-draft-foundation';
const canResolveInitialAgentSessionOwner =
!initialAgentSessionId ||
!userId ||
Boolean(initialAgentUiStateRef.current.ownerUserId) ||
isInitialAgentGenerationRestore;
const isInitialAgentUiStateOwnedByCurrentUser =
!initialAgentUiStateRef.current.ownerUserId ||
initialAgentUiStateRef.current.ownerUserId === userId;
canResolveInitialAgentSessionOwner &&
(!initialAgentUiStateRef.current.ownerUserId ||
initialAgentUiStateRef.current.ownerUserId === userId);
const isHydratingInitialAgentWorkspaceRef = useRef(
Boolean(
initialAgentUiStateRef.current.activeSessionId &&
initialAgentSessionId &&
shouldRestoreInitialAgentUiStateRef.current &&
isInitialAgentUiStateOwnedByCurrentUser,
),
@@ -115,9 +128,12 @@ export function useRpgCreationSessionController(
const [pendingAgentUserMessage, setPendingAgentUserMessage] =
useState<PendingAgentUserMessage | null>(null);
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
const [creationTypeError, setCreationTypeError] = useState<string | null>(null);
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] =
useState<string | null>(null);
const [creationTypeError, setCreationTypeError] = useState<string | null>(
null,
);
const [agentWorkspaceRestoreError, setAgentWorkspaceRestoreError] = useState<
string | null
>(null);
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
useState<CustomWorldProfile | null>(null);
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
@@ -127,7 +143,10 @@ export function useRpgCreationSessionController(
useState<CustomWorldResultViewSource>(null);
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
useState<number | null>(null);
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(null);
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(
null,
);
const latestAgentResultViewOpenRequestIdRef = useRef(0);
useEffect(() => {
currentAgentSessionIdRef.current = agentSession?.sessionId ?? null;
@@ -191,35 +210,54 @@ export function useRpgCreationSessionController(
[userId],
);
const syncAgentSessionSnapshot = useCallback(async (sessionId: string) => {
const requestId = latestAgentSessionSyncRequestIdRef.current + 1;
latestAgentSessionSyncRequestIdRef.current = requestId;
const nextSession = await getRpgCreationSession(sessionId);
const mergedSession = mergePendingAgentUserMessageIntoSession(nextSession);
const syncAgentSessionSnapshot = useCallback(
async (sessionId: string) => {
const requestId = latestAgentSessionSyncRequestIdRef.current + 1;
latestAgentSessionSyncRequestIdRef.current = requestId;
const nextSession = await getRpgCreationSession(sessionId);
const mergedSession =
mergePendingAgentUserMessageIntoSession(nextSession);
if (latestAgentSessionSyncRequestIdRef.current === requestId) {
setAgentSession(mergedSession);
const currentPendingAgentUserMessage = pendingAgentUserMessageRef.current;
const hasServerEchoedPendingMessage =
currentPendingAgentUserMessage?.sessionId === nextSession.sessionId &&
nextSession.messages.some(
(message) => message.id === currentPendingAgentUserMessage.message.id,
);
if (hasServerEchoedPendingMessage) {
setPendingAgentUserMessage(null);
if (latestAgentSessionSyncRequestIdRef.current === requestId) {
setAgentSession(mergedSession);
const currentPendingAgentUserMessage =
pendingAgentUserMessageRef.current;
const hasServerEchoedPendingMessage =
currentPendingAgentUserMessage?.sessionId === nextSession.sessionId &&
nextSession.messages.some(
(message) =>
message.id === currentPendingAgentUserMessage.message.id,
);
if (hasServerEchoedPendingMessage) {
setPendingAgentUserMessage(null);
}
}
}
return mergedSession;
}, [mergePendingAgentUserMessageIntoSession]);
return mergedSession;
},
[mergePendingAgentUserMessageIntoSession],
);
const syncAgentCreationResultView = useCallback(
async (sessionId: string) => {
const resultView = await getRpgCreationResultView(sessionId);
const mergedSession = mergePendingAgentUserMessageIntoSession(
resultView.session,
);
setAgentSession(mergedSession);
return {
...resultView,
session: mergedSession ?? resultView.session,
};
},
[mergePendingAgentUserMessageIntoSession],
);
useEffect(() => {
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
const initialAgentSessionId =
initialAgentUiStateRef.current.activeSessionId;
if (
!initialAgentSessionId ||
hasAppliedInitialAgentWorkspaceRef.current
) {
if (!initialAgentSessionId || hasAppliedInitialAgentWorkspaceRef.current) {
return;
}
@@ -260,6 +298,20 @@ export function useRpgCreationSessionController(
return;
}
if (
!initialAgentUiStateRef.current.ownerUserId &&
!(
initialAgentUiStateRef.current.activeOperationId &&
initialAgentUiStateRef.current.customWorldGenerationSource ===
'agent-draft-foundation'
)
) {
hasAppliedInitialAgentWorkspaceRef.current = true;
isHydratingInitialAgentWorkspaceRef.current = false;
persistAgentUiState(null, null);
return;
}
if (
initialAgentUiStateRef.current.ownerUserId &&
initialAgentUiStateRef.current.ownerUserId !== userId
@@ -283,7 +335,13 @@ export function useRpgCreationSessionController(
}
setSelectionStage('agent-workspace');
}, [enterCreateTab, openLoginModal, persistAgentUiState, setSelectionStage, userId]);
}, [
enterCreateTab,
openLoginModal,
persistAgentUiState,
setSelectionStage,
userId,
]);
useEffect(() => {
if (
@@ -365,7 +423,10 @@ export function useRpgCreationSessionController(
setAgentWorkspaceRestoreError(null);
} else {
setAgentWorkspaceRestoreError(
resolveRpgCreationErrorMessage(error, '读取 Agent 共创工作区失败。'),
resolveRpgCreationErrorMessage(
error,
'读取 Agent 共创工作区失败。',
),
);
}
setAgentSession(null);
@@ -426,37 +487,32 @@ export function useRpgCreationSessionController(
attempt += 1
) {
await new Promise((resolve) => {
window.setTimeout(
resolve,
AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS,
);
window.setTimeout(resolve, AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS);
});
if (cancelled) {
return;
}
const latestSession = activeAgentSessionId
? await syncAgentSessionSnapshot(activeAgentSessionId).catch(
const latestResultView = activeAgentSessionId
? await syncAgentCreationResultView(activeAgentSessionId).catch(
() => null,
)
: agentSession;
: null;
if (cancelled) {
return;
}
const draftResultProfile =
rpgCreationPreviewAdapter.buildPreviewFromSession(
latestSession ?? agentSession,
rpgCreationPreviewAdapter.buildPreviewFromResultView(
latestResultView,
);
if (!draftResultProfile) {
continue;
}
setGeneratedCustomWorldProfile(
normalizeAgentBackedProfile(draftResultProfile),
);
setGeneratedCustomWorldProfile(draftResultProfile);
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('agent-draft');
@@ -479,7 +535,7 @@ export function useRpgCreationSessionController(
customWorldGenerationViewSource,
selectionStage,
setSelectionStage,
syncAgentSessionSnapshot,
syncAgentCreationResultView,
]);
const agentDraftSettingPreview = useMemo(
@@ -490,25 +546,6 @@ export function useRpgCreationSessionController(
() => buildAgentDraftFoundationAnchorEntries(agentSession),
[agentSession],
);
const agentDraftResultProfile = useMemo(
() => rpgCreationPreviewAdapter.buildPreviewFromSession(agentSession),
[agentSession],
);
const shouldAutoOpenAgentDraftResult = useMemo(
() =>
Boolean(
agentDraftResultProfile &&
agentSession &&
(agentSession.stage === 'object_refining' ||
agentSession.stage === 'visual_refining' ||
agentSession.stage === 'long_tail_review' ||
agentSession.stage === 'ready_to_publish' ||
agentSession.stage === 'published') &&
agentSession.draftCards.length > 0,
),
[agentDraftResultProfile, agentSession],
);
const agentDraftGenerationProgress = useMemo(
() =>
buildAgentDraftFoundationGenerationProgress(
@@ -530,7 +567,11 @@ export function useRpgCreationSessionController(
: null;
useEffect(() => {
if (!shouldAutoOpenAgentDraftResult || !agentDraftResultProfile) {
if (
!agentSession ||
!activeAgentSessionId ||
agentSession.draftCards.length === 0
) {
return;
}
@@ -538,28 +579,63 @@ export function useRpgCreationSessionController(
return;
}
if (selectionStage === 'agent-workspace') {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
isAgentDraftResultAutoOpenSuppressedRef.current = false;
setSelectionStage('custom-world-result');
if (
selectionStage !== 'agent-workspace' &&
selectionStage !== 'custom-world-result'
) {
return;
}
if (
selectionStage === 'custom-world-result' &&
!generatedCustomWorldProfile
generatedCustomWorldProfile
) {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
isAgentDraftResultAutoOpenSuppressedRef.current = false;
return;
}
const requestId = latestAgentResultViewOpenRequestIdRef.current + 1;
latestAgentResultViewOpenRequestIdRef.current = requestId;
let cancelled = false;
void syncAgentCreationResultView(activeAgentSessionId)
.then((resultView) => {
if (
cancelled ||
latestAgentResultViewOpenRequestIdRef.current !== requestId ||
isAgentDraftResultAutoOpenSuppressedRef.current ||
resultView.targetStage !== 'custom-world-result'
) {
return;
}
const resultProfile =
rpgCreationPreviewAdapter.buildPreviewFromResultView(resultView);
if (!resultProfile) {
return;
}
setGeneratedCustomWorldProfile(resultProfile);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(
resultView.resultViewSource ?? 'agent-draft',
);
isAgentDraftResultAutoOpenSuppressedRef.current = false;
if (selectionStage === 'agent-workspace') {
setSelectionStage('custom-world-result');
}
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [
agentDraftResultProfile,
activeAgentSessionId,
agentSession,
generatedCustomWorldProfile,
selectionStage,
setSelectionStage,
shouldAutoOpenAgentDraftResult,
syncAgentCreationResultView,
]);
const openRpgAgentWorkspace = useCallback(
@@ -689,26 +765,26 @@ export function useRpgCreationSessionController(
kind: 'warning',
text: errorMessage,
});
setAgentSession((current) =>
{
const mergedCurrentSession = mergePendingAgentUserMessageIntoSession(
current,
pendingMessagePayload,
);
return mergedCurrentSession
? {
...mergedCurrentSession,
messages: [...mergedCurrentSession.messages, warningMessage],
updatedAt: warningMessage.createdAt,
}
: current;
},
);
setAgentSession((current) => {
const mergedCurrentSession = mergePendingAgentUserMessageIntoSession(
current,
pendingMessagePayload,
);
return mergedCurrentSession
? {
...mergedCurrentSession,
messages: [...mergedCurrentSession.messages, warningMessage],
updatedAt: warningMessage.createdAt,
}
: current;
});
setPendingAgentUserMessage(null);
setStreamingAgentReplyText('');
persistAgentUiState(activeAgentSessionId, null);
} finally {
if (activeAgentReplyAbortControllerRef.current === replyAbortController) {
if (
activeAgentReplyAbortControllerRef.current === replyAbortController
) {
activeAgentReplyAbortControllerRef.current = null;
}
if (!replyAbortController.signal.aborted) {
@@ -780,9 +856,7 @@ export function useRpgCreationSessionController(
const setNormalizedGeneratedCustomWorldProfile = useCallback(
(profile: CustomWorldProfile | null) => {
setGeneratedCustomWorldProfile(
profile ? normalizeAgentBackedProfile(profile) : null,
);
setGeneratedCustomWorldProfile(profile);
},
[],
);
@@ -807,7 +881,8 @@ export function useRpgCreationSessionController(
return {
initialAgentSessionId:
shouldRestoreInitialAgentUiStateRef.current
shouldRestoreInitialAgentUiStateRef.current &&
isInitialAgentUiStateOwnedByCurrentUser
? (initialAgentUiStateRef.current.activeSessionId ?? null)
: null,
isCreatingAgentSession,
@@ -837,7 +912,10 @@ export function useRpgCreationSessionController(
setAgentDraftGenerationStartedAt,
agentDraftSettingPreview,
agentDraftAnchorPreviewEntries,
agentDraftResultProfile,
agentDraftResultProfile:
rpgCreationPreviewAdapter.buildPreviewFromResultPreview(
agentSession?.resultPreview,
),
agentDraftGenerationProgress,
isAgentDraftGenerationView,
isAgentDraftResultView,
@@ -848,6 +926,7 @@ export function useRpgCreationSessionController(
releaseAgentDraftResultAutoOpenSuppression,
persistAgentUiState,
syncAgentSessionSnapshot,
syncAgentCreationResultView,
openRpgAgentWorkspace,
submitAgentMessage,
executeAgentAction,