From ba2bc460429ddaeff75f7e14985cf35532818c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 22 May 2026 14:24:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E9=80=89=20Agent=20=E7=9B=B4=E8=BE=BE=E3=80=81=E6=BE=84?= =?UTF-8?q?=E6=B8=85=E9=97=AE=E9=A2=98=E6=89=B9=E9=87=8F=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E3=80=81LLM=20=E9=94=99=E8=AF=AF=E4=B8=AD=E6=96=87=E5=8C=96?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端:新会话选 Agent 后直接跳转并自动发送,智能体页输入框宽度与首页对齐 - 前端:澄清卡片支持多选+多问题合并提交,消除单题逐一发送的割裂感 - 前端:定时任务弹窗 Agent 下拉支持长描述截断,避免撑破布局 - 后端:ask_clarification 支持同回合多次调用以提多个独立问题 - 后端:LLM 错误处理与定时任务失败提示统一改为中文 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workspace/messages/message-list.tsx | 277 +++++++++++++++--- frontend-web/src/pages/AgentChatPage.tsx | 50 +++- frontend-web/src/pages/ScheduledTasksPage.tsx | 19 +- .../deerflow/agents/lead_agent/prompt.py | 11 + .../llm_error_handling_middleware.py | 14 +- .../deerflow/runtime/scheduler/service.py | 2 +- .../tools/builtins/clarification_tool.py | 27 +- 7 files changed, 327 insertions(+), 73 deletions(-) diff --git a/frontend-web/src/components/workspace/messages/message-list.tsx b/frontend-web/src/components/workspace/messages/message-list.tsx index 6c6a745f..63cee2fb 100644 --- a/frontend-web/src/components/workspace/messages/message-list.tsx +++ b/frontend-web/src/components/workspace/messages/message-list.tsx @@ -1,6 +1,6 @@ import type { BaseStream } from "@langchain/langgraph-sdk/react"; import { CheckIcon, SendIcon, XIcon } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Conversation, @@ -64,6 +64,111 @@ export function MessageList({ const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const updateSubtask = useUpdateSubtask(); const messages = thread.messages; + + // Track which clarification messages are still awaiting an answer. + // Pending = clarification tool messages that appear after the last human message + // (no user reply has been sent for them yet). + const pendingClarificationIds = useMemo(() => { + let lastHumanIndex = -1; + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i]?.type === "human") { + lastHumanIndex = i; + break; + } + } + const ids: string[] = []; + for (let i = lastHumanIndex + 1; i < messages.length; i++) { + const msg = messages[i]; + if (msg && msg.id && msg.type === "tool" && msg.name === "ask_clarification") { + ids.push(msg.id); + } + } + return ids; + }, [messages]); + + const [clarificationSelections, setClarificationSelections] = useState< + Map + >(new Map()); + const [submittingClarification, setSubmittingClarification] = useState(false); + + const handleClarificationSelectionChange = useCallback( + (id: string, next: ClarificationSelection) => { + setClarificationSelections((prev) => { + const map = new Map(prev); + map.set(id, next); + return map; + }); + }, + [], + ); + + const handleSubmitAllClarifications = useCallback(async () => { + if (pendingClarificationIds.length === 0 || !onClarificationSubmit) { + return; + } + const parts: string[] = []; + for (let index = 0; index < pendingClarificationIds.length; index++) { + const id = pendingClarificationIds[index]!; + const message = messages.find((msg) => msg.id === id); + const payload = message ? getClarificationPayload(message) : null; + const selection = clarificationSelections.get(id) ?? { + selectedIds: [], + custom: "", + }; + const options = (payload?.options ?? []).filter( + (option) => typeof option.label === "string" && option.label.trim(), + ); + const selectedLabels = selection.selectedIds + .map( + (sid) => + options.find((option) => (option.id || option.label) === sid)?.label, + ) + .filter((label): label is string => Boolean(label && label.trim())); + const trimmedCustom = selection.custom.trim(); + let part = ""; + if (selectedLabels.length > 0 && trimmedCustom) { + part = `我选择:${selectedLabels.join("、")};补充:${trimmedCustom}`; + } else if (selectedLabels.length > 0) { + part = `我选择:${selectedLabels.join("、")}`; + } else if (trimmedCustom) { + part = trimmedCustom; + } else { + continue; + } + if (pendingClarificationIds.length > 1) { + const question = payload?.question?.trim(); + parts.push( + question + ? `问题${index + 1}「${question}」:${part}` + : `问题${index + 1}:${part}`, + ); + } else { + parts.push(part); + } + } + if (parts.length === 0) { + return; + } + const combined = + pendingClarificationIds.length > 1 + ? `针对你提出的多个问题,依次回答:\n${parts.map((p) => `- ${p}`).join("\n")}` + : parts[0]!; + setSubmittingClarification(true); + try { + await onClarificationSubmit(combined); + } finally { + setSubmittingClarification(false); + } + }, [pendingClarificationIds, messages, clarificationSelections, onClarificationSubmit]); + + const totalPending = pendingClarificationIds.length; + const hasAnyClarificationAnswer = useMemo(() => { + return pendingClarificationIds.some((id) => { + const sel = clarificationSelections.get(id); + return (sel?.selectedIds.length ?? 0) > 0 || (sel?.custom.trim() ?? "").length > 0; + }); + }, [pendingClarificationIds, clarificationSelections]); + if (thread.isThreadLoading && messages.length === 0) { return ; } @@ -89,14 +194,34 @@ export function MessageList({ } else if (group.type === "assistant:clarification") { const message = group.messages[0]; if (message && hasContent(message)) { + const messageId = message.id ?? ""; + const pendingIndex = pendingClarificationIds.indexOf(messageId); + const isPending = pendingIndex >= 0; + const isLastPending = + isPending && pendingIndex === totalPending - 1; return (
+ handleClarificationSelectionChange(messageId, next) + } + submitting={submittingClarification} + canSubmit={hasAnyClarificationAnswer} + onSubmitAll={handleSubmitAllClarifications} /> void | Promise; rehypePlugins: ReturnType; + isPending: boolean; + pendingIndex?: number; + pendingCount?: number; + isLastPending: boolean; + selection: ClarificationSelection; + onSelectionChange: (next: ClarificationSelection) => void; + submitting: boolean; + canSubmit: boolean; + onSubmitAll: () => void | Promise; }) { const payload = getClarificationPayload(message); - const [customAnswer, setCustomAnswer] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [submitted, setSubmitted] = useState(false); const options = useMemo( () => @@ -479,27 +622,36 @@ function ClarificationCard({ ); } - const disabled = false; + const disabled = !isPending || submitting; + const multiPending = (pendingCount ?? 0) > 1; - async function submitAnswer(answer: string) { - const trimmed = answer.trim(); - if (!trimmed) { + function toggleOption(optionId: string) { + if (disabled) { return; } - setSubmitting(true); - try { - await onSubmit?.(trimmed); - setSubmitted(true); - } finally { - setSubmitting(false); + const next = selection.selectedIds.includes(optionId) + ? selection.selectedIds.filter((id) => id !== optionId) + : [...selection.selectedIds, optionId]; + onSelectionChange({ ...selection, selectedIds: next }); + } + + function updateCustom(value: string) { + if (disabled) { + return; } + onSelectionChange({ ...selection, custom: value }); } return (
-
- 需要你的确认 +
+ 需要你的确认 + {multiPending && pendingIndex !== undefined && pendingCount ? ( + + {pendingIndex + 1} / {pendingCount} + + ) : null}
{payload.context ? (

@@ -513,49 +665,84 @@ function ClarificationCard({ {options.length > 0 ? (

- {options.map((option) => ( - - ))} + + ); + })}
) : null} {payload.allow_custom !== false ? (