diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6e179..79a896c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ++ [优化] Agent「读取画布」工具卡片按文本、图片、配置、视频、音频、分组及连线分类展示画布内容概览。 + [优化] Agent 首次发送消息时立即清空输入框并展示用户消息,避免等待线程创建期间出现输入内容滞留和后续草稿被清空。 + [优化] Agent 动态工具卡片显示具体工具名称、准确执行状态和失败原因,避免统一显示为含糊的「工具操作」。 + [修复] 画布 Agent 默认通过画布节点生成内容,仅在用户明确指定时使用 Codex 内置生图并将结果展示到对话和当前画布。 diff --git a/canvas-agent/src/agent/codex-history.ts b/canvas-agent/src/agent/codex-history.ts index 2bd526f..a59bb3e 100644 --- a/canvas-agent/src/agent/codex-history.ts +++ b/canvas-agent/src/agent/codex-history.ts @@ -48,7 +48,7 @@ export function threadMessages(thread: unknown, planUpdates: CodexPlanUpdate[] = const tool = String(field(item, "tool") || "工具调用"); const error = String(field(field(item, "error"), "message") || ""); const input = toolArguments(field(item, "arguments")); - messages.push({ id, role: "tool", title: toolName(tool), text: error || toolHistorySummary(tool, input), detail: toolHistoryDetail(tool, item, input, error) }); + messages.push({ id, role: "tool", title: toolName(tool), text: error || toolHistorySummary(tool, item, input), detail: toolHistoryDetail(tool, item, input, error) }); } if (type === "commandExecution") { const command = String(field(item, "command") || "").trim(); @@ -169,10 +169,15 @@ function toolHistoryDetail(tool: string, item: unknown, input: unknown, error: s } /** 生成 MCP 工具在对话中的结果摘要。 */ -function toolHistorySummary(tool: string, input: unknown) { +function toolHistorySummary(tool: string, item: unknown, input: unknown) { if (tool === "site_navigate") return `已打开${routeName(String(field(input, "path") || "/"))}`; if (tool === "canvas_list_projects") return "已读取画布列表"; - if (tool === "canvas_get_state") return "已读取当前画布内容"; + if (tool === "canvas_get_state") { + const result = parseToolResult(field(item, "result")); + const nodes = arrayValue(field(result, "nodes")); + const connections = arrayValue(field(result, "connections")); + return nodes.length || connections.length || result ? canvasContentSummary(nodes, connections.length) : "已读取当前画布内容"; + } if (tool === "canvas_get_selection") return "已读取当前选中内容"; if (tool === "prompts_search") return `已搜索提示词“${String(field(input, "query") || "") || "全部"}”`; if (tool === "assets_list") return "已读取我的素材"; @@ -180,6 +185,42 @@ function toolHistorySummary(tool: string, input: unknown) { return `${toolName(tool)}已完成`; } +/** 按节点类型生成人类可读的画布内容概览。 */ +function canvasContentSummary(nodes: unknown[], connections: number) { + const counts = nodes.reduce>((result, node) => { + const type = String(field(node, "type") || "other"); + result[type] = (result[type] || 0) + 1; + return result; + }, {}); + const known = new Set(["text", "image", "config", "video", "audio", "group"]); + const other = Object.entries(counts).reduce((total, [type, count]) => total + (known.has(type) ? 0 : count), 0); + const parts = [ + counts.text ? `${counts.text} 个文本` : "", + counts.image ? `${counts.image} 张图片` : "", + counts.config ? `${counts.config} 个配置` : "", + counts.video ? `${counts.video} 个视频` : "", + counts.audio ? `${counts.audio} 个音频` : "", + counts.group ? `${counts.group} 个分组` : "", + other ? `${other} 个其他节点` : "", + connections ? `${connections} 条连线` : "", + ].filter(Boolean); + return parts.length ? parts.join("、") : "当前画布为空"; +} + +/** 从 MCP 历史结果中还原工具返回的数据。 */ +function parseToolResult(result: unknown) { + const content = field(result, "content"); + const text = arrayValue(content) + .map((item) => field(item, "text")) + .filter((item): item is string => typeof item === "string") + .join("\n"); + try { + return text ? JSON.parse(text) : result; + } catch { + return text || result; + } +} + /** 提取工具参数中适合普通用户查看的信息。 */ function toolInputRows(tool: string, input: unknown) { if (tool === "site_navigate") return [textRow("目标页面", routeName(String(field(input, "path") || "/")))].filter(Boolean); diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 7304540..86e82a4 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。 - Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。 - Agent 动态工具信息:执行内置生图、查看图片、命令、文件修改或其他动态工具时,卡片标题应显示具体工具名称;执行失败时正文应显示真实错误原因,刷新并恢复历史对话后仍应保持一致,不再统一显示「工具操作已完成」。 - Agent 画布生图:重启 Canvas Agent 后,其自动生成的工作区 `AGENTS.md` 应同步为最新版指令(用户自行编写的其他 `AGENTS.md` 不应被覆盖);输入“生成一张图片”时应调用 `canvas_generate_image`,通过当前画布节点创建并运行生成流程;只有明确输入“使用 Codex 内置生图/ImageGen 技能生成”时才允许调用内置能力,Codex App Server 返回的 `imageGeneration` 结果应显示在对话中并作为保持原比例的图片节点添加到当前画布;Agent 不应在没有生成结果时提前声称“已生成”。 diff --git a/web/src/components/agent/agent-chat-message.tsx b/web/src/components/agent/agent-chat-message.tsx index df6e20f..d252fbb 100644 --- a/web/src/components/agent/agent-chat-message.tsx +++ b/web/src/components/agent/agent-chat-message.tsx @@ -197,6 +197,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t if (kind === "command") return ; const state = toolCardState(title, text, detail); const view = userDetail(detail); + const showText = title !== "读取画布" || text !== "已读取当前画布内容"; return (
{ if (!view) event.preventDefault(); }}> @@ -206,9 +207,11 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t {state.label} {view ? : null} -
- {text} -
+ {showText ? ( +
+ {text} +
+ ) : null}
{view ?
: null}
diff --git a/web/src/components/agent/agent-event-formatters.ts b/web/src/components/agent/agent-event-formatters.ts index 5b159c7..ad40a91 100644 --- a/web/src/components/agent/agent-event-formatters.ts +++ b/web/src/components/agent/agent-event-formatters.ts @@ -372,10 +372,32 @@ export function toolSummary(item?: AgentEventItem) { const connectionField = objectField(result, "connections"); const nodes = Array.isArray(nodeField) ? nodeField : []; const connections = Array.isArray(connectionField) ? connectionField : []; + if (item?.tool === "canvas_get_state" && (Array.isArray(nodeField) || Array.isArray(connectionField))) return canvasContentSummary(nodes, connections.length); if (Array.isArray(nodeField) || Array.isArray(connectionField)) return `读取到 ${nodes.length} 个节点,${connections.length} 条连线`; return "工具调用完成"; } +function canvasContentSummary(nodes: unknown[], connections: number) { + const counts = nodes.reduce>((result, node) => { + const type = stringText(objectField(node, "type")) || "other"; + result[type] = (result[type] || 0) + 1; + return result; + }, {}); + const known = new Set(["text", "image", "config", "video", "audio", "group"]); + const other = Object.entries(counts).reduce((total, [type, count]) => total + (known.has(type) ? 0 : count), 0); + const parts = [ + counts.text ? `${counts.text} 个文本` : "", + counts.image ? `${counts.image} 张图片` : "", + counts.config ? `${counts.config} 个配置` : "", + counts.video ? `${counts.video} 个视频` : "", + counts.audio ? `${counts.audio} 个音频` : "", + counts.group ? `${counts.group} 个分组` : "", + other ? `${other} 个其他节点` : "", + connections ? `${connections} 条连线` : "", + ].filter(Boolean); + return parts.length ? parts.join("、") : "当前画布为空"; +} + export function toolAction(name: string) { const label = toolName(name); if (label.startsWith("读取") || label.startsWith("查看") || label.startsWith("搜索") || label.startsWith("打开")) return label;