import { useCallback, useRef, useState } from 'react'; import type { TextStreamOptions } from '../../services/aiTypes'; import type { SelectionStage } from './platformEntryTypes'; type CreationAgentMessageLike = { clientMessageId: string; text: string; }; type CreationAgentSessionLike = { sessionId: string; draft?: unknown; messages: Array<{ id: string; role: string; kind?: string; text: string; createdAt?: string; }>; updatedAt?: string; }; type CreationAgentClientAdapter< TSession extends CreationAgentSessionLike, TCreatePayload, TCreateResponse, TMessagePayload extends CreationAgentMessageLike, TActionPayload, TActionResponse, > = { createSession: (payload: TCreatePayload) => Promise; getSession: (sessionId: string) => Promise; streamMessage: ( sessionId: string, payload: TMessagePayload, options?: TextStreamOptions, ) => Promise; executeAction: ( sessionId: string, payload: TActionPayload, ) => Promise; selectSession: (response: TCreateResponse) => TSession; }; type PlatformCreationAgentFlowControllerOptions< TSession extends CreationAgentSessionLike, TCreatePayload, TCreateResponse, TMessagePayload extends CreationAgentMessageLike, TActionPayload, TActionResponse, > = { client: CreationAgentClientAdapter< TSession, TCreatePayload, TCreateResponse, TMessagePayload, TActionPayload, TActionResponse >; createPayload: TCreatePayload; workspaceStage: SelectionStage; resultStage: SelectionStage; platformStage: SelectionStage; isCompileAction: (payload: TActionPayload) => boolean; resolveErrorMessage: (error: unknown, fallback: string) => string; errorMessages: { open: string; restoreMissingSession: string; restore: string; submit: string; execute: string; }; enterCreateTab: () => void; setSelectionStage: (stage: SelectionStage) => void; onSessionOpened?: () => void; onOpenError?: (params: { error: unknown; errorMessage: string; }) => void; onActionComplete?: (params: { payload: TActionPayload; response: TActionResponse; session: TSession; setSession: (session: TSession) => void; }) => Promise | void; beforeExecuteAction?: (params: { payload: TActionPayload; session: TSession; }) => void; onActionError?: (params: { payload: TActionPayload; error: unknown; errorMessage: string; }) => void; }; function buildOptimisticMessage( payload: TMessagePayload, ) { return { id: payload.clientMessageId, role: 'user', kind: 'chat', text: payload.text.trim(), createdAt: new Date().toISOString(), }; } function buildInterruptedAssistantMessage(text: string) { return { id: `assistant-interrupted-${Date.now().toString(36)}`, role: 'assistant', kind: 'warning', text: text.trim(), createdAt: new Date().toISOString(), }; } /** * 轻量作品 Agent 创作流程的通用前端控制器。 * 这里只处理跨玩法一致的会话、流式消息、忙碌态与草稿恢复,玩法结果页和运行态动作留给外层。 */ export function usePlatformCreationAgentFlowController< TSession extends CreationAgentSessionLike, TCreatePayload, TCreateResponse, TMessagePayload extends CreationAgentMessageLike, TActionPayload, TActionResponse, >( options: PlatformCreationAgentFlowControllerOptions< TSession, TCreatePayload, TCreateResponse, TMessagePayload, TActionPayload, TActionResponse >, ) { const [session, setSession] = useState(null); const [error, setError] = useState(null); const [isBusy, setIsBusy] = useState(false); const [streamingReplyText, setStreamingReplyText] = useState(''); const [isStreamingReply, setIsStreamingReply] = useState(false); const latestStreamingReplyTextRef = useRef(''); const updateStreamingReplyText = useCallback((text: string) => { latestStreamingReplyTextRef.current = text; setStreamingReplyText(text); }, []); const resetStreamingReply = useCallback(() => { latestStreamingReplyTextRef.current = ''; setStreamingReplyText(''); setIsStreamingReply(false); }, []); const openWorkspace = useCallback(async (createPayload?: TCreatePayload) => { if (isBusy) { return null; } setIsBusy(true); setError(null); resetStreamingReply(); try { const response = await options.client.createSession( createPayload ?? options.createPayload, ); const nextSession = options.client.selectSession(response); setSession(nextSession); options.enterCreateTab(); options.onSessionOpened?.(); options.setSelectionStage(options.workspaceStage); return nextSession; } catch (caughtError) { const errorMessage = options.resolveErrorMessage( caughtError, options.errorMessages.open, ); setError(errorMessage); options.onOpenError?.({ error: caughtError, errorMessage, }); return null; } finally { setIsBusy(false); } }, [isBusy, options, resetStreamingReply]); const restoreDraft = useCallback( async (sessionId: string | null | undefined) => { const normalizedSessionId = sessionId?.trim(); if (!normalizedSessionId) { setError(options.errorMessages.restoreMissingSession); return null; } setIsBusy(true); setError(null); resetStreamingReply(); try { const response = await options.client.getSession(normalizedSessionId); const nextSession = options.client.selectSession(response); setSession(nextSession); options.enterCreateTab(); options.setSelectionStage( nextSession.draft ? options.resultStage : options.workspaceStage, ); return nextSession; } catch (caughtError) { setError( options.resolveErrorMessage(caughtError, options.errorMessages.restore), ); options.enterCreateTab(); options.setSelectionStage(options.platformStage); return null; } finally { setIsBusy(false); } }, [options, resetStreamingReply], ); const submitMessage = useCallback( async (payload: TMessagePayload) => { if (!session || isStreamingReply) { return; } const optimisticMessage = buildOptimisticMessage(payload); setError(null); updateStreamingReplyText(''); setIsStreamingReply(true); setSession((current) => current ? { ...current, messages: [...current.messages, optimisticMessage], updatedAt: optimisticMessage.createdAt, } : current, ); try { const nextSession = await options.client.streamMessage( session.sessionId, payload, { onUpdate: updateStreamingReplyText, }, ); setSession(nextSession); updateStreamingReplyText(''); } catch (caughtError) { const interruptedReplyText = latestStreamingReplyTextRef.current.trim(); // 上游流可能在已经吐出可读回复后才失败;把这段回复落进本地消息列表,避免 UI 收尾时突然消失。 if (interruptedReplyText) { const interruptedMessage = buildInterruptedAssistantMessage(interruptedReplyText); setSession((current) => current ? { ...current, messages: [...current.messages, interruptedMessage], updatedAt: interruptedMessage.createdAt, } : current, ); } setError( options.resolveErrorMessage(caughtError, options.errorMessages.submit), ); } finally { setIsStreamingReply(false); } }, [isStreamingReply, options, session, updateStreamingReplyText], ); const executeAction = useCallback( async (payload: TActionPayload, sessionOverride?: TSession | null) => { const targetSession = sessionOverride ?? session; if (!targetSession || isBusy) { return; } setIsBusy(true); setError(null); try { options.beforeExecuteAction?.({ payload, session: targetSession }); const response = await options.client.executeAction( targetSession.sessionId, payload, ); await options.onActionComplete?.({ payload, response, session: targetSession, setSession, }); if (options.isCompileAction(payload)) { options.setSelectionStage(options.resultStage); } } catch (caughtError) { const errorMessage = options.resolveErrorMessage( caughtError, options.errorMessages.execute, ); setError(errorMessage); options.onActionError?.({ payload, error: caughtError, errorMessage, }); } finally { setIsBusy(false); } }, [isBusy, options, session], ); const leaveFlow = useCallback(() => { setError(null); resetStreamingReply(); options.enterCreateTab(); options.setSelectionStage(options.platformStage); }, [options, resetStreamingReply]); const resetTransientState = useCallback(() => { setError(null); resetStreamingReply(); }, [resetStreamingReply]); return { session, setSession, error, setError, isBusy, setIsBusy, streamingReplyText, setStreamingReplyText, isStreamingReply, setIsStreamingReply, openWorkspace, restoreDraft, submitMessage, executeAction, leaveFlow, resetTransientState, }; }