From f7b030e20913a8a4ac66e5e874c43875d3c17acb Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 16 Jun 2026 14:02:53 +0800 Subject: [PATCH] feat(agent): enhance online Agent functionality with improved read-only responses and log view --- docs/content/docs/progress/pending-test.mdx | 3 + .../components/canvas-assistant-panel.tsx | 130 ++++++++++++++---- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 4fa50b9..90598f2 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -8,3 +8,6 @@ description: 当前版本已实现但仍需人工验证的变更项 - 右上角 `Agent` 按钮在面板已打开时再次点击会走与右侧收起图标一致的收起动画,不再立即卸载面板;需要验证打开、右上角再次点击收起、右侧收起图标收起和收起过程中再次打开都正常。 - 右侧 `Agent` 面板默认打开宽度已调大;需要验证首次打开空间更宽,手动拖拽调整宽度仍正常。 - 在线 `Agent` 输入框底部的文本模型控件改为无背景无阴影的轻量下拉,只展示图标、模型名和渠道,并移除了输入区上方分割线;需要验证能正常切换文本模型。 +- 在线 `Agent` 询问“画布里有什么内容”等只读问题时不再进入工具确认流程;需要验证只展示普通回答,不出现“等待确认”工具卡片。 +- 在线 `Agent` 连接文本节点和生成配置节点时会把 `Note`、`生成配置` 等别名归一化为真实节点 id,模型漏传连接端点时也会回退到当前画布的文本节点与生成配置节点;需要验证不会再因标题当 id 或空端点而失败。 +- 在线 `Agent` 日志页改为类似本机 Agent 的可复制诊断日志视图;需要验证排查日志、原始 JSON、复制、最近错误和清空操作正常。 diff --git a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx index a8065ae..447102f 100644 --- a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx +++ b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import copyToClipboard from "copy-to-clipboard"; import { Bot, Copy, Cpu, History, PanelRightClose, Plus, Settings2, Trash2, X } from "lucide-react"; -import { Button, Modal, Switch, Tooltip } from "antd"; +import { Button, Modal, Segmented, Switch, Tooltip } from "antd"; import { motion } from "motion/react"; import { modelOptionName, resolveModelChannel, selectableModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; @@ -28,9 +28,10 @@ export const CANVAS_AGENT_PANEL_MOTION_MS = 500; const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000; const ONLINE_AGENT_MAX_STEPS = 4; const ONLINE_AGENT_PROMPT = - '你是 Infinite Canvas 网页内置在线画布助手。你只能返回 JSON,不要 Markdown,不要解释。格式:{"reply":"给用户看的中文说明","ops":[...]}。reply 只能说明“准备执行/等待确认”,不能说“已完成/已删除/已连接/已调整”,因为工具操作需要用户确认后才会执行。工具执行结果返回后,你要判断任务是否完成;完成时返回 ops:[],未完成时返回下一步 ops。ops 可用类型:add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。add_node 支持 nodeType: text/image/config/video/audio,position:{x,y},metadata。delete_node 必须带 id/ids,或用 nodeType:"config" 删除全部生成配置节点。delete_connections 可用 all:true 删除全部连线。文本内容放 metadata.content。用户要求生图、生成文字、视频或音频时,不要直接生成最终内容,要创建提示词文本节点、config 节点、connect_nodes,并追加 run_generation 触发画布已有生成工具;config 节点 metadata 至少包含 generationMode、composerContent、prompt、status:"idle",composerContent/prompt 用 @[node:id] 引用提示词节点或参考节点。只输出能直接 JSON.parse 的对象。'; + '你是 Infinite Canvas 网页内置在线画布助手。你只能返回 JSON,不要 Markdown,不要解释。格式:{"reply":"给用户看的中文说明","ops":[...]}。用户只是询问、查看、总结、描述或分析当前画布内容时,必须返回 ops:[],reply 直接回答画布现状,不要出现“等待确认”。只有需要修改画布时才返回 ops,且 reply 只能说明“准备执行/等待确认”,不能说“已完成/已删除/已连接/已调整”,因为工具操作需要用户确认后才会执行。返回 ops 时必须使用当前画布 JSON 里的真实 node.id / connection.id,不要使用节点标题、类型名、Note、生成配置等别名当 id。工具执行结果返回后,你要判断用户任务是否完成;完成时返回 ops:[],未完成时返回下一步 ops。ops 可用类型:add_node、update_node、delete_node、delete_connections、connect_nodes、set_viewport、select_nodes、run_generation。add_node 支持 nodeType: text/image/config/video/audio,position:{x,y},metadata。delete_node 必须带 id/ids,或用 nodeType:"config" 删除全部生成配置节点。delete_connections 可用 all:true 删除全部连线。文本内容放 metadata.content。用户要求生图、生成文字、视频或音频时,不要直接生成最终内容,要创建提示词文本节点、config 节点、connect_nodes,并追加 run_generation 触发画布已有生成工具;config 节点 metadata 至少包含 generationMode、composerContent、prompt、status:"idle",composerContent/prompt 用 @[node:id] 引用提示词节点或参考节点。只输出能直接 JSON.parse 的对象。'; type OnlineAgentTab = "setup" | "chat" | "history" | "log"; type OnlineAgentLog = { id: string; time: string; title: string; data?: unknown }; +type OnlineAgentLogContext = { model: string; running: boolean; confirmTools: boolean; messages: number; nodes: number; connections: number }; type OnlineLoopContext = { step: number; previous?: unknown }; type CanvasAssistantPanelProps = { @@ -193,8 +194,10 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, session }); addOnlineLog("模型原始回复", answer); const result = parseAgentResult(answer); + const readOnly = isReadOnlyCanvasQuestion(userMessage.text); + const reply = readOnly ? cleanReadOnlyReply(result.reply, snapshotRef.current) : result.reply; addOnlineLog("解析结果", result); - const ops = normalizeOnlineOps(result.ops, userMessage.text, snapshotRef.current); + const ops = readOnly ? [] : normalizeOnlineOps(result.ops, userMessage.text, snapshotRef.current); addOnlineLog("归一化操作", ops); if (ops.length && sameOps(ops, objectDetail(loop.previous).ops)) { addOnlineLog(`Agent Loop ${loop.step} 停止`, { reason: "same_ops", ops }); @@ -202,14 +205,14 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, session return; } if (ops.length) { - upsertMessage(sessionId, { id: assistantId, role: "assistant", text: pendingReply(result.reply) }); + upsertMessage(sessionId, { id: assistantId, role: "assistant", text: pendingReply(reply) }); const toolMessage: CanvasAssistantMessage = { id: nanoid(), role: "tool", title: confirmTools ? "确认工具调用" : "画布操作执行中", text: summarizeCanvasAgentOps(ops) || "画布操作", detail: { name: "canvas_apply_ops", ops, intent: userMessage.text, assistantId, step: loop.step, status: confirmTools ? "pending" : "running" } }; appendMessage(sessionId, toolMessage); addOnlineLog(confirmTools ? "等待用户确认" : "自动执行工具", { step: loop.step, ops }); if (!confirmTools) continued = executeOnlineTool(sessionId, toolMessage.id, ops, { assistantId, userMessage, history, step: loop.step }); } else { - addOnlineLog(`Agent Loop ${loop.step} 结束`, { reply: result.reply, reason: "no_ops" }); - upsertMessage(sessionId, { id: assistantId, role: "assistant", text: result.reply }); + addOnlineLog(`Agent Loop ${loop.step} 结束`, { reply, reason: readOnly ? "readonly" : "no_ops" }); + upsertMessage(sessionId, { id: assistantId, role: "assistant", text: reply }); } } catch (error) { addOnlineLog("请求失败", error instanceof Error ? error.message : error); @@ -305,11 +308,6 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, session + + + + +