From e697853d6657adc337f672630c08f6efa6229e8d Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 3 Aug 2026 17:19:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E6=89=93=E9=80=9A?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=9B=9E=E5=A4=8D=E6=B5=81=E5=BC=8F=E6=8A=95?= =?UTF-8?q?=E9=80=92=E4=B8=8E=E7=BB=93=E6=9E=84=E5=8C=96=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:Run 完成会抢在 SSE 流式事件之前清空 pending,导致回复整块出现;无 Quote 的确认按钮又会降级为普通聊天,因此无法创建任务。 实现:为 Main 事件队列增加有界投递屏障,严格识别生成确认短语并只调用结构化 Quote Action,接入 generation capability 提示,同时补齐竞态、误触和任务对账测试。 --- .../image-workspace/works-square-workspace.ts | 65 +++++- src/pages/ImageCanvas/index.tsx | 75 ++++-- tests/unit/image-canvas-page.test.tsx | 103 +++++++-- .../works-square-design-workspace.test.ts | 216 ++++++++++++++++++ 4 files changed, 408 insertions(+), 51 deletions(-) diff --git a/electron/image-workspace/works-square-workspace.ts b/electron/image-workspace/works-square-workspace.ts index ce16d2d..80ee307 100644 --- a/electron/image-workspace/works-square-workspace.ts +++ b/electron/image-workspace/works-square-workspace.ts @@ -184,11 +184,16 @@ type AgentWebSocketFactory = ( type TaskEventQueue = { events: AsyncIterable; - push(event: DesignWorkspaceEvent): void; + push(event: DesignWorkspaceEvent): Promise; finish(): void; fail(error: unknown): void; }; +type QueuedTaskEvent = { + event: DesignWorkspaceEvent; + acknowledge(): void; +}; + type AgentRunEventWaiter = (run: ServerAgentRun) => void; const AGENT_WEBSOCKET_OPEN = 1; @@ -196,6 +201,7 @@ const AGENT_WEBSOCKET_PING_INTERVAL_MS = 20_000; const AGENT_RUN_INITIAL_POLL_INTERVAL_MS = 1_000; const AGENT_RUN_MAX_POLL_INTERVAL_MS = 5_000; const AGENT_RUN_TIMEOUT_MS = 10 * 60_000; +const DESIGN_EVENT_DELIVERY_BARRIER_TIMEOUT_MS = 1_000; function mapBrief(brief: ServerBrief): DesignBrief { return { @@ -475,7 +481,8 @@ function normalizeAgentRunEvent(value: unknown, sessionId: string): ServerAgentR } function createTaskEventQueue(): TaskEventQueue { - const queued: DesignWorkspaceEvent[] = []; + const queued: QueuedTaskEvent[] = []; + const pendingDeliveries = new Set(); const waiters: Array<() => void> = []; let finished = false; let failed = false; @@ -488,9 +495,13 @@ function createTaskEventQueue(): TaskEventQueue { events: { async *[Symbol.asyncIterator]() { while (true) { - const event = queued.shift(); - if (event) { - yield event; + const entry = queued.shift(); + if (entry) { + try { + yield entry.event; + } finally { + entry.acknowledge(); + } continue; } if (failed) throw failure; @@ -500,24 +511,52 @@ function createTaskEventQueue(): TaskEventQueue { }, }, push(event) { - if (finished || failed) return; - queued.push(event); + if (finished || failed) return Promise.resolve(); + let resolveDelivery!: () => void; + const delivered = new Promise((resolve) => { + resolveDelivery = resolve; + }); + let acknowledged = false; + const entry: QueuedTaskEvent = { + event, + acknowledge() { + if (acknowledged) return; + acknowledged = true; + pendingDeliveries.delete(entry); + resolveDelivery(); + }, + }; + pendingDeliveries.add(entry); + queued.push(entry); wake(); + return delivered; }, finish() { if (finished || failed) return; finished = true; + for (const entry of [...pendingDeliveries]) entry.acknowledge(); wake(); }, fail(error) { if (finished || failed) return; failed = true; failure = error; + for (const entry of [...pendingDeliveries]) entry.acknowledge(); wake(); }, }; } +function boundedTaskEventDelivery(delivered: Promise): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(resolve, DESIGN_EVENT_DELIVERY_BARRIER_TIMEOUT_MS); + void delivered.then(() => { + clearTimeout(timeout); + resolve(); + }); + }); +} + function webSocketCloseError(code: number): DesignWorkspaceModuleError | null { if (code === 1000 || code === 1001) return null; if (code === 4401) { @@ -924,6 +963,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule { const { socket } = connection; const queue = createTaskEventQueue(); + let latestWorkspaceEventDelivery = Promise.resolve(); let didOpen = false; let ending = false; let settled = false; @@ -1010,14 +1050,19 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule { socket.onmessage = ({ data }) => { if (ending) return; const agentEvent = agentEventFromWebSocketFrame(data); - const run = normalizeAgentRunEvent(agentEvent, session.session_id); - if (run) this.publishAgentRun(session.session_id, run); const event = normalizeWorkspaceEvent( agentEvent, session.session_id, input.workspaceId, ); - if (event) queue.push(event); + if (event) { + latestWorkspaceEventDelivery = boundedTaskEventDelivery(queue.push(event)); + } + const run = normalizeAgentRunEvent(agentEvent, session.session_id); + if (run) { + const deliveryBarrier = latestWorkspaceEventDelivery; + void deliveryBarrier.then(() => this.publishAgentRun(session.session_id, run)); + } }; socket.onerror = () => { setTimeout(() => { diff --git a/src/pages/ImageCanvas/index.tsx b/src/pages/ImageCanvas/index.tsx index 1c868a8..d9e4ffa 100644 --- a/src/pages/ImageCanvas/index.tsx +++ b/src/pages/ImageCanvas/index.tsx @@ -37,6 +37,21 @@ import type { } from '../../../shared/image-workspace'; const ACTIVE_TASK_STATUSES = new Set(['queued', 'running']); +const CONFIRMATION_UNAVAILABLE_MESSAGE = ( + '当前没有可确认的生成报价,请先让设计 Agent 完成方案与报价。' +); +const GENERATION_CONFIRMATION_INTENTS = new Set([ + '确认生成', + '确认开始生成', + '确认并开始生成', + '确认开始制作', + '确认并开始制作', +]); + +function isGenerationConfirmationIntent(message: string): boolean { + const normalized = message.trim().replace(/[\s,,。.!!??、]/g, ''); + return GENERATION_CONFIRMATION_INTENTS.has(normalized); +} function taskStatusLabel(status: DesignTaskStatus): string { if (status === 'queued') return '排队中'; @@ -211,6 +226,7 @@ export function ImageCanvas() { () => workspace ? activeQuote(workspace.messages) : null, [workspace], ); + const generationAvailable = bootstrap?.capabilities.generation ?? false; const conversationMessages = useMemo(() => { if (!workspace) return []; const messages: RenderedDesignMessage[] = [...workspace.messages]; @@ -259,9 +275,37 @@ export function ImageCanvas() { } }, [conversationMessages.length, pendingTurn?.assistantText]); + const handleConfirm = async (quoteId: string) => { + if (!workspace || confirmingQuoteId) return; + if (!generationAvailable) { + setActionError(CONFIRMATION_UNAVAILABLE_MESSAGE); + return; + } + const requestedWorkspaceId = workspace.workspaceId; + setConfirmingQuoteId(quoteId); + setActionError(null); + try { + await confirmGeneration(quoteId); + } catch (error) { + if (useImageWorkspaceStore.getState().activeWorkspaceId !== requestedWorkspaceId) return; + setActionError(error instanceof Error ? error.message : String(error)); + } finally { + setConfirmingQuoteId(null); + } + }; + const handleSend = async (override?: string) => { const message = (override ?? prompt).trim(); if (!workspace || !message || submitting) return; + if (isGenerationConfirmationIntent(message)) { + setPrompt(''); + if (quote && generationAvailable) { + await handleConfirm(quote.quoteId); + } else { + setActionError(CONFIRMATION_UNAVAILABLE_MESSAGE); + } + return; + } const requestedWorkspaceId = workspace.workspaceId; setSubmitting(true); setActionError(null); @@ -277,21 +321,6 @@ export function ImageCanvas() { } }; - const handleConfirm = async (quoteId: string) => { - if (!workspace || confirmingQuoteId) return; - const requestedWorkspaceId = workspace.workspaceId; - setConfirmingQuoteId(quoteId); - setActionError(null); - try { - await confirmGeneration(quoteId); - } catch (error) { - if (useImageWorkspaceStore.getState().activeWorkspaceId !== requestedWorkspaceId) return; - setActionError(error instanceof Error ? error.message : String(error)); - } finally { - setConfirmingQuoteId(null); - } - }; - const handleComposerKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault(); @@ -469,11 +498,16 @@ export function ImageCanvas() { size="sm" className="h-8 rounded-full px-3 text-xs font-semibold" onClick={() => { - if (reply.includes('确认') && quote) { - void handleConfirm(quote.quoteId); - } else { + if (!isGenerationConfirmationIntent(reply)) { setPrompt(reply); + return; } + if (quote && generationAvailable) { + void handleConfirm(quote.quoteId); + return; + } + setPrompt(''); + setActionError(CONFIRMATION_UNAVAILABLE_MESSAGE); }} > {reply} @@ -502,6 +536,11 @@ export function ImageCanvas() { >
+ {!generationAvailable ? ( +

+ 当前环境仅支持设计沟通,图片/视频生成能力尚未启用。 +

+ ) : null}