import { create } from 'zustand'; import { confirmImageWorkspaceGeneration, createImageWorkspaceConversation, createImageWorkspaceTurnId, createImageWorkspaceProject, deleteImageWorkspaceProject, fetchImageWorkspace, fetchImageWorkspaceConversation, fetchImageWorkspaceProject, fetchImageWorkspaceTasks, ImageWorkspaceApiError, openImageWorkspaceTaskEvents, renameImageWorkspaceProject, sendImageWorkspaceMessage, updateImageWorkspaceGenerationQuote, } from '@/lib/image-workspace'; import { useAuthStore } from '@/stores/auth'; import { setDesktopBackgroundLease } from '@/lib/host-api'; import { IMAGE_WORKSPACE_UNAVAILABLE_CODE, type DesignAssistantDeltaEvent, type DesignConversation, type DesignConversationSnapshotEvent, type DesignDeleteWorkspaceResult, type DesignGenerationTask, type DesignGenerationParameters, type DesignGenerationQuote, type DesignGenerationTaskUpdatedEvent, type DesignMessage, 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; deletingWorkspaceId: string | null; tasks: DesignGenerationTask[]; pendingTurn: PendingDesignTurn | null; loadingOlderMessages: boolean; olderMessagesError: string | null; taskStreamState: ImageWorkspaceTaskStreamState; error: string | null; load: () => Promise; createProject: (title: string) => Promise; createConversation: (title?: string) => Promise; renameProject: (workspaceId: string, title: string) => Promise; deleteProject: (workspaceId: string) => Promise; selectProject: (workspaceId: string) => Promise; selectConversation: (conversationId: string) => Promise; refreshWorkspace: () => Promise; refreshConversation: () => Promise; loadOlderMessages: () => Promise; refreshTasks: () => Promise; connectTaskStream: () => void; markTaskStreamNeedsReconciliation: () => void; disconnectTaskStream: () => void; sendMessage: (message: string, attachmentAssetIds?: string[]) => Promise; updateGenerationQuote: ( quoteId: string, finalPrompt: string, generationParameters: DesignGenerationParameters, ) => Promise; confirmGeneration: ( quoteId: string, finalPrompt: string, generationParameters: DesignGenerationParameters, ) => 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; const pendingTaskEventConnections = new Map(); let taskFallbackTimer: ReturnType | null = null; const taskEventRevisions = new Map(); let workspaceLoadGeneration = 0; let conversationSelectionGeneration = 0; let projectDeletionGeneration = 0; let generationQuoteUpdateSequence = 0; let reconcileTaskStateOnNextConnect = false; function updateBackgroundLease(input: { id: string; kind: string; active: boolean; }): void { try { void Promise.resolve(setDesktopBackgroundLease(input)).catch(() => undefined); } catch { // Renderer-only tests and embedded previews may not expose lifecycle IPC. } } function taskRevisionKey(workspaceId: string, taskId: string): string { return `${workspaceId}:${taskId}`; } function taskStreamKey(workspaceId: string, conversationId: string): string { return `${workspaceId}:${conversationId}`; } function stopFallbackPolling(): void { if (taskFallbackTimer !== null) clearInterval(taskFallbackTimer); taskFallbackTimer = null; } function closeTaskEventSource(): void { 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 mostRecentlyUpdatedWorkspace( workspaces: DesignWorkspaceSummary[], ): DesignWorkspaceSummary | null { return workspaces.reduce((latest, workspace) => { if (!latest || workspace.updatedAt > latest.updatedAt) return workspace; return latest; }, null); } 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(), }; } function mergePendingUserMessage( conversation: DesignConversation, pendingTurn: PendingDesignTurn | null, ): DesignConversation { if (!pendingTurn || pendingTurn.userText === null || pendingTurn.workspaceId !== conversation.workspaceId || pendingTurn.conversationId !== conversation.conversationId || conversation.turnRevision < pendingTurn.turnRevision) { return conversation; } const alreadyPersisted = conversation.messages.some((message) => ( message.role === 'user' && message.turnRevision === pendingTurn.turnRevision && message.text === pendingTurn.userText )); if (alreadyPersisted) return conversation; const userMessage: DesignMessage = { id: `${pendingTurn.clientTurnId}:user`, role: 'user', kind: 'user', text: pendingTurn.userText, quickReplies: [], generationQuote: null, turnRevision: pendingTurn.turnRevision, createdAt: pendingTurn.createdAt, }; const insertionIndex = conversation.messages.findIndex( (message) => message.turnRevision >= pendingTurn.turnRevision, ); const messages = [...conversation.messages]; messages.splice(insertionIndex < 0 ? messages.length : insertionIndex, 0, userMessage); return { ...conversation, messages }; } function mergeConversationMessagePages( current: DesignConversation | null, incoming: DesignConversation, source: 'latest' | 'older', ): DesignConversation { if (!current || current.conversationId !== incoming.conversationId || current.workspaceId !== incoming.workspaceId) { return incoming; } const messagesById = new Map(); const messageSources = source === 'older' ? [incoming.messages, current.messages] : [current.messages, incoming.messages]; for (const messages of messageSources) { for (const message of messages) messagesById.set(message.id, message); } const messages = [...messagesById.values()].sort((left, right) => ( left.turnRevision - right.turnRevision || (left.role === right.role ? left.id.localeCompare(right.id) : left.role === 'user' ? -1 : 1) || left.createdAt.localeCompare(right.createdAt) )); return { ...incoming, messages, messagePage: source === 'older' ? incoming.messagePage : current.messagePage ?? incoming.messagePage, }; } function replaceGenerationQuote( conversation: DesignConversation, previousQuoteId: string, quote: DesignGenerationQuote, ): DesignConversation { let changed = false; const messages = conversation.messages.map((message) => { if (message.generationQuote?.quoteId !== previousQuoteId) return message; changed = true; return { ...message, generationQuote: quote }; }); return changed ? { ...conversation, messages } : conversation; } export const useImageWorkspaceStore = create((set, get) => { const pendingAssistantDeltas: DesignAssistantDeltaEvent[] = []; let assistantDeltaFlushTimer: ReturnType | null = null; const applyAssistantDelta = ( state: ImageWorkspaceState, delta: DesignAssistantDeltaEvent, ): ImageWorkspaceState => { 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 { ...state, 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 { ...state, pendingTurn: { ...pending, assistantText: `${pending.assistantText}${delta.delta}`, lastChunkIndex: delta.chunkIndex, }, }; }; const flushAssistantDeltas = () => { assistantDeltaFlushTimer = null; if (pendingAssistantDeltas.length === 0) return; const queued = pendingAssistantDeltas.splice(0, pendingAssistantDeltas.length); set((state) => queued.reduce(applyAssistantDelta, state)); }; const queueAssistantDelta = (delta: DesignAssistantDeltaEvent) => { pendingAssistantDeltas.push(delta); if (assistantDeltaFlushTimer === null) { assistantDeltaFlushTimer = setTimeout(flushAssistantDeltas, 50); } }; const stopTaskStream = () => { pendingAssistantDeltas.length = 0; if (assistantDeltaFlushTimer !== null) clearTimeout(assistantDeltaFlushTimer); assistantDeltaFlushTimer = null; 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(); activeTaskEventWorkspaceId = workspaceId; activeTaskEventConversationId = conversationId; set({ taskStreamState: 'connecting' }); const connectionKey = taskStreamKey(workspaceId, conversationId); if (pendingTaskEventConnections.has(connectionKey)) return; const connectionAttempt = Symbol(connectionKey); pendingTaskEventConnections.set(connectionKey, connectionAttempt); void openImageWorkspaceTaskEvents(workspaceId, conversationId).then((source) => { if (pendingTaskEventConnections.get(connectionKey) !== connectionAttempt) { source.close(); return; } pendingTaskEventConnections.delete(connectionKey); if (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; queueAssistantDelta(delta); }); 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 resolvedConversation = mergeConversationMessagePages( state.conversation, mergePendingUserMessage(conversation, state.pendingTurn), 'latest', ); const workspace = upsertConversation( state.workspace ? { ...state.workspace, viewRevision: Math.max( state.workspace.viewRevision, snapshot.workspaceViewRevision, ), } : null, resolvedConversation, ); const bootstrap = workspace?.workspaceId === snapshot.workspaceId ? upsertSummary(state.bootstrap, workspace) : state.bootstrap; const pendingTurn = state.pendingTurn?.workspaceId === snapshot.workspaceId && state.pendingTurn.conversationId === snapshot.conversationId && resolvedConversation.turnRevision >= state.pendingTurn.turnRevision ? null : state.pendingTurn; return { tasks: sortTasks([...tasksById.values()]), workspace, conversation: resolvedConversation, 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 (pendingTaskEventConnections.get(connectionKey) !== connectionAttempt) return; pendingTaskEventConnections.delete(connectionKey); if (get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return; set({ taskStreamState: 'degraded' }); startFallbackPolling(); }); }; const handleRequestError = (error: unknown): string => { const message = messageOf(error); if (authenticationRequired(error)) { projectDeletionGeneration += 1; stopTaskStream(); useAuthStore.getState().invalidateSession(); set({ status: 'auth-required', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, deletingWorkspaceId: null, 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, source: 'latest' | 'older' = 'latest', ): 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 resolvedConversation = mergeConversationMessagePages( state.conversation, mergePendingUserMessage(conversation, state.pendingTurn), source, ); applied = resolvedConversation; const pendingTurn = state.pendingTurn?.workspaceId === resolvedConversation.workspaceId && state.pendingTurn.conversationId === resolvedConversation.conversationId && resolvedConversation.turnRevision >= state.pendingTurn.turnRevision ? null : state.pendingTurn; return { workspace: upsertConversation(state.workspace, resolvedConversation), activeConversationId: resolvedConversation.conversationId, conversation: resolvedConversation, pendingTurn, loadingOlderMessages: false, olderMessagesError: null, 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; set((state) => ({ status: 'ready', bootstrap: upsertSummary(state.bootstrap, workspace), activeWorkspaceId: workspaceId, activeConversationId: conversationId, workspace, conversation: null, tasks: [], pendingTurn: null, loadingOlderMessages: false, olderMessagesError: null, error: null, })); void fetchImageWorkspaceTasks(workspaceId).then((tasks) => { if (loadGeneration !== workspaceLoadGeneration || get().activeWorkspaceId !== workspaceId) return; set({ tasks: sortTasks(tasks) }); }).catch(() => undefined); 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, deletingWorkspaceId: null, tasks: [], pendingTurn: null, loadingOlderMessages: false, olderMessagesError: 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; stopTaskStream(); 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)) { stopTaskStream(); 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, loadingOlderMessages: false, olderMessagesError: 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; } }, deleteProject: async (workspaceId) => { const state = get(); if (state.deletingWorkspaceId) { const error = new Error('正在删除设计项目,请稍候'); set({ error: error.message }); throw error; } const deletionGeneration = ++projectDeletionGeneration; const isCurrentDeletion = (): boolean => ( deletionGeneration === projectDeletionGeneration && get().deletingWorkspaceId === workspaceId ); set({ deletingWorkspaceId: workspaceId, error: null }); try { const result = await deleteImageWorkspaceProject(workspaceId); if (!isCurrentDeletion()) return result; const current = get(); const remainingWorkspaces = current.bootstrap?.workspaces.filter( (workspace) => workspace.workspaceId !== workspaceId, ) ?? []; const bootstrap = current.bootstrap ? { ...current.bootstrap, workspaces: remainingWorkspaces } : null; if (current.activeWorkspaceId !== workspaceId) { set({ bootstrap, error: null }); return result; } workspaceLoadGeneration += 1; conversationSelectionGeneration += 1; reconcileTaskStateOnNextConnect = false; stopTaskStream(); const nextWorkspaceId = mostRecentlyUpdatedWorkspace(remainingWorkspaces)?.workspaceId ?? null; set({ status: 'ready', bootstrap, activeWorkspaceId: nextWorkspaceId, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, tasks: [], pendingTurn: null, taskStreamState: 'idle', error: null, }); if (nextWorkspaceId) { try { await loadWorkspace(nextWorkspaceId); } catch (error) { if (get().activeWorkspaceId === nextWorkspaceId) { set({ error: handleRequestError(error) }); } } } return result; } catch (error) { if (isCurrentDeletion()) set({ error: handleRequestError(error) }); throw error; } finally { if (isCurrentDeletion()) set({ deletingWorkspaceId: null }); } }, 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, loadingOlderMessages: false, olderMessagesError: 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, loadingOlderMessages: false, olderMessagesError: 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; } }, loadOlderMessages: async () => { const workspaceId = get().activeWorkspaceId; const conversation = get().conversation; const before = conversation?.messagePage?.nextBefore; if (!workspaceId || !conversation || !conversation.messagePage?.hasOlder || !before) { return conversation ?? null; } const conversationId = conversation.conversationId; const selectionGeneration = conversationSelectionGeneration; set({ loadingOlderMessages: true, olderMessagesError: null }); try { const older = await fetchImageWorkspaceConversation(workspaceId, conversationId, before); if (selectionGeneration !== conversationSelectionGeneration || get().activeWorkspaceId !== workspaceId || get().activeConversationId !== conversationId) return null; return applyConversation(older, 'older'); } catch (error) { if (selectionGeneration === conversationSelectionGeneration && get().activeWorkspaceId === workspaceId && get().activeConversationId === conversationId) { set({ loadingOlderMessages: false, olderMessagesError: handleRequestError(error), }); } return null; } }, 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; const shouldReconcile = reconcileTaskStateOnNextConnect; reconcileTaskStateOnNextConnect = false; if (shouldReconcile && get().taskStreamState === 'degraded') { closeTaskEventSource(); } if (workspaceId && conversationId) startTaskStream(workspaceId, conversationId); if (!workspaceId || !conversationId || !shouldReconcile) return; void Promise.all([ get().refreshConversation(), get().refreshTasks(), ]).catch(() => undefined); }, markTaskStreamNeedsReconciliation: () => { reconcileTaskStateOnNextConnect = true; }, 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('请先选择设计会话'); const leaseId = `canvas:${workspace.workspaceId}:${clientTurnId}`; updateBackgroundLease({ id: leaseId, kind: 'canvas-generation', active: true }); 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); } finally { updateBackgroundLease({ id: leaseId, kind: 'canvas-generation', active: false }); } }, updateGenerationQuote: async (quoteId, finalPrompt, generationParameters) => { const workspace = get().workspace; const conversation = get().conversation; if (!workspace || !conversation) throw new Error('请先选择设计会话'); const updateSequence = ++generationQuoteUpdateSequence; const requestedWorkspaceId = workspace.workspaceId; const requestedConversationId = conversation.conversationId; try { const updatedQuote = await updateImageWorkspaceGenerationQuote( requestedWorkspaceId, quoteId, finalPrompt, generationParameters, ); if (updateSequence !== generationQuoteUpdateSequence) return updatedQuote; if (get().activeWorkspaceId !== requestedWorkspaceId || get().activeConversationId !== requestedConversationId) { return updatedQuote; } set((state) => { if (!state.conversation || state.conversation.conversationId !== requestedConversationId) return state; const conversationWithQuote = replaceGenerationQuote( state.conversation, quoteId, updatedQuote, ); if (conversationWithQuote === state.conversation) return state; const workspaceWithQuote = upsertConversation( state.workspace, conversationWithQuote, ); return { conversation: conversationWithQuote, workspace: workspaceWithQuote, bootstrap: workspaceWithQuote ? upsertSummary(state.bootstrap, workspaceWithQuote) : state.bootstrap, error: null, }; }); return updatedQuote; } catch (error) { if (updateSequence === generationQuoteUpdateSequence && get().activeWorkspaceId === requestedWorkspaceId && get().activeConversationId === requestedConversationId) { set({ error: handleRequestError(error) }); } throw error; } }, confirmGeneration: async (quoteId, finalPrompt, generationParameters) => { const workspace = get().workspace; const conversation = get().conversation; if (!workspace || !conversation) throw new Error('请先选择设计会话'); const clientTurnId = createImageWorkspaceTurnId(); const leaseId = `canvas:${workspace.workspaceId}:${clientTurnId}`; updateBackgroundLease({ id: leaseId, kind: 'canvas-generation', active: true }); 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 = finalPrompt === undefined || generationParameters === undefined ? await confirmImageWorkspaceGeneration( workspace.workspaceId, conversation.conversationId, conversation.turnRevision, quoteId, clientTurnId, ) : await confirmImageWorkspaceGeneration( workspace.workspaceId, conversation.conversationId, conversation.turnRevision, quoteId, clientTurnId, finalPrompt, generationParameters, ); 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); } finally { updateBackgroundLease({ id: leaseId, kind: 'canvas-generation', active: false }); } }, reset: () => { inFlightLoad = null; workspaceLoadGeneration += 1; conversationSelectionGeneration += 1; projectDeletionGeneration += 1; generationQuoteUpdateSequence += 1; reconcileTaskStateOnNextConnect = false; stopTaskStream(); pendingTaskEventConnections.clear(); set({ status: 'idle', bootstrap: null, activeWorkspaceId: null, activeConversationId: null, workspace: null, conversation: null, creatingConversation: false, deletingWorkspaceId: null, tasks: [], pendingTurn: null, loadingOlderMessages: false, olderMessagesError: null, taskStreamState: 'idle', error: null, }); }, }; });