import { create } from 'zustand'; import { confirmImageWorkspaceGeneration, createImageWorkspaceConversation, createImageWorkspaceTurnId, createImageWorkspaceProject, fetchImageWorkspace, fetchImageWorkspaceConversation, fetchImageWorkspaceProject, fetchImageWorkspaceTasks, ImageWorkspaceApiError, openImageWorkspaceTaskEvents, renameImageWorkspaceProject, sendImageWorkspaceMessage, } from '@/lib/image-workspace'; import { useAuthStore } from '@/stores/auth'; import { IMAGE_WORKSPACE_UNAVAILABLE_CODE, type DesignAssistantDeltaEvent, type DesignConversation, type DesignConversationSnapshotEvent, type DesignGenerationTask, type DesignGenerationTaskUpdatedEvent, type DesignWorkspace, type DesignWorkspaceBootstrap, type DesignWorkspaceSummary, } from '../../shared/image-workspace'; export type ImageWorkspaceLoadStatus = | 'idle' | 'loading' | 'ready' | 'unavailable' | 'error' | 'auth-required'; export type ImageWorkspaceTaskStreamState = 'idle' | 'connecting' | 'connected' | 'degraded'; export type PendingDesignTurn = { workspaceId: string; conversationId: string; clientTurnId: string; turnRevision: number; userText: string | null; assistantText: string; lastChunkIndex: number; createdAt: string; }; type ImageWorkspaceState = { status: ImageWorkspaceLoadStatus; bootstrap: DesignWorkspaceBootstrap | null; activeWorkspaceId: string | null; activeConversationId: string | null; workspace: DesignWorkspace | null; conversation: DesignConversation | null; creatingConversation: boolean; tasks: DesignGenerationTask[]; pendingTurn: PendingDesignTurn | null; taskStreamState: ImageWorkspaceTaskStreamState; error: string | null; load: () => Promise; createProject: (title: string) => Promise; createConversation: (title?: string) => Promise; renameProject: (workspaceId: string, title: string) => Promise; selectProject: (workspaceId: string) => Promise; selectConversation: (conversationId: string) => Promise; refreshWorkspace: () => Promise; refreshConversation: () => Promise; refreshTasks: () => Promise; connectTaskStream: () => void; disconnectTaskStream: () => void; sendMessage: (message: string, attachmentAssetIds?: string[]) => Promise; confirmGeneration: (quoteId: string) => Promise; reset: () => void; }; let inFlightLoad: Promise | null = null; const TASK_FALLBACK_POLL_INTERVAL_MS = 15_000; const CONFIRMED_TASK_RECONCILE_ATTEMPTS = 5; const CONFIRMED_TASK_RECONCILE_INTERVAL_MS = 250; let activeTaskEventSource: EventSource | null = null; let activeTaskEventWorkspaceId: string | null = null; let activeTaskEventConversationId: string | null = null; let taskStreamGeneration = 0; let taskFallbackTimer: ReturnType | null = null; const taskEventRevisions = new Map(); let workspaceLoadGeneration = 0; let conversationSelectionGeneration = 0; function taskRevisionKey(workspaceId: string, taskId: string): string { return `${workspaceId}:${taskId}`; } function stopFallbackPolling(): void { if (taskFallbackTimer !== null) clearInterval(taskFallbackTimer); taskFallbackTimer = null; } function closeTaskEventSource(): void { taskStreamGeneration += 1; stopFallbackPolling(); if (activeTaskEventSource) { activeTaskEventSource.onopen = null; activeTaskEventSource.onerror = null; activeTaskEventSource.close(); } activeTaskEventSource = null; activeTaskEventWorkspaceId = null; activeTaskEventConversationId = null; taskEventRevisions.clear(); } function parseTaskUpdatedEvent(event: Event): DesignGenerationTaskUpdatedEvent | null { const data = (event as MessageEvent).data; if (typeof data !== 'string') return null; try { const payload = JSON.parse(data) as Partial; if (payload.type !== 'design.generation_task.updated' || typeof payload.id !== 'string' || typeof payload.workspaceId !== 'string' || !Number.isInteger(payload.workspaceViewRevision) || !payload.generationTask || typeof payload.generationTask.taskId !== 'string' || payload.generationTask.workspaceId !== payload.workspaceId) { return null; } return payload as DesignGenerationTaskUpdatedEvent; } catch { return null; } } function parseConversationSnapshotEvent(event: Event): DesignConversationSnapshotEvent | null { const data = (event as MessageEvent).data; if (typeof data !== 'string') return null; try { const payload = JSON.parse(data) as Partial; if (payload.type !== 'design.conversation.snapshot' || typeof payload.id !== 'string' || typeof payload.workspaceId !== 'string' || typeof payload.conversationId !== 'string' || !Number.isInteger(payload.workspaceViewRevision) || !payload.conversation || payload.conversation.workspaceId !== payload.workspaceId || payload.conversation.conversationId !== payload.conversationId || !Array.isArray(payload.conversation.messages) || !Array.isArray(payload.generationTasks) || !payload.generationTasks.every((task) => ( typeof task?.taskId === 'string' && task.workspaceId === payload.workspaceId ))) { return null; } return payload as DesignConversationSnapshotEvent; } catch { return null; } } function parseAssistantDeltaEvent(event: Event): DesignAssistantDeltaEvent | null { const data = (event as MessageEvent).data; if (typeof data !== 'string') return null; try { const payload = JSON.parse(data) as Partial; if (payload.type !== 'design.assistant.delta' || typeof payload.id !== 'string' || typeof payload.workspaceId !== 'string' || typeof payload.conversationId !== 'string' || typeof payload.clientTurnId !== 'string' || !Number.isInteger(payload.turnRevision) || Number(payload.turnRevision) < 1 || !Number.isInteger(payload.chunkIndex) || Number(payload.chunkIndex) < 0 || typeof payload.delta !== 'string' || payload.delta.length === 0 || payload.delta.length > 128) { return null; } return payload as DesignAssistantDeltaEvent; } catch { return null; } } function sortTasks(tasks: DesignGenerationTask[]): DesignGenerationTask[] { return [...tasks].sort((left, right) => ( right.createdAt.localeCompare(left.createdAt) || right.taskId.localeCompare(left.taskId) )); } function mergeTasks( current: DesignGenerationTask[], incoming: DesignGenerationTask[], ): DesignGenerationTask[] { const tasksById = new Map(current.map((task) => [task.taskId, task])); for (const task of incoming) { const existing = tasksById.get(task.taskId); if (!existing || existing.updatedAt <= task.updatedAt) tasksById.set(task.taskId, task); } return sortTasks([...tasksById.values()]); } function unavailable(error: unknown): boolean { return error instanceof ImageWorkspaceApiError && (error.status === 501 || error.code === IMAGE_WORKSPACE_UNAVAILABLE_CODE); } function authenticationRequired(error: unknown): boolean { return error instanceof ImageWorkspaceApiError && error.code === 'AUTH_REQUIRED'; } function messageOf(error: unknown): string { return error instanceof Error ? error.message : String(error); } function toSummary(workspace: DesignWorkspace): DesignWorkspaceSummary { const { conversations: _conversations, ...summary } = workspace; return summary; } function upsertSummary( bootstrap: DesignWorkspaceBootstrap | null, workspace: DesignWorkspace, ): DesignWorkspaceBootstrap | null { if (!bootstrap) return null; const summary = toSummary(workspace); const workspaces = bootstrap.workspaces.filter( (item) => item.workspaceId !== workspace.workspaceId, ); return { ...bootstrap, workspaces: [summary, ...workspaces], }; } function upsertConversation( workspace: DesignWorkspace | null, conversation: DesignConversation, ): DesignWorkspace | null { if (!workspace || workspace.workspaceId !== conversation.workspaceId) return workspace; const conversations = workspace.conversations.filter( (item) => item.conversationId !== conversation.conversationId, ); const { messages: _messages, ...summary } = conversation; return { ...workspace, conversationCount: Math.max(workspace.conversationCount, conversations.length + 1), conversations: [summary, ...conversations], }; } function createPendingTurn( conversation: DesignConversation, clientTurnId: string, userText: string, ): PendingDesignTurn { return { workspaceId: conversation.workspaceId, conversationId: conversation.conversationId, clientTurnId, turnRevision: conversation.turnRevision + 1, userText, assistantText: '', lastChunkIndex: -1, createdAt: new Date().toISOString(), }; } export const useImageWorkspaceStore = create((set, get) => { const stopTaskStream = () => { closeTaskEventSource(); set({ taskStreamState: 'idle' }); }; const startFallbackPolling = () => { if (taskFallbackTimer !== null) return; taskFallbackTimer = setInterval(() => { if (get().taskStreamState !== 'degraded' || !get().activeWorkspaceId) return; void get().refreshWorkspace() .catch(() => null) .then(() => get().refreshConversation().catch(() => null)) .then(() => get().refreshTasks().catch(() => [])); }, TASK_FALLBACK_POLL_INTERVAL_MS); }; const startTaskStream = (workspaceId: string, conversationId: string) => { if (activeTaskEventWorkspaceId === workspaceId && activeTaskEventConversationId === conversationId && get().taskStreamState !== 'idle') return; closeTaskEventSource(); const generation = taskStreamGeneration; activeTaskEventWorkspaceId = workspaceId; activeTaskEventConversationId = conversationId; set({ taskStreamState: 'connecting' }); void openImageWorkspaceTaskEvents(workspaceId, conversationId).then((source) => { if (generation !== taskStreamGeneration || get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) { source.close(); return; } activeTaskEventSource = source; source.addEventListener('design.assistant.delta', (event) => { const delta = parseAssistantDeltaEvent(event); if (!delta || delta.workspaceId !== activeTaskEventWorkspaceId || delta.conversationId !== activeTaskEventConversationId) return; set((state) => { if (state.activeWorkspaceId !== delta.workspaceId || state.activeConversationId !== delta.conversationId || (state.conversation?.turnRevision ?? 0) >= delta.turnRevision) { return state; } const pending = state.pendingTurn; if (!pending) { if (delta.chunkIndex !== 0) return state; return { pendingTurn: { workspaceId: delta.workspaceId, conversationId: delta.conversationId, clientTurnId: delta.clientTurnId, turnRevision: delta.turnRevision, userText: null, assistantText: delta.delta, lastChunkIndex: delta.chunkIndex, createdAt: new Date().toISOString(), }, }; } if (pending.workspaceId !== delta.workspaceId || pending.conversationId !== delta.conversationId || pending.clientTurnId !== delta.clientTurnId || pending.turnRevision !== delta.turnRevision || delta.chunkIndex !== pending.lastChunkIndex + 1) { return state; } return { pendingTurn: { ...pending, assistantText: `${pending.assistantText}${delta.delta}`, lastChunkIndex: delta.chunkIndex, }, }; }); }); source.addEventListener('design.conversation.snapshot', (event) => { const snapshot = parseConversationSnapshotEvent(event); if (!snapshot || snapshot.workspaceId !== activeTaskEventWorkspaceId || snapshot.conversationId !== activeTaskEventConversationId) return; set((state) => { if (state.activeWorkspaceId !== snapshot.workspaceId || state.activeConversationId !== snapshot.conversationId) return state; const tasksById = new Map(state.tasks.map((task) => [task.taskId, task])); for (const task of snapshot.generationTasks) { const key = taskRevisionKey(snapshot.workspaceId, task.taskId); const knownRevision = taskEventRevisions.get(key) ?? 0; const existing = tasksById.get(task.taskId); if (snapshot.workspaceViewRevision >= knownRevision && (!existing || existing.updatedAt <= task.updatedAt)) { tasksById.set(task.taskId, task); } taskEventRevisions.set( key, Math.max(knownRevision, snapshot.workspaceViewRevision), ); } const conversation = !state.conversation || snapshot.conversation.turnRevision >= state.conversation.turnRevision ? snapshot.conversation : state.conversation; const workspace = upsertConversation( state.workspace ? { ...state.workspace, viewRevision: Math.max( state.workspace.viewRevision, snapshot.workspaceViewRevision, ), } : null, conversation, ); const bootstrap = workspace?.workspaceId === snapshot.workspaceId ? upsertSummary(state.bootstrap, workspace) : state.bootstrap; const pendingTurn = state.pendingTurn?.workspaceId === snapshot.workspaceId && state.pendingTurn.conversationId === snapshot.conversationId && snapshot.conversation.turnRevision >= state.pendingTurn.turnRevision ? null : state.pendingTurn; return { tasks: sortTasks([...tasksById.values()]), workspace, conversation, bootstrap, pendingTurn, }; }); }); source.addEventListener('design.generation_task.updated', (event) => { const update = parseTaskUpdatedEvent(event); if (!update || update.workspaceId !== activeTaskEventWorkspaceId) return; set((state) => { if (state.activeWorkspaceId !== update.workspaceId) return state; const key = taskRevisionKey(update.workspaceId, update.generationTask.taskId); const knownRevision = taskEventRevisions.get(key) ?? 0; if (update.workspaceViewRevision <= knownRevision) return state; const existing = state.tasks.find( (candidate) => candidate.taskId === update.generationTask.taskId, ); if (existing && existing.updatedAt > update.generationTask.updatedAt) return state; taskEventRevisions.set(key, update.workspaceViewRevision); const tasks = sortTasks([ update.generationTask, ...state.tasks.filter((candidate) => candidate.taskId !== update.generationTask.taskId), ]); const workspace = state.workspace?.workspaceId === update.workspaceId ? { ...state.workspace, viewRevision: Math.max( state.workspace.viewRevision, update.workspaceViewRevision, ), } : state.workspace; const bootstrap = state.bootstrap ? { ...state.bootstrap, workspaces: state.bootstrap.workspaces.map((candidate) => ( candidate.workspaceId === update.workspaceId ? { ...candidate, viewRevision: Math.max( candidate.viewRevision, update.workspaceViewRevision, ), } : candidate )), } : null; return { tasks, workspace, bootstrap }; }); }); source.onopen = () => { if (activeTaskEventSource !== source) return; stopFallbackPolling(); set({ taskStreamState: 'connected' }); }; source.onerror = () => { if (activeTaskEventSource !== source) return; set({ taskStreamState: 'degraded' }); startFallbackPolling(); }; }).catch(() => { if (generation !== taskStreamGeneration || get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return; set({ taskStreamState: 'degraded' }); startFallbackPolling(); }); }; const handleRequestError = (error: unknown): string => { const message = messageOf(error); if (authenticationRequired(error)) { closeTaskEventSource(); useAuthStore.getState().invalidateSession(); set({ status: 'auth-required', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: message, }); } return message; }; const refreshWorkspaceTasks = async ( workspaceId: string, ): Promise => { const requestedViewRevision = get().workspace?.workspaceId === workspaceId ? get().workspace?.viewRevision ?? 0 : 0; const tasks = await fetchImageWorkspaceTasks(workspaceId); const currentWorkspace = get().workspace; if (get().activeWorkspaceId === workspaceId && currentWorkspace?.workspaceId === workspaceId && currentWorkspace.viewRevision === requestedViewRevision) { set((state) => ({ tasks: mergeTasks(state.tasks, tasks) })); } return tasks; }; const applyWorkspace = (workspace: DesignWorkspace): DesignWorkspace => { set((state) => { return { status: 'ready', bootstrap: upsertSummary(state.bootstrap, workspace), activeWorkspaceId: workspace.workspaceId, workspace, error: null, }; }); return workspace; }; const applyConversation = (conversation: DesignConversation): DesignConversation => { let applied = conversation; set((state) => { if (state.conversation?.conversationId === conversation.conversationId && (state.conversation.turnRevision > conversation.turnRevision || (state.conversation.turnRevision === conversation.turnRevision && state.conversation.updatedAt > conversation.updatedAt))) { applied = state.conversation; return state; } const pendingTurn = state.pendingTurn?.workspaceId === conversation.workspaceId && state.pendingTurn.conversationId === conversation.conversationId && conversation.turnRevision >= state.pendingTurn.turnRevision ? null : state.pendingTurn; return { workspace: upsertConversation(state.workspace, conversation), activeConversationId: conversation.conversationId, conversation, pendingTurn, error: null, }; }); return applied; }; const loadWorkspace = async (workspaceId: string): Promise => { const loadGeneration = ++workspaceLoadGeneration; conversationSelectionGeneration += 1; const workspace = await fetchImageWorkspaceProject(workspaceId); if (loadGeneration !== workspaceLoadGeneration || get().activeWorkspaceId !== workspaceId) return; const currentConversationId = get().activeConversationId; const conversationId = workspace.conversations.some( (item) => item.conversationId === currentConversationId, ) ? currentConversationId : workspace.conversations[0]?.conversationId ?? null; const tasks = await fetchImageWorkspaceTasks(workspaceId); if (loadGeneration !== workspaceLoadGeneration || get().activeWorkspaceId !== workspaceId) return; set((state) => ({ status: 'ready', bootstrap: upsertSummary(state.bootstrap, workspace), activeWorkspaceId: workspaceId, activeConversationId: conversationId, workspace, conversation: null, tasks: sortTasks(tasks), error: null, })); if (!conversationId) return; const selectionGeneration = ++conversationSelectionGeneration; const conversation = await fetchImageWorkspaceConversation(workspaceId, conversationId); if (loadGeneration !== workspaceLoadGeneration || selectionGeneration !== conversationSelectionGeneration || get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return; applyConversation(conversation); startTaskStream(workspaceId, conversationId); }; const recoverRevisionConflict = async (error: unknown): Promise => { if (error instanceof ImageWorkspaceApiError && (error.code === 'conversation_revision_conflict' || error.code === 'workspace_revision_conflict')) { await get().refreshConversation().catch(() => null); throw new ImageWorkspaceApiError( 409, error.code, '设计会话已更新,内容已刷新,请重新提交', ); } throw error; }; return { status: 'idle', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: null, load: () => { if (inFlightLoad) return inFlightLoad; set({ status: 'loading', error: null }); inFlightLoad = (async () => { try { const bootstrap = await fetchImageWorkspace(); const currentId = get().activeWorkspaceId; const activeWorkspaceId = bootstrap.workspaces.some( (workspace) => workspace.workspaceId === currentId, ) ? currentId : bootstrap.workspaces[0]?.workspaceId ?? null; closeTaskEventSource(); set({ status: 'ready', bootstrap, activeWorkspaceId, activeConversationId: null, workspace: null, conversation: null, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: null, }); if (activeWorkspaceId) await loadWorkspace(activeWorkspaceId); return bootstrap; } catch (error) { const message = handleRequestError(error); if (!authenticationRequired(error)) { closeTaskEventSource(); set({ status: unavailable(error) ? 'unavailable' : 'error', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: message, }); } return null; } finally { inFlightLoad = null; } })(); return inFlightLoad; }, createProject: async (title) => { try { const workspace = applyWorkspace(await createImageWorkspaceProject(title)); const conversationId = workspace.conversations[0]?.conversationId ?? null; set({ activeConversationId: conversationId, conversation: null, tasks: [], pendingTurn: null, }); if (conversationId) { const selectionGeneration = ++conversationSelectionGeneration; const conversation = await fetchImageWorkspaceConversation( workspace.workspaceId, conversationId, ); if (selectionGeneration === conversationSelectionGeneration && get().activeWorkspaceId === workspace.workspaceId && get().activeConversationId === conversationId) { applyConversation(conversation); startTaskStream(workspace.workspaceId, conversationId); } } return workspace; } catch (error) { set({ error: handleRequestError(error) }); throw error; } }, createConversation: async (title = '新会话') => { if (get().creatingConversation) { throw new Error('正在创建新会话,请稍候'); } const workspaceId = get().activeWorkspaceId; if (!workspaceId) throw new Error('请先选择设计项目'); set({ creatingConversation: true }); try { const conversation = await createImageWorkspaceConversation(workspaceId, title); if (get().activeWorkspaceId !== workspaceId) return conversation; conversationSelectionGeneration += 1; stopTaskStream(); set({ activeConversationId: conversation.conversationId, conversation: null, pendingTurn: null, taskStreamState: 'idle', error: null, }); applyConversation(conversation); startTaskStream(workspaceId, conversation.conversationId); void get().refreshWorkspace().catch(() => null); return conversation; } catch (error) { set({ error: handleRequestError(error) }); throw error; } finally { set({ creatingConversation: false }); } }, renameProject: async (workspaceId, title) => { try { return applyWorkspace(await renameImageWorkspaceProject(workspaceId, title)); } catch (error) { set({ error: handleRequestError(error) }); throw error; } }, selectProject: async (workspaceId) => { if (!get().bootstrap?.workspaces.some((item) => item.workspaceId === workspaceId)) return; stopTaskStream(); set({ activeWorkspaceId: workspaceId, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: null, }); const loadPromise = loadWorkspace(workspaceId); const loadGeneration = workspaceLoadGeneration; try { await loadPromise; } catch (error) { if (loadGeneration === workspaceLoadGeneration && get().activeWorkspaceId === workspaceId) { set({ error: handleRequestError(error) }); } } }, selectConversation: async (conversationId) => { const state = get(); const workspaceId = state.activeWorkspaceId; if (!workspaceId || !state.workspace?.conversations.some( (item) => item.conversationId === conversationId, )) return; const selectionGeneration = ++conversationSelectionGeneration; stopTaskStream(); set({ activeConversationId: conversationId, conversation: null, pendingTurn: null, taskStreamState: 'idle', error: null, }); try { const conversation = await fetchImageWorkspaceConversation(workspaceId, conversationId); if (selectionGeneration !== conversationSelectionGeneration || get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return; applyConversation(conversation); startTaskStream(workspaceId, conversationId); } catch (error) { if (selectionGeneration === conversationSelectionGeneration && get().activeWorkspaceId === workspaceId && get().activeConversationId === conversationId) { set({ error: handleRequestError(error) }); } } }, refreshWorkspace: async () => { const workspaceId = get().activeWorkspaceId; if (!workspaceId) return null; try { const refreshed = await fetchImageWorkspaceProject(workspaceId); if (get().activeWorkspaceId !== workspaceId) return null; const current = get().workspace; if (current?.workspaceId === workspaceId && current.viewRevision > refreshed.viewRevision) { return current; } return applyWorkspace(refreshed); } catch (error) { set({ error: handleRequestError(error) }); throw error; } }, refreshConversation: async () => { const workspaceId = get().activeWorkspaceId; const conversationId = get().activeConversationId; if (!workspaceId || !conversationId) return null; try { const refreshed = await fetchImageWorkspaceConversation(workspaceId, conversationId); if (get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return null; const current = get().conversation; if (current?.conversationId === conversationId && current.turnRevision > refreshed.turnRevision) { return current; } return applyConversation(refreshed); } catch (error) { set({ error: handleRequestError(error) }); throw error; } }, refreshTasks: async () => { const workspaceId = get().activeWorkspaceId; if (!workspaceId) { set({ tasks: [] }); return []; } try { return await refreshWorkspaceTasks(workspaceId); } catch (error) { set({ error: handleRequestError(error) }); throw error; } }, connectTaskStream: () => { const workspaceId = get().activeWorkspaceId; const conversationId = get().activeConversationId; if (workspaceId && conversationId) startTaskStream(workspaceId, conversationId); }, disconnectTaskStream: stopTaskStream, sendMessage: async (message, attachmentAssetIds = []) => { const workspace = get().workspace; const conversation = get().conversation; const userText = message.trim(); const clientTurnId = createImageWorkspaceTurnId(); if (!workspace || !conversation) throw new Error('请先选择设计会话'); try { set({ pendingTurn: createPendingTurn(conversation, clientTurnId, userText), error: null, }); const updated = attachmentAssetIds.length > 0 ? await sendImageWorkspaceMessage( workspace.workspaceId, conversation.conversationId, conversation.turnRevision, userText, clientTurnId, attachmentAssetIds, ) : await sendImageWorkspaceMessage( workspace.workspaceId, conversation.conversationId, conversation.turnRevision, userText, clientTurnId, ); if (get().activeWorkspaceId === workspace.workspaceId && get().activeConversationId === conversation.conversationId) { applyConversation(updated); await get().refreshTasks().catch(() => []); } return updated; } catch (error) { if (get().activeWorkspaceId !== workspace.workspaceId || get().activeConversationId !== conversation.conversationId) throw error; const errorMessage = handleRequestError(error); set((state) => ({ error: errorMessage, pendingTurn: state.pendingTurn?.clientTurnId === clientTurnId ? null : state.pendingTurn, })); return await recoverRevisionConflict(error); } }, confirmGeneration: async (quoteId) => { const workspace = get().workspace; const conversation = get().conversation; if (!workspace || !conversation) throw new Error('请先选择设计会话'); const clientTurnId = createImageWorkspaceTurnId(); const requestWorkspaceLoadGeneration = workspaceLoadGeneration; const requestConversationSelectionGeneration = conversationSelectionGeneration; const isActiveConfirmationWorkspace = (): boolean => ( get().activeWorkspaceId === workspace.workspaceId ); const isCurrentConfirmationConversation = (): boolean => ( isActiveConfirmationWorkspace() && requestWorkspaceLoadGeneration === workspaceLoadGeneration && requestConversationSelectionGeneration === conversationSelectionGeneration && get().activeConversationId === conversation.conversationId ); try { set({ pendingTurn: createPendingTurn(conversation, clientTurnId, '确认生成'), error: null, }); const updated = await confirmImageWorkspaceGeneration( workspace.workspaceId, conversation.conversationId, conversation.turnRevision, quoteId, clientTurnId, ); if (isCurrentConfirmationConversation()) applyConversation(updated); if (!isActiveConfirmationWorkspace()) return updated; let confirmedTaskVisible = get().tasks.some((task) => task.quoteId === quoteId); for ( let attempt = 0; !confirmedTaskVisible && attempt < CONFIRMED_TASK_RECONCILE_ATTEMPTS; attempt += 1 ) { if (!isActiveConfirmationWorkspace()) return updated; const refreshed = await refreshWorkspaceTasks(workspace.workspaceId); if (!isActiveConfirmationWorkspace()) return updated; confirmedTaskVisible = refreshed.some((task) => task.quoteId === quoteId) || get().tasks.some((task) => task.quoteId === quoteId); if (!confirmedTaskVisible && attempt + 1 < CONFIRMED_TASK_RECONCILE_ATTEMPTS) { await new Promise((resolve) => { setTimeout(resolve, CONFIRMED_TASK_RECONCILE_INTERVAL_MS); }); } } if (!confirmedTaskVisible) { throw new ImageWorkspaceApiError( 502, 'generation_task_not_visible', '生成已确认,但任务列表同步超时。请刷新项目查看,不要重复确认。', ); } return updated; } catch (error) { if (!isActiveConfirmationWorkspace()) throw error; const taskVisibilityAlreadyReconciled = error instanceof ImageWorkspaceApiError && error.code === 'generation_task_not_visible'; let confirmedTaskVisible = get().tasks.some((task) => task.quoteId === quoteId); if (!taskVisibilityAlreadyReconciled && !confirmedTaskVisible) { const refreshed = await refreshWorkspaceTasks(workspace.workspaceId).catch(() => []); if (!isActiveConfirmationWorkspace()) throw error; confirmedTaskVisible = refreshed.some((task) => task.quoteId === quoteId) || get().tasks.some((task) => task.quoteId === quoteId); } if (confirmedTaskVisible) { if (!isCurrentConfirmationConversation()) return conversation; const refreshedConversation = await fetchImageWorkspaceConversation( workspace.workspaceId, conversation.conversationId, ).catch(() => null); if (!isCurrentConfirmationConversation()) return conversation; const recoveredConversation = refreshedConversation ? applyConversation(refreshedConversation) : get().conversation; if (recoveredConversation?.conversationId === conversation.conversationId) { set((state) => ({ error: null, pendingTurn: state.pendingTurn?.clientTurnId === clientTurnId ? null : state.pendingTurn, })); return recoveredConversation; } } if (!isCurrentConfirmationConversation()) throw error; const errorMessage = handleRequestError(error); set((state) => ({ error: errorMessage, pendingTurn: state.pendingTurn?.clientTurnId === clientTurnId ? null : state.pendingTurn, })); return await recoverRevisionConflict(error); } }, reset: () => { inFlightLoad = null; workspaceLoadGeneration += 1; conversationSelectionGeneration += 1; closeTaskEventSource(); set({ status: 'idle', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: null, }); }, }; });