From a1dc9b26869b19b2fdae882731401dae1652a69a Mon Sep 17 00:00:00 2001 From: AetherNo2332 <130164460+AetherNo2332@users.noreply.github.com> Date: Fri, 25 Sep 2026 03:15:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20Agent=20=E9=9D=A2=E6=9D=BF=20-?= =?UTF-8?q?=20=E8=AF=BB=E6=B8=85=E5=8D=95=E4=B8=8E=E7=9C=8B=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E5=88=86=E6=A1=A3=E3=80=81=E6=93=8D=E4=BD=9C=E8=A1=8C?= =?UTF-8?q?=E6=8A=98=E5=8F=A0=E5=8A=A8=E6=95=88=E4=B8=8E=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=BA=BF=E6=A0=87=E8=AF=86=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 6c0cadcf9d0dad48a065598ff57b4bde1e135fcd) --- .../canvas/canvas-cloud-agent-chat-ui.tsx | 103 +++++++++++--- .../canvas/canvas-cloud-agent-panel.tsx | 14 +- .../components/canvas/canvas-cloud-agent.css | 46 +++--- web/src/lib/canvas/agent-canvas-actions.ts | 11 +- web/src/lib/canvas/agent-operation-feed.ts | 39 +++++- web/src/lib/canvas/agent-tool-presentation.ts | 132 ++++++++++++++++-- web/test/agent-chat-tool-card.test.tsx | 29 +++- web/test/agent-operation-feed-view.test.tsx | 55 ++++++-- web/test/agent-operation-feed.test.ts | 49 ++++++- web/test/agent-tool-presentation.test.ts | 80 ++++++++++- 10 files changed, 477 insertions(+), 81 deletions(-) diff --git a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx index 63eb25d46..729cb7885 100644 --- a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx @@ -3,8 +3,8 @@ import { Button } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; import { useCallback, useEffect, useId, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; -import { motion, useReducedMotion } from "motion/react"; -import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, X, XCircle } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { ArrowLeft, ArrowUp, AtSign, Bookmark, CheckCircle2, ChevronDown, ChevronUp, CircleAlert, CircleDot, Clapperboard, Eye, HelpCircle, ImagePlus, Layers3, List, ListChecks, LoaderCircle, Palette, Pencil, Plus, RotateCcw, Shapes, Share2, ShoppingBag, Sparkles, Square, Wrench, X, XCircle } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; @@ -13,8 +13,8 @@ import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textare import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import type { Skill, SkillPreset } from "@/services/api/skills"; import { buildSkillMentionReferences } from "@/services/skill-runtime"; -import { agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolName, agentToolRetryLabel, agentToolStatus, friendlyAgentToolSummary } from "@/lib/canvas/agent-tool-presentation"; -import { agentOperationFailed, agentOperationLabel } from "@/lib/canvas/agent-operation-feed"; +import { agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolName, agentToolRetryLabel, agentToolStatus, friendlyAgentToolSummary, type AgentToolCategory } from "@/lib/canvas/agent-tool-presentation"; +import { agentOperationCategory, agentOperationFailed, agentOperationSegmentLabel } from "@/lib/canvas/agent-operation-feed"; import { agentToolRetry, type AgentToolRetryAttempt } from "@/lib/canvas/agent-tool-retry"; export type CloudAgentChatAttachment = { id: string; name: string; url: string }; @@ -39,6 +39,12 @@ export type CloudAgentChatMessage = { text: string; streaming?: boolean; reasoning?: boolean; + /** + * 本轮最终答复(服务端收尾闸门的结论)。false 表示这是过程正文——模型边做边说的那一段, + * 不是结论:运行还在继续,或者这次收尾被闸门拦下了。缺省按 true 处理(老后端没有这个字段)。 + * 界面不再为它单独出角标(用户口径),但标记仍如实保存。 + */ + final?: boolean; planItems?: CloudAgentPlanItem[]; question?: CloudAgentUserQuestion; meta?: string; @@ -47,6 +53,34 @@ export type CloudAgentChatMessage = { interjection?: "sent" | "undelivered"; }; +/** + * 助手正文的收尾语义(工作项 A):final=false 是过程说明,true / 缺省是结论。 + * 缺省按"结论"处理是为了兼容升级前的后端事件——那时所有正文都按最终正文展示。 + */ +export function agentAssistantFinality(payload: Record): boolean { + return payload.final !== false; +} + +/** + * 服务端控制消息(例如运行时的收尾闸门 `completion_blocked`)在时间线里的表示: + * 它是服务端说明,不是用户发言,所以走 system 行——绝不能渲染成真人 user 气泡。 + */ +export function agentControlMessage(id: string, text: string, meta?: string): CloudAgentChatMessage { + return { id, role: "system", text, meta }; +} + +/** + * 类别的图标语言:三档互不串味 —— 清单用 List(读到存在与规模)、画面用 Eye(图片字节真的 + * 交给了模型)、写画布用 Pencil / 创建用 Plus;未登记的工具走中性的 Wrench。 + */ +function agentToolCategoryIcon(category: AgentToolCategory) { + if (category === "vision") return ; + if (category === "read") return ; + if (category === "create") return ; + if (category === "operate") return ; + return ; +} + export type CloudAgentQuickAction = { label: string; prompt: string }; /** @@ -174,8 +208,7 @@ export function AgentChatMessage({ } return (
- {!isUser ? : null} -
+
{item.interjection ? ( 180 ? `${text.slice(0, 180)}…` : text; - const categoryIcon = category === "read" ? : category === "create" ? : ; + const categoryIcon = agentToolCategoryIcon(category); const retry = agentToolRetry(detail); const attempts = objectField(detail, "retryAttempts"); if (retry && Array.isArray(attempts)) { @@ -474,7 +507,7 @@ export function AgentToolCard({ ))} {isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? ( ) : null}
@@ -502,46 +535,67 @@ export function AgentToolCard({ } /** - * Agent 的操作记录(连续的 `role === "tool"` 消息)折成一行:只报最新一步,点击展开完整记录。 + * Agent 的操作记录(连续的 `role === "tool"` 消息)折成一行:报最新一步,点击展开完整记录。 + * + * 语义要点:折叠态整段只有这一行可见,所以**类别徽标必须在折叠行上**(清单 / 查看画面 / + * 修改画布),否则默认状态看不出刚才到底是读了清单还是真看了画面。 * - * 为什么默认收起:一轮运行的十几条工具记录会把正文挤出可视区,用户真正要读的是结论; * 展开状态用「用户覆盖 + 失败时默认展开」两段决定 —— 失败的操作不能被折进一行里看不见, - * 但用户手动收起之后就不再自动弹开(跑动中新步骤只会追加,不会重置展开状态)。 + * 但用户手动收起之后就不再自动弹开(跑动中新步骤只会追加,不会重置状态)。 + * `live` 由面板按"这一段是不是对话末尾且在跑"给出:只有还在推进时才流光。 */ export function AgentOperationFeed({ items, theme, references = [], onFocusNode, + live = false, }: { items: CloudAgentChatMessage[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; references?: CanvasResourceReference[]; onFocusNode?: (nodeId: string) => void; + live?: boolean; }) { const [userExpanded, setUserExpanded] = useState(null); + const reducedMotion = useReducedMotion(); const listId = useId(); const latest = items[items.length - 1]; if (!latest) return null; - const latestState = toolCardState(latest.title || "工具执行", latest.text, latest.detail); - const latestLabel = agentOperationLabel(latest); + const category = agentOperationCategory(latest); + const categoryLabel = agentToolCategoryLabel(agentToolName(latest.title || "工具执行", latest.detail), category); + const label = agentOperationSegmentLabel(items); const failed = items.some((item) => agentOperationFailed(item)); const expanded = userExpanded ?? failed; + const shimmering = live && !failed; return ( -
+
@@ -1171,12 +1225,17 @@ export function AgentPanelTabs({ ); } -function AgentTimelineMarker({ theme, tone, icon }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; tone: "agent" | "muted" | "tool" | "approval" | "error"; icon?: ReactNode }) { - const color = tone === "error" ? "#ef4444" : tone === "approval" ? "#f97316" : tone === "tool" ? "#4f7cff" : tone === "agent" ? theme.accent.primary : theme.node.muted; +/** + * 时间线标记:图标由调用方给(系统行给 Sparkles、错误与审批给 CircleAlert…),不再有默认 + * 的模型品牌 glyph —— 那个圆环标志会被读成"这条是模型/OpenAI 出品",与它实际表达的 + * "这是谁的一行"无关。 + */ +function AgentTimelineMarker({ theme, tone, icon }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; tone: "agent" | "muted" | "approval" | "error"; icon: ReactNode }) { + const color = tone === "error" ? "#ef4444" : tone === "approval" ? "#f97316" : tone === "agent" ? theme.accent.primary : theme.node.muted; return (