修复: 打通设计回复流式投递与结构化确认

问题:Run 完成会抢在 SSE 流式事件之前清空 pending,导致回复整块出现;无 Quote 的确认按钮又会降级为普通聊天,因此无法创建任务。

实现:为 Main 事件队列增加有界投递屏障,严格识别生成确认短语并只调用结构化 Quote Action,接入 generation capability 提示,同时补齐竞态、误触和任务对账测试。
This commit is contained in:
2026-08-03 17:19:53 +08:00
parent fab0e4034c
commit e697853d66
4 changed files with 408 additions and 51 deletions

View File

@@ -37,6 +37,21 @@ import type {
} from '../../../shared/image-workspace';
const ACTIVE_TASK_STATUSES = new Set<DesignTaskStatus>(['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<RenderedDesignMessage[]>(() => {
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<HTMLTextAreaElement>) => {
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() {
>
<div className="mx-auto w-full max-w-3xl">
<div className="chat-composer-surface rounded-2xl border border-border/70 bg-background p-3 shadow-soft focus-within:ring-1 focus-within:ring-foreground/10">
{!generationAvailable ? (
<p className="mb-2 rounded-xl bg-amber-500/10 px-3 py-2 text-xs font-semibold text-amber-700 dark:text-amber-300">
/
</p>
) : null}
<Textarea
aria-label="设计需求"
value={prompt}