import { create } from 'zustand'; import { createImageWorkspaceOperationId, createImageWorkspaceProject, deleteImageWorkspaceProject, fetchImageWorkspace, fetchImageWorkspaceProject, ImageWorkspaceApiError, openImageWorkspaceEvents, renameImageWorkspaceProject, submitImageWorkspaceCommand, } from '@/lib/image-workspace'; import { setDesktopBackgroundLease } from '@/lib/host-api'; import { useAuthStore } from '@/stores/auth'; import { IMAGE_WORKSPACE_UNAVAILABLE_CODE, type DesignCommandInput, type DesignCompilationIssue, type DesignDeleteWorkspaceResult, type DesignFieldChange, type DesignGenerationTask, type DesignUserFieldOperation, type DesignWorkspace, type DesignWorkspaceBootstrap, type DesignWorkspaceEvent, type DesignWorkspaceSummary, } from '../../shared/image-workspace'; export type ImageWorkspaceLoadStatus = | 'idle' | 'loading' | 'ready' | 'unavailable' | 'error' | 'auth-required'; export type ImageWorkspaceEventState = 'idle' | 'connecting' | 'connected' | 'degraded'; export type PendingDesignOperation = { id: string; label: string; command: DesignCommandInput; status: 'submitting' | 'unknown'; error: string | null; clearDraftPaths: string[]; clearChatDraft: boolean; }; type ExecuteCommandOptions = { label: string; clearDraftPaths?: string[]; clearChatDraft?: boolean; }; type ImageWorkspaceState = { status: ImageWorkspaceLoadStatus; bootstrap: DesignWorkspaceBootstrap | null; activeWorkspaceId: string | null; workspace: DesignWorkspace | null; deletingWorkspaceId: string | null; eventState: ImageWorkspaceEventState; lastEventId: string | null; pendingOperations: Record; fieldDrafts: Record; chatDraft: string; assistantStreams: Record; quoteBlockers: DesignCompilationIssue[]; error: string | null; load: () => Promise; createProject: (title: string) => Promise; renameProject: (workspaceId: string, title: string) => Promise; deleteProject: (workspaceId: string) => Promise; selectProject: (workspaceId: string) => Promise; refreshWorkspace: () => Promise; connectEvents: () => void; disconnectEvents: () => void; setFieldDraft: (path: string, value: unknown) => void; discardFieldDraft: (path: string) => void; setChatDraft: (value: string) => void; applyFieldOperations: ( operations: DesignUserFieldOperation[], clearDraftPaths?: string[], ) => Promise; sendChat: () => Promise; resolveDecisionPrompt: ( promptId: string, action: 'select' | 'reject', optionId?: string, ) => Promise; requestQuote: () => Promise; confirmGeneration: (quoteId: string) => Promise; retryOperation: (clientOperationId: string) => Promise; reset: () => void; }; let inFlightLoad: Promise | null = null; let activeEventSource: EventSource | null = null; let activeEventWorkspaceId: string | null = null; let selectionGeneration = 0; function updateBackgroundLease(tasks: DesignGenerationTask[]): void { const active = tasks.some((task) => task.status === 'queued' || task.status === 'running'); try { void Promise.resolve(setDesktopBackgroundLease({ id: 'design-generation', kind: 'design-generation', active, })).catch(() => undefined); } catch { // Renderer-only previews may not expose lifecycle IPC. } } function closeEventSource(): void { if (activeEventSource) { activeEventSource.onopen = null; activeEventSource.onerror = null; activeEventSource.close(); } activeEventSource = null; activeEventWorkspaceId = null; } function messageForError(error: unknown): string { if (error instanceof Error && error.message.trim()) return error.message; return 'AI 设计暂时无法同步,请稍后重试'; } function isAuthError(error: unknown): boolean { return error instanceof ImageWorkspaceApiError && (error.status === 401 || error.status === 403); } function isUnavailableError(error: unknown): boolean { return error instanceof ImageWorkspaceApiError && (error.status === 501 || error.code === IMAGE_WORKSPACE_UNAVAILABLE_CODE); } function isDirectionConflict(error: unknown): boolean { return error instanceof ImageWorkspaceApiError && (error.status === 409 || error.code === 'DESIGN_DIRECTION_REVISION_CONFLICT' || error.code === 'DIRECTION_REVISION_CONFLICT'); } function parseWorkspaceEvent(event: Event): DesignWorkspaceEvent | null { const data = (event as MessageEvent).data; if (typeof data !== 'string') return null; try { const parsed = JSON.parse(data) as Partial; if (typeof parsed.id !== 'string' || typeof parsed.type !== 'string') return null; if (![ 'design.session.snapshot', 'design.assistant.delta', 'design.direction.updated', 'design.quote.blocked', 'design.workspace.updated', ].includes(parsed.type)) return null; return parsed as DesignWorkspaceEvent; } catch { return null; } } function mergeTask( tasks: DesignGenerationTask[], incoming: DesignGenerationTask, ): DesignGenerationTask[] { const existing = tasks.find((task) => task.taskId === incoming.taskId); if (existing && existing.taskRevision > incoming.taskRevision) return tasks; return [incoming, ...tasks.filter((task) => task.taskId !== incoming.taskId)] .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt)); } function mergeSummary( summaries: DesignWorkspaceSummary[], summary: DesignWorkspaceSummary, ): DesignWorkspaceSummary[] { return [summary, ...summaries.filter((item) => item.workspaceId !== summary.workspaceId)] .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)); } function withWorkspace( state: ImageWorkspaceState, workspace: DesignWorkspace, ): Partial { updateBackgroundLease(workspace.tasks); return { status: 'ready', activeWorkspaceId: workspace.workspace.workspaceId, workspace, bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: mergeSummary(state.bootstrap.workspaces, workspace.workspace), } : state.bootstrap, error: null, }; } function applyEventToWorkspace( workspace: DesignWorkspace, event: DesignWorkspaceEvent, ): DesignWorkspace { if (event.type === 'design.session.snapshot' || event.type === 'design.direction.updated') { return { ...workspace, workspace: { ...workspace.workspace, directionRevision: event.form.directionRevision, specificationRevision: event.form.specificationRevision, workspaceViewRevision: event.form.workspaceViewRevision, }, form: event.form, }; } if (event.type === 'design.quote.blocked') { return { ...workspace, workspace: { ...workspace.workspace, directionRevision: event.form.directionRevision, specificationRevision: event.form.specificationRevision, workspaceViewRevision: event.form.workspaceViewRevision, }, form: event.form, }; } if (event.type === 'design.workspace.updated' && event.changedTask) { return { ...workspace, workspace: { ...workspace.workspace, workspaceViewRevision: Math.max( workspace.workspace.workspaceViewRevision, event.workspaceViewRevision, ), }, tasks: mergeTask(workspace.tasks, event.changedTask), }; } return workspace; } export const useImageWorkspaceStore = create((set, get) => { const refreshWorkspaceById = async (workspaceId: string): Promise => { const generation = ++selectionGeneration; const workspace = await fetchImageWorkspaceProject(workspaceId); if (generation !== selectionGeneration || get().activeWorkspaceId !== workspaceId) { return workspace; } set((state) => withWorkspace(state, workspace)); return workspace; }; const executeCommand = async ( command: DesignCommandInput, options: ExecuteCommandOptions, ): Promise => { const operation: PendingDesignOperation = { id: command.clientOperationId, label: options.label, command, status: 'submitting', error: null, clearDraftPaths: options.clearDraftPaths ?? [], clearChatDraft: options.clearChatDraft ?? false, }; set((state) => ({ pendingOperations: { ...state.pendingOperations, [operation.id]: operation }, error: null, quoteBlockers: command.kind === 'request_quote' ? [] : state.quoteBlockers, })); try { const result = await submitImageWorkspaceCommand(command); set((state) => { const pendingOperations = { ...state.pendingOperations }; delete pendingOperations[operation.id]; const fieldDrafts = { ...state.fieldDrafts }; for (const path of operation.clearDraftPaths) delete fieldDrafts[path]; const assistantStreams = { ...state.assistantStreams }; delete assistantStreams[operation.id]; return { ...withWorkspace(state, result.workspace), pendingOperations, fieldDrafts, chatDraft: operation.clearChatDraft ? '' : state.chatDraft, assistantStreams, }; }); return result.workspace; } catch (error) { const message = messageForError(error); if (isDirectionConflict(error)) { set((state) => { const pendingOperations = { ...state.pendingOperations }; delete pendingOperations[operation.id]; return { pendingOperations, error: '设计已在其他位置更新,已刷新最新内容,请再次提交' }; }); await get().refreshWorkspace().catch(() => null); } else if (isAuthError(error)) { set((state) => { const pendingOperations = { ...state.pendingOperations }; delete pendingOperations[operation.id]; return { pendingOperations, status: 'auth-required', error: message }; }); } else { set((state) => ({ pendingOperations: { ...state.pendingOperations, [operation.id]: { ...operation, status: 'unknown', error: message }, }, error: '操作结果尚未确认,可使用同一操作标识安全重试', })); } throw error; } }; return { status: 'idle', bootstrap: null, activeWorkspaceId: null, workspace: null, deletingWorkspaceId: null, eventState: 'idle', lastEventId: null, pendingOperations: {}, fieldDrafts: {}, chatDraft: '', assistantStreams: {}, quoteBlockers: [], error: null, load: async () => { if (inFlightLoad) return inFlightLoad; inFlightLoad = (async () => { if (!useAuthStore.getState().isAuthenticated()) { set({ status: 'auth-required', error: null }); return null; } set({ status: 'loading', error: null }); try { const bootstrap = await fetchImageWorkspace(); const currentId = get().activeWorkspaceId; const selectedId = currentId && bootstrap.workspaces.some( (item) => item.workspaceId === currentId, ) ? currentId : bootstrap.workspaces[0]?.workspaceId ?? null; set({ status: 'ready', bootstrap, activeWorkspaceId: selectedId, workspace: selectedId === currentId ? get().workspace : null, error: null, }); if (selectedId) { await refreshWorkspaceById(selectedId); get().connectEvents(); } else { closeEventSource(); } return bootstrap; } catch (error) { set({ status: isAuthError(error) ? 'auth-required' : isUnavailableError(error) ? 'unavailable' : 'error', error: messageForError(error), }); return null; } finally { inFlightLoad = null; } })(); return inFlightLoad; }, createProject: async (title) => { const workspace = await createImageWorkspaceProject(title); selectionGeneration += 1; closeEventSource(); set((state) => ({ ...withWorkspace(state, workspace), fieldDrafts: {}, chatDraft: '', quoteBlockers: [], lastEventId: null, })); get().connectEvents(); return workspace; }, renameProject: async (workspaceId, title) => { const workspace = await renameImageWorkspaceProject(workspaceId, title); set((state) => { if (state.activeWorkspaceId === workspaceId) { return withWorkspace(state, workspace); } return { bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: mergeSummary(state.bootstrap.workspaces, workspace.workspace), } : state.bootstrap, error: null, }; }); return workspace; }, deleteProject: async (workspaceId) => { set({ deletingWorkspaceId: workspaceId, error: null }); try { const result = await deleteImageWorkspaceProject(workspaceId); const remaining = get().bootstrap?.workspaces.filter( (item) => item.workspaceId !== workspaceId, ) ?? []; const deletingActive = get().activeWorkspaceId === workspaceId; if (deletingActive) { selectionGeneration += 1; closeEventSource(); } set((state) => ({ bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: remaining } : null, activeWorkspaceId: deletingActive ? remaining[0]?.workspaceId ?? null : state.activeWorkspaceId, workspace: deletingActive ? null : state.workspace, deletingWorkspaceId: null, fieldDrafts: deletingActive ? {} : state.fieldDrafts, chatDraft: deletingActive ? '' : state.chatDraft, error: null, })); if (deletingActive && remaining[0]) { await refreshWorkspaceById(remaining[0].workspaceId); get().connectEvents(); } return result; } catch (error) { set({ deletingWorkspaceId: null, error: messageForError(error) }); throw error; } }, selectProject: async (workspaceId) => { if (workspaceId === get().activeWorkspaceId && get().workspace) return; selectionGeneration += 1; closeEventSource(); set({ activeWorkspaceId: workspaceId, workspace: null, eventState: 'connecting', lastEventId: null, fieldDrafts: {}, chatDraft: '', quoteBlockers: [], error: null, }); try { await refreshWorkspaceById(workspaceId); get().connectEvents(); } catch (error) { set({ error: messageForError(error), eventState: 'degraded' }); } }, refreshWorkspace: async () => { const workspaceId = get().activeWorkspaceId; if (!workspaceId) return null; return refreshWorkspaceById(workspaceId); }, connectEvents: () => { const workspace = get().workspace; if (!workspace) return; const workspaceId = workspace.workspace.workspaceId; if (activeEventSource && activeEventWorkspaceId === workspaceId) return; closeEventSource(); activeEventWorkspaceId = workspaceId; set({ eventState: 'connecting' }); void openImageWorkspaceEvents( workspaceId, workspace.workspace.sessionId, get().lastEventId ?? undefined, ).then((source) => { if (activeEventWorkspaceId !== workspaceId || get().activeWorkspaceId !== workspaceId) { source.close(); return; } activeEventSource = source; source.onopen = () => set({ eventState: 'connected' }); source.onerror = () => { set({ eventState: 'degraded' }); void get().refreshWorkspace().catch(() => null); }; const receive = (raw: Event) => { const event = parseWorkspaceEvent(raw); if (!event || get().activeWorkspaceId !== workspaceId) return; if (event.type === 'design.assistant.delta') { set((state) => ({ lastEventId: event.id, assistantStreams: { ...state.assistantStreams, [event.clientOperationId]: `${state.assistantStreams[event.clientOperationId] ?? ''}${event.delta}`, }, })); return; } set((state) => { if (!state.workspace) return { lastEventId: event.id }; const nextWorkspace = applyEventToWorkspace(state.workspace, event); updateBackgroundLease(nextWorkspace.tasks); return { workspace: nextWorkspace, bootstrap: state.bootstrap ? { ...state.bootstrap, workspaces: mergeSummary(state.bootstrap.workspaces, nextWorkspace.workspace), } : null, lastEventId: event.id, quoteBlockers: event.type === 'design.quote.blocked' ? event.blockers : state.quoteBlockers, }; }); if (event.type === 'design.workspace.updated' && event.changedDirection) { void get().refreshWorkspace().catch(() => null); } }; for (const eventType of [ 'design.session.snapshot', 'design.assistant.delta', 'design.direction.updated', 'design.quote.blocked', 'design.workspace.updated', ]) source.addEventListener(eventType, receive); }).catch((error) => { if (activeEventWorkspaceId === workspaceId) { set({ eventState: 'degraded', error: messageForError(error) }); } }); }, disconnectEvents: () => { closeEventSource(); set({ eventState: 'idle' }); }, setFieldDraft: (path, value) => set((state) => ({ fieldDrafts: { ...state.fieldDrafts, [path]: value }, })), discardFieldDraft: (path) => set((state) => { const fieldDrafts = { ...state.fieldDrafts }; delete fieldDrafts[path]; return { fieldDrafts }; }), setChatDraft: (value) => set({ chatDraft: value }), applyFieldOperations: async (operations, clearDraftPaths = []) => { const workspace = get().workspace; if (!workspace) throw new Error('请先选择一个设计项目'); const command: DesignCommandInput = { kind: 'apply_input', workspaceId: workspace.workspace.workspaceId, sessionId: workspace.workspace.sessionId, expectedDirectionRevision: workspace.form.directionRevision, clientOperationId: createImageWorkspaceOperationId(), input: { kind: 'direct_edit', operations }, }; return executeCommand(command, { label: '保存设计字段', clearDraftPaths, }); }, sendChat: async () => { const workspace = get().workspace; const message = get().chatDraft.trim(); if (!workspace) throw new Error('请先选择一个设计项目'); if (!message) throw new Error('请输入想法或修改要求'); const command: DesignCommandInput = { kind: 'apply_input', workspaceId: workspace.workspace.workspaceId, sessionId: workspace.workspace.sessionId, expectedDirectionRevision: workspace.form.directionRevision, clientOperationId: createImageWorkspaceOperationId(), input: { kind: 'chat', message }, }; return executeCommand(command, { label: '发送设计对话', clearChatDraft: true }); }, resolveDecisionPrompt: async (promptId, action, optionId) => { const workspace = get().workspace; if (!workspace) throw new Error('请先选择一个设计项目'); const command: DesignCommandInput = { kind: 'apply_input', workspaceId: workspace.workspace.workspaceId, sessionId: workspace.workspace.sessionId, expectedDirectionRevision: workspace.form.directionRevision, clientOperationId: createImageWorkspaceOperationId(), input: { kind: 'prompt_resolution', promptId, action, ...(optionId ? { optionId } : {}), }, }; return executeCommand(command, { label: '确认设计决策' }); }, requestQuote: async () => { const workspace = get().workspace; if (!workspace) throw new Error('请先选择一个设计项目'); const command: DesignCommandInput = { kind: 'request_quote', workspaceId: workspace.workspace.workspaceId, sessionId: workspace.workspace.sessionId, expectedDirectionRevision: workspace.form.directionRevision, specificationRevision: workspace.form.specificationRevision, clientOperationId: createImageWorkspaceOperationId(), }; return executeCommand(command, { label: '编译生成方案' }); }, confirmGeneration: async (quoteId) => { const workspace = get().workspace; if (!workspace) throw new Error('请先选择一个设计项目'); const command: DesignCommandInput = { kind: 'confirm_generation', workspaceId: workspace.workspace.workspaceId, sessionId: workspace.workspace.sessionId, quoteId, expectedDirectionRevision: workspace.form.directionRevision, clientOperationId: createImageWorkspaceOperationId(), }; return executeCommand(command, { label: '确认生成' }); }, retryOperation: async (clientOperationId) => { const pending = get().pendingOperations[clientOperationId]; if (!pending) throw new Error('待重试操作不存在'); return executeCommand(pending.command, { label: pending.label, clearDraftPaths: pending.clearDraftPaths, clearChatDraft: pending.clearChatDraft, }); }, reset: () => { selectionGeneration += 1; closeEventSource(); updateBackgroundLease([]); set({ status: 'idle', bootstrap: null, activeWorkspaceId: null, workspace: null, deletingWorkspaceId: null, eventState: 'idle', lastEventId: null, pendingOperations: {}, fieldDrafts: {}, chatDraft: '', assistantStreams: {}, quoteBlockers: [], error: null, }); }, }; }); export function recentDesignChanges(workspace: DesignWorkspace | null): DesignFieldChange[] { return workspace?.form.recentChangeSet.changes ?? []; }