feat(agent): enhance canvas content summary to display detailed node and connection counts

This commit is contained in:
HouYunFei
2026-07-30 13:15:43 +08:00
parent f2757baf20
commit 1291dd08c3
5 changed files with 74 additions and 6 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unreleased
+ [优化] Agent「读取画布」工具卡片按文本、图片、配置、视频、音频、分组及连线分类展示画布内容概览。
+ [优化] Agent 首次发送消息时立即清空输入框并展示用户消息,避免等待线程创建期间出现输入内容滞留和后续草稿被清空。
+ [优化] Agent 动态工具卡片显示具体工具名称、准确执行状态和失败原因,避免统一显示为含糊的「工具操作」。
+ [修复] 画布 Agent 默认通过画布节点生成内容,仅在用户明确指定时使用 Codex 内置生图并将结果展示到对话和当前画布。
+44 -3
View File
@@ -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<Record<string, number>>((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);
@@ -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 不应在没有生成结果时提前声称“已生成”。
@@ -197,6 +197,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
if (kind === "command") return <AgentCommandSummary text={text} detail={detail} theme={theme} />;
const state = toolCardState(title, text, detail);
const view = userDetail(detail);
const showText = title !== "读取画布" || text !== "已读取当前画布内容";
return (
<details className="group min-w-0 rounded-xl border px-3 py-2.5 text-left" style={{ borderColor: theme.node.stroke, background: "transparent", color: theme.node.text }}>
<summary className={`list-none ${view ? "cursor-pointer" : "cursor-default"}`} onClick={(event) => { if (!view) event.preventDefault(); }}>
@@ -206,9 +207,11 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
<span className="shrink-0 text-[11px]" style={{ color: state.color }}>{state.label}</span>
{view ? <ChevronDown className="ml-auto size-3.5 shrink-0 transition-transform group-open:rotate-180" style={{ color: theme.node.muted }} /> : null}
</div>
<div className={`mt-1 whitespace-pre-wrap break-words pl-6 text-sm leading-5 ${kind === "command" ? "font-mono text-[12px]" : ""}`} style={{ color: state.isError ? state.color : theme.node.muted }}>
{text}
</div>
{showText ? (
<div className={`mt-1 whitespace-pre-wrap break-words pl-6 text-sm leading-5 ${kind === "command" ? "font-mono text-[12px]" : ""}`} style={{ color: state.isError ? state.color : theme.node.muted }}>
{text}
</div>
) : null}
</summary>
{view ? <div className="ml-6"><AgentDetailBlock detail={view} theme={theme} /></div> : null}
</details>
@@ -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<Record<string, number>>((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;