合并 AI 设计多会话客户端
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

整合 Enter 发送与多会话交互,保留服务端持久 Conversation Session,并补齐迁移、回归、Electron E2E 与 canonical 文档。
This commit is contained in:
2026-08-11 16:18:52 +08:00
32 changed files with 2417 additions and 804 deletions

View File

@@ -1,9 +1,11 @@
import { create } from 'zustand';
import {
confirmImageWorkspaceGeneration,
createImageWorkspaceConversation,
createImageWorkspaceTurnId,
createImageWorkspaceProject,
fetchImageWorkspace,
fetchImageWorkspaceConversation,
fetchImageWorkspaceProject,
fetchImageWorkspaceTasks,
ImageWorkspaceApiError,
@@ -15,8 +17,9 @@ import { useAuthStore } from '@/stores/auth';
import {
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
type DesignAssistantDeltaEvent,
type DesignConversation,
type DesignConversationSnapshotEvent,
type DesignGenerationTask,
type DesignGenerationTasksSnapshotEvent,
type DesignGenerationTaskUpdatedEvent,
type DesignWorkspace,
type DesignWorkspaceBootstrap,
@@ -35,6 +38,7 @@ export type ImageWorkspaceTaskStreamState = 'idle' | 'connecting' | 'connected'
export type PendingDesignTurn = {
workspaceId: string;
conversationId: string;
clientTurnId: string;
turnRevision: number;
userText: string | null;
@@ -47,21 +51,27 @@ 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<DesignWorkspaceBootstrap | null>;
createProject: (title: string) => Promise<DesignWorkspace>;
createConversation: (title?: string) => Promise<DesignConversation>;
renameProject: (workspaceId: string, title: string) => Promise<DesignWorkspace>;
selectProject: (workspaceId: string) => Promise<void>;
selectConversation: (conversationId: string) => Promise<void>;
refreshWorkspace: () => Promise<DesignWorkspace | null>;
refreshConversation: () => Promise<DesignConversation | null>;
refreshTasks: () => Promise<DesignGenerationTask[]>;
connectTaskStream: () => void;
disconnectTaskStream: () => void;
sendMessage: (message: string, attachmentAssetIds?: string[]) => Promise<DesignWorkspace>;
confirmGeneration: (quoteId: string) => Promise<DesignWorkspace>;
sendMessage: (message: string, attachmentAssetIds?: string[]) => Promise<DesignConversation>;
confirmGeneration: (quoteId: string) => Promise<DesignConversation>;
reset: () => void;
};
@@ -71,9 +81,12 @@ 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<typeof setInterval> | null = null;
const taskEventRevisions = new Map<string, number>();
let workspaceLoadGeneration = 0;
let conversationSelectionGeneration = 0;
function taskRevisionKey(workspaceId: string, taskId: string): string {
return `${workspaceId}:${taskId}`;
@@ -94,6 +107,7 @@ function closeTaskEventSource(): void {
}
activeTaskEventSource = null;
activeTaskEventWorkspaceId = null;
activeTaskEventConversationId = null;
taskEventRevisions.clear();
}
@@ -117,26 +131,27 @@ function parseTaskUpdatedEvent(event: Event): DesignGenerationTaskUpdatedEvent |
}
}
function parseTasksSnapshotEvent(event: Event): DesignGenerationTasksSnapshotEvent | null {
function parseConversationSnapshotEvent(event: Event): DesignConversationSnapshotEvent | null {
const data = (event as MessageEvent<unknown>).data;
if (typeof data !== 'string') return null;
try {
const payload = JSON.parse(data) as Partial<DesignGenerationTasksSnapshotEvent>;
if (payload.type !== 'design.generation_tasks.snapshot'
const payload = JSON.parse(data) as Partial<DesignConversationSnapshotEvent>;
if (payload.type !== 'design.conversation.snapshot'
|| typeof payload.id !== 'string'
|| typeof payload.workspaceId !== 'string'
|| typeof payload.conversationId !== 'string'
|| !Number.isInteger(payload.workspaceViewRevision)
|| !payload.workspace
|| payload.workspace.workspaceId !== payload.workspaceId
|| payload.workspace.viewRevision !== payload.workspaceViewRevision
|| !Array.isArray(payload.workspace.messages)
|| !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 DesignGenerationTasksSnapshotEvent;
return payload as DesignConversationSnapshotEvent;
} catch {
return null;
}
@@ -150,6 +165,7 @@ function parseAssistantDeltaEvent(event: Event): DesignAssistantDeltaEvent | nul
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
@@ -198,7 +214,7 @@ function messageOf(error: unknown): string {
}
function toSummary(workspace: DesignWorkspace): DesignWorkspaceSummary {
const { messages: _messages, ...summary } = workspace;
const { conversations: _conversations, ...summary } = workspace;
return summary;
}
@@ -217,15 +233,32 @@ function upsertSummary(
};
}
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(
workspace: DesignWorkspace,
conversation: DesignConversation,
clientTurnId: string,
userText: string,
): PendingDesignTurn {
return {
workspaceId: workspace.workspaceId,
workspaceId: conversation.workspaceId,
conversationId: conversation.conversationId,
clientTurnId,
turnRevision: workspace.turnRevision + 1,
turnRevision: conversation.turnRevision + 1,
userText,
assistantText: '',
lastChunkIndex: -1,
@@ -245,29 +278,37 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
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) => {
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).then((source) => {
if (generation !== taskStreamGeneration || get().activeWorkspaceId !== workspaceId) {
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) return;
if (!delta
|| delta.workspaceId !== activeTaskEventWorkspaceId
|| delta.conversationId !== activeTaskEventConversationId) return;
set((state) => {
if (state.activeWorkspaceId !== delta.workspaceId
|| (state.workspace?.turnRevision ?? 0) >= delta.turnRevision) {
|| state.activeConversationId !== delta.conversationId
|| (state.conversation?.turnRevision ?? 0) >= delta.turnRevision) {
return state;
}
const pending = state.pendingTurn;
@@ -276,6 +317,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
return {
pendingTurn: {
workspaceId: delta.workspaceId,
conversationId: delta.conversationId,
clientTurnId: delta.clientTurnId,
turnRevision: delta.turnRevision,
userText: null,
@@ -286,6 +328,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
};
}
if (pending.workspaceId !== delta.workspaceId
|| pending.conversationId !== delta.conversationId
|| pending.clientTurnId !== delta.clientTurnId
|| pending.turnRevision !== delta.turnRevision
|| delta.chunkIndex !== pending.lastChunkIndex + 1) {
@@ -300,11 +343,14 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
};
});
});
source.addEventListener('design.generation_tasks.snapshot', (event) => {
const snapshot = parseTasksSnapshotEvent(event);
if (!snapshot || snapshot.workspaceId !== activeTaskEventWorkspaceId) return;
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) return 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);
@@ -319,33 +365,34 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
Math.max(knownRevision, snapshot.workspaceViewRevision),
);
}
const currentWorkspace = state.workspace?.workspaceId === snapshot.workspaceId
? state.workspace
: null;
let workspace = state.workspace;
if (!currentWorkspace
|| snapshot.workspaceViewRevision >= currentWorkspace.viewRevision) {
workspace = snapshot.workspace;
} else if (snapshot.workspace.turnRevision > currentWorkspace.turnRevision) {
workspace = {
...snapshot.workspace,
title: currentWorkspace.title,
viewRevision: currentWorkspace.viewRevision,
updatedAt: currentWorkspace.updatedAt > snapshot.workspace.updatedAt
? currentWorkspace.updatedAt
: snapshot.workspace.updatedAt,
};
}
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
&& snapshot.workspace.turnRevision >= state.pendingTurn.turnRevision
&& state.pendingTurn.conversationId === snapshot.conversationId
&& snapshot.conversation.turnRevision >= state.pendingTurn.turnRevision
? null
: state.pendingTurn;
return {
tasks: sortTasks([...tasksById.values()]),
workspace,
conversation,
bootstrap,
pendingTurn,
};
@@ -407,7 +454,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
startFallbackPolling();
};
}).catch(() => {
if (generation !== taskStreamGeneration || get().activeWorkspaceId !== workspaceId) return;
if (generation !== taskStreamGeneration
|| get().activeWorkspaceId !== workspaceId
|| get().activeConversationId !== conversationId) return;
set({ taskStreamState: 'degraded' });
startFallbackPolling();
});
@@ -422,7 +471,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
status: 'auth-required',
bootstrap: null,
activeWorkspaceId: null,
activeConversationId: null,
workspace: null,
conversation: null,
creatingConversation: false,
tasks: [],
pendingTurn: null,
taskStreamState: 'idle',
@@ -434,49 +486,88 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
const applyWorkspace = (workspace: DesignWorkspace): DesignWorkspace => {
set((state) => {
const pendingTurn = state.pendingTurn?.workspaceId === workspace.workspaceId
&& workspace.turnRevision >= state.pendingTurn.turnRevision
? null
: state.pendingTurn;
return {
status: 'ready',
bootstrap: upsertSummary(state.bootstrap, workspace),
activeWorkspaceId: workspace.workspaceId,
workspace,
pendingTurn,
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<void> => {
const loadGeneration = ++workspaceLoadGeneration;
conversationSelectionGeneration += 1;
const workspace = await fetchImageWorkspaceProject(workspaceId);
if (get().activeWorkspaceId !== workspaceId) return;
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 (get().activeWorkspaceId !== workspaceId) return;
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,
}));
startTaskStream(workspaceId);
for (const task of tasks) {
taskEventRevisions.set(taskRevisionKey(workspaceId, task.taskId), workspace.viewRevision);
}
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<never> => {
if (error instanceof ImageWorkspaceApiError
&& error.code === 'workspace_revision_conflict') {
await get().refreshWorkspace().catch(() => null);
&& (error.code === 'conversation_revision_conflict'
|| error.code === 'workspace_revision_conflict')) {
await get().refreshConversation().catch(() => null);
throw new ImageWorkspaceApiError(
409,
error.code,
'设计项目已更新,内容已刷新,请重新提交',
'设计会话已更新,内容已刷新,请重新提交',
);
}
throw error;
@@ -486,7 +577,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
status: 'idle',
bootstrap: null,
activeWorkspaceId: null,
activeConversationId: null,
workspace: null,
conversation: null,
creatingConversation: false,
tasks: [],
pendingTurn: null,
taskStreamState: 'idle',
@@ -509,7 +603,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
status: 'ready',
bootstrap,
activeWorkspaceId,
activeConversationId: null,
workspace: null,
conversation: null,
tasks: [],
pendingTurn: null,
taskStreamState: 'idle',
@@ -525,7 +621,9 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
status: unavailable(error) ? 'unavailable' : 'error',
bootstrap: null,
activeWorkspaceId: null,
activeConversationId: null,
workspace: null,
conversation: null,
tasks: [],
pendingTurn: null,
taskStreamState: 'idle',
@@ -543,8 +641,26 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
createProject: async (title) => {
try {
const workspace = applyWorkspace(await createImageWorkspaceProject(title));
set({ tasks: [], pendingTurn: null });
startTaskStream(workspace.workspaceId);
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) });
@@ -552,6 +668,37 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
}
},
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));
@@ -566,16 +713,56 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
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 loadWorkspace(workspaceId);
await loadPromise;
} catch (error) {
set({ error: handleRequestError(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) });
}
}
},
@@ -597,6 +784,26 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
}
},
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) {
@@ -613,13 +820,6 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
&& currentWorkspace?.workspaceId === workspaceId
&& currentWorkspace.viewRevision === requestedViewRevision) {
set((state) => ({ tasks: mergeTasks(state.tasks, tasks) }));
for (const task of tasks) {
const key = taskRevisionKey(workspaceId, task.taskId);
taskEventRevisions.set(
key,
Math.max(taskEventRevisions.get(key) ?? 0, requestedViewRevision),
);
}
}
return tasks;
} catch (error) {
@@ -630,39 +830,48 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
connectTaskStream: () => {
const workspaceId = get().activeWorkspaceId;
if (workspaceId) startTaskStream(workspaceId);
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) throw new Error('请先选择设计项目');
if (!workspace || !conversation) throw new Error('请先选择设计会话');
try {
set({ pendingTurn: createPendingTurn(workspace, clientTurnId, userText), error: null });
set({
pendingTurn: createPendingTurn(conversation, clientTurnId, userText),
error: null,
});
const updated = attachmentAssetIds.length > 0
? await sendImageWorkspaceMessage(
workspace.workspaceId,
workspace.turnRevision,
conversation.conversationId,
conversation.turnRevision,
userText,
clientTurnId,
attachmentAssetIds,
)
: await sendImageWorkspaceMessage(
workspace.workspaceId,
workspace.turnRevision,
conversation.conversationId,
conversation.turnRevision,
userText,
clientTurnId,
);
if (get().activeWorkspaceId === workspace.workspaceId) {
applyWorkspace(updated);
if (get().activeWorkspaceId === workspace.workspaceId
&& get().activeConversationId === conversation.conversationId) {
applyConversation(updated);
await get().refreshTasks().catch(() => []);
}
return updated;
} catch (error) {
if (get().activeWorkspaceId !== workspace.workspaceId) throw error;
if (get().activeWorkspaceId !== workspace.workspaceId
|| get().activeConversationId !== conversation.conversationId) throw error;
const errorMessage = handleRequestError(error);
set((state) => ({
error: errorMessage,
@@ -676,21 +885,25 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
confirmGeneration: async (quoteId) => {
const workspace = get().workspace;
if (!workspace) throw new Error('请先选择设计项目');
const conversation = get().conversation;
if (!workspace || !conversation) throw new Error('请先选择设计会话');
const clientTurnId = createImageWorkspaceTurnId();
try {
set({
pendingTurn: createPendingTurn(workspace, clientTurnId, '确认生成'),
pendingTurn: createPendingTurn(conversation, clientTurnId, '确认生成'),
error: null,
});
const updated = await confirmImageWorkspaceGeneration(
workspace.workspaceId,
workspace.turnRevision,
conversation.conversationId,
conversation.turnRevision,
quoteId,
clientTurnId,
);
if (get().activeWorkspaceId === workspace.workspaceId) {
applyWorkspace(updated);
if (get().activeConversationId === conversation.conversationId) {
applyConversation(updated);
}
let confirmedTaskVisible = get().tasks.some((task) => task.quoteId === quoteId);
for (
let attempt = 0;
@@ -718,7 +931,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
}
return updated;
} catch (error) {
if (get().activeWorkspaceId !== workspace.workspaceId) throw error;
if (get().activeWorkspaceId !== workspace.workspaceId
|| get().activeConversationId !== conversation.conversationId) throw error;
const errorMessage = handleRequestError(error);
set((state) => ({
error: errorMessage,
@@ -732,12 +946,17 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
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',