935 lines
29 KiB
TypeScript
935 lines
29 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import type {
|
|
CustomWorldAgentActionRequest,
|
|
CustomWorldAgentOperationRecord,
|
|
CustomWorldAgentSessionSnapshot,
|
|
SendCustomWorldAgentMessageRequest,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import {
|
|
buildAgentDraftFoundationAnchorEntries,
|
|
buildAgentDraftFoundationGenerationProgress,
|
|
buildAgentDraftFoundationSettingText,
|
|
isDraftFoundationOperation,
|
|
isDraftFoundationOperationRunning,
|
|
} from '../../services/customWorldAgentGenerationProgress';
|
|
import {
|
|
readCustomWorldAgentUiState,
|
|
shouldRestoreCustomWorldAgentUiState,
|
|
writeCustomWorldAgentUiState,
|
|
} from '../../services/customWorldAgentUiState';
|
|
import {
|
|
createRpgCreationSession,
|
|
executeRpgCreationAction,
|
|
getRpgCreationResultView,
|
|
getRpgCreationSession,
|
|
streamRpgCreationMessage,
|
|
} from '../../services/rpg-creation';
|
|
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
import {
|
|
buildOptimisticAgentMessage,
|
|
createFailedAgentOperation,
|
|
resolveRpgCreationErrorMessage,
|
|
} from './rpgEntryShared';
|
|
import type {
|
|
CustomWorldGenerationViewSource,
|
|
CustomWorldResultViewSource,
|
|
SelectionStage,
|
|
} from './rpgEntryTypes';
|
|
|
|
type UseRpgCreationSessionControllerParams = {
|
|
userId: string | null | undefined;
|
|
openLoginModal?:
|
|
| ((postLoginAction?: (() => void) | null) => void)
|
|
| undefined;
|
|
selectionStage: SelectionStage;
|
|
setSelectionStage: (stage: SelectionStage) => void;
|
|
enterCreateTab?: (() => void) | undefined;
|
|
onSessionOpened?: (() => void) | undefined;
|
|
};
|
|
|
|
type PendingAgentUserMessage = {
|
|
sessionId: string;
|
|
message: CustomWorldAgentSessionSnapshot['messages'][number];
|
|
};
|
|
|
|
const AGENT_DRAFT_RESULT_AUTO_OPEN_MAX_ATTEMPTS = 12;
|
|
const AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS = 900;
|
|
|
|
export function useRpgCreationSessionController(
|
|
params: UseRpgCreationSessionControllerParams,
|
|
) {
|
|
const {
|
|
userId,
|
|
openLoginModal,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
enterCreateTab,
|
|
onSessionOpened,
|
|
} = params;
|
|
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
|
|
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 =
|
|
canResolveInitialAgentSessionOwner &&
|
|
(!initialAgentUiStateRef.current.ownerUserId ||
|
|
initialAgentUiStateRef.current.ownerUserId === userId);
|
|
const isHydratingInitialAgentWorkspaceRef = useRef(
|
|
Boolean(
|
|
initialAgentSessionId &&
|
|
shouldRestoreInitialAgentUiStateRef.current &&
|
|
isInitialAgentUiStateOwnedByCurrentUser,
|
|
),
|
|
);
|
|
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
|
const hasRequestedInitialAgentWorkspaceAuthRef = useRef(false);
|
|
const isAgentDraftResultAutoOpenSuppressedRef = useRef(false);
|
|
const currentAgentSessionIdRef = useRef<string | null>(null);
|
|
const activeAgentReplyAbortControllerRef = useRef<AbortController | null>(
|
|
null,
|
|
);
|
|
const latestAgentSessionSyncRequestIdRef = useRef(0);
|
|
|
|
const [isCreatingAgentSession, setIsCreatingAgentSession] = useState(false);
|
|
const [activeAgentSessionId, setActiveAgentSessionId] = useState<
|
|
string | null
|
|
>(() =>
|
|
shouldRestoreInitialAgentUiStateRef.current &&
|
|
isInitialAgentUiStateOwnedByCurrentUser
|
|
? (initialAgentUiStateRef.current.activeSessionId ?? null)
|
|
: null,
|
|
);
|
|
const [activeAgentOperationId, setActiveAgentOperationId] = useState<
|
|
string | null
|
|
>(() =>
|
|
shouldRestoreInitialAgentUiStateRef.current &&
|
|
isInitialAgentUiStateOwnedByCurrentUser
|
|
? (initialAgentUiStateRef.current.activeOperationId ?? null)
|
|
: null,
|
|
);
|
|
const [agentSession, setAgentSession] =
|
|
useState<CustomWorldAgentSessionSnapshot | null>(null);
|
|
const [agentOperation, setAgentOperation] =
|
|
useState<CustomWorldAgentOperationRecord | null>(null);
|
|
const [streamingAgentReplyText, setStreamingAgentReplyText] = useState('');
|
|
const [isStreamingAgentReply, setIsStreamingAgentReply] = useState(false);
|
|
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 [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
|
useState<CustomWorldProfile | null>(null);
|
|
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
|
|
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
|
|
useState<CustomWorldGenerationViewSource>(null);
|
|
const [customWorldResultViewSource, setCustomWorldResultViewSource] =
|
|
useState<CustomWorldResultViewSource>(null);
|
|
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
|
useState<number | null>(null);
|
|
const pendingAgentUserMessageRef = useRef<PendingAgentUserMessage | null>(
|
|
null,
|
|
);
|
|
const latestAgentResultViewOpenRequestIdRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
currentAgentSessionIdRef.current = agentSession?.sessionId ?? null;
|
|
}, [agentSession]);
|
|
|
|
useEffect(() => {
|
|
pendingAgentUserMessageRef.current = pendingAgentUserMessage;
|
|
}, [pendingAgentUserMessage]);
|
|
|
|
const invalidateAgentSessionSyncRequests = useCallback(() => {
|
|
latestAgentSessionSyncRequestIdRef.current += 1;
|
|
}, []);
|
|
|
|
const abortActiveAgentReplyStream = useCallback(() => {
|
|
activeAgentReplyAbortControllerRef.current?.abort();
|
|
activeAgentReplyAbortControllerRef.current = null;
|
|
}, []);
|
|
|
|
const mergePendingAgentUserMessageIntoSession = useCallback(
|
|
(
|
|
session: CustomWorldAgentSessionSnapshot | null,
|
|
pending: PendingAgentUserMessage | null = pendingAgentUserMessageRef.current,
|
|
) => {
|
|
if (!session || !pending || pending.sessionId !== session.sessionId) {
|
|
return session;
|
|
}
|
|
|
|
const hasServerEchoedPendingMessage = session.messages.some(
|
|
(message) => message.id === pending.message.id,
|
|
);
|
|
if (hasServerEchoedPendingMessage) {
|
|
return session;
|
|
}
|
|
|
|
return {
|
|
...session,
|
|
messages: [...session.messages, pending.message],
|
|
updatedAt: pending.message.createdAt,
|
|
};
|
|
},
|
|
[],
|
|
);
|
|
|
|
const persistAgentUiState = useCallback(
|
|
(
|
|
nextSessionId: string | null,
|
|
nextOperationId: string | null,
|
|
nextGenerationSource: CustomWorldGenerationViewSource = null,
|
|
) => {
|
|
setActiveAgentSessionId(nextSessionId);
|
|
setActiveAgentOperationId(nextOperationId);
|
|
writeCustomWorldAgentUiState({
|
|
activeSessionId: nextSessionId,
|
|
activeOperationId: nextOperationId,
|
|
customWorldGenerationSource: nextGenerationSource,
|
|
// 工作区 session 是按 userId 持久化的,恢复指针必须绑定当前登录用户,
|
|
// 避免切换账号或复用旧 URL 时反复请求不属于当前用户的 session 产生 404。
|
|
ownerUserId: nextSessionId ? userId : null,
|
|
});
|
|
},
|
|
[userId],
|
|
);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (!initialAgentSessionId || hasAppliedInitialAgentWorkspaceRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
initialAgentUiStateRef.current.ownerUserId &&
|
|
userId &&
|
|
initialAgentUiStateRef.current.ownerUserId !== userId
|
|
) {
|
|
hasAppliedInitialAgentWorkspaceRef.current = true;
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
persistAgentUiState(null, null);
|
|
return;
|
|
}
|
|
|
|
if (!shouldRestoreInitialAgentUiStateRef.current) {
|
|
return;
|
|
}
|
|
|
|
enterCreateTab?.();
|
|
|
|
if (!userId) {
|
|
if (!hasRequestedInitialAgentWorkspaceAuthRef.current) {
|
|
hasRequestedInitialAgentWorkspaceAuthRef.current = true;
|
|
openLoginModal?.(() => {
|
|
if (
|
|
initialAgentUiStateRef.current.activeOperationId &&
|
|
initialAgentUiStateRef.current.customWorldGenerationSource ===
|
|
'agent-draft-foundation'
|
|
) {
|
|
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
|
setSelectionStage('custom-world-generating');
|
|
return;
|
|
}
|
|
|
|
setSelectionStage('agent-workspace');
|
|
});
|
|
}
|
|
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
|
|
) {
|
|
hasAppliedInitialAgentWorkspaceRef.current = true;
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
persistAgentUiState(null, null);
|
|
return;
|
|
}
|
|
|
|
hasAppliedInitialAgentWorkspaceRef.current = true;
|
|
if (
|
|
initialAgentUiStateRef.current.activeOperationId &&
|
|
initialAgentUiStateRef.current.customWorldGenerationSource ===
|
|
'agent-draft-foundation'
|
|
) {
|
|
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
|
setCustomWorldResultViewSource(null);
|
|
setSelectionStage('custom-world-generating');
|
|
return;
|
|
}
|
|
|
|
setSelectionStage('agent-workspace');
|
|
}, [
|
|
enterCreateTab,
|
|
openLoginModal,
|
|
persistAgentUiState,
|
|
setSelectionStage,
|
|
userId,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
selectionStage !== 'agent-workspace' &&
|
|
selectionStage !== 'custom-world-generating'
|
|
) {
|
|
abortActiveAgentReplyStream();
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
}
|
|
}, [abortActiveAgentReplyStream, selectionStage]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
abortActiveAgentReplyStream();
|
|
};
|
|
}, [abortActiveAgentReplyStream]);
|
|
|
|
useEffect(() => {
|
|
if (!activeAgentSessionId) {
|
|
abortActiveAgentReplyStream();
|
|
invalidateAgentSessionSyncRequests();
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setIsLoadingAgentSession(false);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setPendingAgentUserMessage(null);
|
|
setAgentWorkspaceRestoreError(null);
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
return;
|
|
}
|
|
|
|
if (!userId) {
|
|
abortActiveAgentReplyStream();
|
|
invalidateAgentSessionSyncRequests();
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setIsLoadingAgentSession(false);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setPendingAgentUserMessage(null);
|
|
setAgentWorkspaceRestoreError(null);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
const isInitialWorkspaceRestore =
|
|
isHydratingInitialAgentWorkspaceRef.current &&
|
|
activeAgentSessionId === initialAgentUiStateRef.current.activeSessionId;
|
|
|
|
if (currentAgentSessionIdRef.current !== activeAgentSessionId) {
|
|
abortActiveAgentReplyStream();
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setPendingAgentUserMessage(null);
|
|
}
|
|
|
|
setIsLoadingAgentSession(true);
|
|
|
|
void syncAgentSessionSnapshot(activeAgentSessionId)
|
|
.then(() => {
|
|
if (!cancelled) {
|
|
setCreationTypeError(null);
|
|
setAgentWorkspaceRestoreError(null);
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
// 登录后自动恢复的是“上一次残留的工作区指针”,
|
|
// 这里失败时应优先静默清理,避免把旧恢复错误冒充成当前登录已失效。
|
|
if (isInitialWorkspaceRestore) {
|
|
setAgentWorkspaceRestoreError(null);
|
|
} else {
|
|
setAgentWorkspaceRestoreError(
|
|
resolveRpgCreationErrorMessage(
|
|
error,
|
|
'读取 Agent 共创工作区失败。',
|
|
),
|
|
);
|
|
}
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
persistAgentUiState(null, null);
|
|
enterCreateTab?.();
|
|
setSelectionStage('platform');
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) {
|
|
setIsLoadingAgentSession(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
activeAgentSessionId,
|
|
abortActiveAgentReplyStream,
|
|
enterCreateTab,
|
|
invalidateAgentSessionSyncRequests,
|
|
persistAgentUiState,
|
|
setSelectionStage,
|
|
syncAgentSessionSnapshot,
|
|
userId,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!isDraftFoundationOperationRunning(agentOperation) ||
|
|
agentDraftGenerationStartedAt
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setAgentDraftGenerationStartedAt(Date.now());
|
|
}, [agentDraftGenerationStartedAt, agentOperation]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
selectionStage !== 'custom-world-generating' ||
|
|
customWorldGenerationViewSource !== 'agent-draft-foundation' ||
|
|
!isDraftFoundationOperation(agentOperation) ||
|
|
agentOperation.status !== 'completed'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
void (async () => {
|
|
for (
|
|
let attempt = 1;
|
|
attempt <= AGENT_DRAFT_RESULT_AUTO_OPEN_MAX_ATTEMPTS;
|
|
attempt += 1
|
|
) {
|
|
await new Promise((resolve) => {
|
|
window.setTimeout(resolve, AGENT_DRAFT_RESULT_AUTO_OPEN_RETRY_MS);
|
|
});
|
|
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
const latestResultView = activeAgentSessionId
|
|
? await syncAgentCreationResultView(activeAgentSessionId).catch(
|
|
() => null,
|
|
)
|
|
: null;
|
|
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
const draftResultProfile =
|
|
rpgCreationPreviewAdapter.buildPreviewFromResultView(
|
|
latestResultView,
|
|
);
|
|
if (!draftResultProfile) {
|
|
continue;
|
|
}
|
|
|
|
setGeneratedCustomWorldProfile(draftResultProfile);
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource('agent-draft');
|
|
setSelectionStage('custom-world-result');
|
|
return;
|
|
}
|
|
|
|
if (!cancelled) {
|
|
setAgentDraftGenerationStartedAt(null);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
activeAgentSessionId,
|
|
agentOperation,
|
|
agentSession,
|
|
customWorldGenerationViewSource,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
syncAgentCreationResultView,
|
|
]);
|
|
|
|
const agentDraftSettingPreview = useMemo(
|
|
() => buildAgentDraftFoundationSettingText(agentSession),
|
|
[agentSession],
|
|
);
|
|
const agentDraftAnchorPreviewEntries = useMemo(
|
|
() => buildAgentDraftFoundationAnchorEntries(agentSession),
|
|
[agentSession],
|
|
);
|
|
const agentDraftGenerationProgress = useMemo(
|
|
() =>
|
|
buildAgentDraftFoundationGenerationProgress(
|
|
agentOperation,
|
|
agentDraftGenerationStartedAt,
|
|
),
|
|
[agentDraftGenerationStartedAt, agentOperation],
|
|
);
|
|
|
|
const isAgentDraftGenerationView =
|
|
customWorldGenerationViewSource === 'agent-draft-foundation';
|
|
const isAgentDraftResultView = customWorldResultViewSource === 'agent-draft';
|
|
const isActiveGenerationRunning =
|
|
isDraftFoundationOperationRunning(agentOperation);
|
|
const activeGenerationError =
|
|
isDraftFoundationOperation(agentOperation) &&
|
|
agentOperation.status === 'failed'
|
|
? agentOperation.error || agentOperation.phaseDetail
|
|
: null;
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!agentSession ||
|
|
!activeAgentSessionId ||
|
|
agentSession.draftCards.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (isAgentDraftResultAutoOpenSuppressedRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
selectionStage !== 'agent-workspace' &&
|
|
selectionStage !== 'custom-world-result'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
selectionStage === 'custom-world-result' &&
|
|
generatedCustomWorldProfile
|
|
) {
|
|
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;
|
|
};
|
|
}, [
|
|
activeAgentSessionId,
|
|
agentSession,
|
|
generatedCustomWorldProfile,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
syncAgentCreationResultView,
|
|
]);
|
|
|
|
const openRpgAgentWorkspace = useCallback(
|
|
async (seedText = '') => {
|
|
if (isCreatingAgentSession) {
|
|
return;
|
|
}
|
|
|
|
setIsCreatingAgentSession(true);
|
|
setCreationTypeError(null);
|
|
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
|
invalidateAgentSessionSyncRequests();
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setPendingAgentUserMessage(null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
|
|
try {
|
|
const { session } = await createRpgCreationSession(
|
|
seedText ? { seedText } : {},
|
|
);
|
|
isHydratingInitialAgentWorkspaceRef.current = false;
|
|
setAgentSession(session);
|
|
setAgentOperation(null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
enterCreateTab?.();
|
|
onSessionOpened?.();
|
|
persistAgentUiState(session.sessionId, null);
|
|
setSelectionStage('agent-workspace');
|
|
} catch (error) {
|
|
setCreationTypeError(
|
|
resolveRpgCreationErrorMessage(error, '开启共创工作台失败。'),
|
|
);
|
|
} finally {
|
|
setIsCreatingAgentSession(false);
|
|
}
|
|
},
|
|
[
|
|
enterCreateTab,
|
|
invalidateAgentSessionSyncRequests,
|
|
isCreatingAgentSession,
|
|
onSessionOpened,
|
|
persistAgentUiState,
|
|
setSelectionStage,
|
|
],
|
|
);
|
|
|
|
const submitAgentMessage = useCallback(
|
|
async (payload: SendCustomWorldAgentMessageRequest) => {
|
|
if (!activeAgentSessionId || isStreamingAgentReply) {
|
|
return;
|
|
}
|
|
|
|
const optimisticUserMessage = buildOptimisticAgentMessage({
|
|
id: payload.clientMessageId,
|
|
role: 'user',
|
|
kind: 'chat',
|
|
text: payload.text.trim(),
|
|
});
|
|
const pendingMessagePayload: PendingAgentUserMessage = {
|
|
sessionId: activeAgentSessionId,
|
|
message: optimisticUserMessage,
|
|
};
|
|
|
|
setAgentOperation(null);
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(true);
|
|
setPendingAgentUserMessage(pendingMessagePayload);
|
|
const replyAbortController = new AbortController();
|
|
activeAgentReplyAbortControllerRef.current = replyAbortController;
|
|
setAgentSession((current) =>
|
|
mergePendingAgentUserMessageIntoSession(current, pendingMessagePayload),
|
|
);
|
|
|
|
try {
|
|
const nextSession = await streamRpgCreationMessage(
|
|
activeAgentSessionId,
|
|
payload,
|
|
{
|
|
onUpdate: (text) => {
|
|
if (replyAbortController.signal.aborted) {
|
|
return;
|
|
}
|
|
setStreamingAgentReplyText(text);
|
|
},
|
|
signal: replyAbortController.signal,
|
|
},
|
|
);
|
|
if (replyAbortController.signal.aborted) {
|
|
return;
|
|
}
|
|
const mergedNextSession = mergePendingAgentUserMessageIntoSession(
|
|
nextSession,
|
|
pendingMessagePayload,
|
|
);
|
|
setAgentSession(mergedNextSession);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
const hasServerEchoedPendingMessage = nextSession.messages.some(
|
|
(message) => message.id === optimisticUserMessage.id,
|
|
);
|
|
setPendingAgentUserMessage(
|
|
hasServerEchoedPendingMessage ? null : pendingMessagePayload,
|
|
);
|
|
} catch (error) {
|
|
if (replyAbortController.signal.aborted) {
|
|
return;
|
|
}
|
|
const errorMessage = resolveRpgCreationErrorMessage(
|
|
error,
|
|
'发送共创消息失败。',
|
|
);
|
|
const warningMessage = buildOptimisticAgentMessage({
|
|
id: `message-error-${Date.now()}`,
|
|
role: 'assistant',
|
|
kind: 'warning',
|
|
text: errorMessage,
|
|
});
|
|
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
|
|
) {
|
|
activeAgentReplyAbortControllerRef.current = null;
|
|
}
|
|
if (!replyAbortController.signal.aborted) {
|
|
setIsStreamingAgentReply(false);
|
|
}
|
|
}
|
|
},
|
|
[
|
|
activeAgentSessionId,
|
|
isStreamingAgentReply,
|
|
mergePendingAgentUserMessageIntoSession,
|
|
persistAgentUiState,
|
|
],
|
|
);
|
|
|
|
const executeAgentAction = useCallback(
|
|
async (payload: CustomWorldAgentActionRequest) => {
|
|
if (!activeAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
const isDraftFoundationAction = payload.action === 'draft_foundation';
|
|
|
|
if (isDraftFoundationAction) {
|
|
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
|
setCustomWorldResultViewSource(null);
|
|
setAgentDraftGenerationStartedAt(Date.now());
|
|
setSelectionStage('custom-world-generating');
|
|
}
|
|
|
|
try {
|
|
const { operation } = await executeRpgCreationAction(
|
|
activeAgentSessionId,
|
|
payload,
|
|
);
|
|
setAgentOperation(operation);
|
|
persistAgentUiState(
|
|
activeAgentSessionId,
|
|
operation.operationId,
|
|
isDraftFoundationAction ? 'agent-draft-foundation' : null,
|
|
);
|
|
} catch (error) {
|
|
const errorMessage = resolveRpgCreationErrorMessage(
|
|
error,
|
|
'执行共创操作失败。',
|
|
);
|
|
setAgentOperation(
|
|
createFailedAgentOperation({
|
|
type:
|
|
payload.action === 'draft_foundation'
|
|
? 'draft_foundation'
|
|
: payload.action,
|
|
phaseLabel: '执行操作失败',
|
|
error: errorMessage,
|
|
}),
|
|
);
|
|
persistAgentUiState(
|
|
activeAgentSessionId,
|
|
null,
|
|
isDraftFoundationAction ? 'agent-draft-foundation' : null,
|
|
);
|
|
}
|
|
},
|
|
[activeAgentSessionId, persistAgentUiState, setSelectionStage],
|
|
);
|
|
|
|
const setNormalizedGeneratedCustomWorldProfile = useCallback(
|
|
(profile: CustomWorldProfile | null) => {
|
|
setGeneratedCustomWorldProfile(profile);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const resetSessionViewState = useCallback(() => {
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setPendingAgentUserMessage(null);
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
}, []);
|
|
|
|
const suppressAgentDraftResultAutoOpen = useCallback(() => {
|
|
isAgentDraftResultAutoOpenSuppressedRef.current = true;
|
|
}, []);
|
|
|
|
const releaseAgentDraftResultAutoOpenSuppression = useCallback(() => {
|
|
isAgentDraftResultAutoOpenSuppressedRef.current = false;
|
|
}, []);
|
|
|
|
return {
|
|
initialAgentSessionId:
|
|
shouldRestoreInitialAgentUiStateRef.current &&
|
|
isInitialAgentUiStateOwnedByCurrentUser
|
|
? (initialAgentUiStateRef.current.activeSessionId ?? null)
|
|
: null,
|
|
isCreatingAgentSession,
|
|
activeAgentSessionId,
|
|
activeAgentOperationId,
|
|
agentSession,
|
|
setAgentSession,
|
|
agentOperation,
|
|
setAgentOperation,
|
|
resetSessionViewState,
|
|
streamingAgentReplyText,
|
|
isStreamingAgentReply,
|
|
isLoadingAgentSession,
|
|
creationTypeError,
|
|
setCreationTypeError,
|
|
agentWorkspaceRestoreError,
|
|
customWorldError,
|
|
setCustomWorldError,
|
|
generatedCustomWorldProfile,
|
|
setGeneratedCustomWorldProfile: setNormalizedGeneratedCustomWorldProfile,
|
|
rawSetGeneratedCustomWorldProfile: setGeneratedCustomWorldProfile,
|
|
customWorldGenerationViewSource,
|
|
setCustomWorldGenerationViewSource,
|
|
customWorldResultViewSource,
|
|
setCustomWorldResultViewSource,
|
|
agentDraftGenerationStartedAt,
|
|
setAgentDraftGenerationStartedAt,
|
|
agentDraftSettingPreview,
|
|
agentDraftAnchorPreviewEntries,
|
|
agentDraftResultProfile:
|
|
rpgCreationPreviewAdapter.buildPreviewFromResultPreview(
|
|
agentSession?.resultPreview,
|
|
),
|
|
agentDraftGenerationProgress,
|
|
isAgentDraftGenerationView,
|
|
isAgentDraftResultView,
|
|
isActiveGenerationRunning,
|
|
activeGenerationError,
|
|
isAgentDraftResultAutoOpenSuppressedRef,
|
|
suppressAgentDraftResultAutoOpen,
|
|
releaseAgentDraftResultAutoOpenSuppression,
|
|
persistAgentUiState,
|
|
syncAgentSessionSnapshot,
|
|
syncAgentCreationResultView,
|
|
openRpgAgentWorkspace,
|
|
submitAgentMessage,
|
|
executeAgentAction,
|
|
};
|
|
}
|