feat(cloud-agent): 操作记录 - 折叠成一行只报最新一步并支持展开完整记录

(cherry picked from commit fce39c847b046ec8b3f11a83f1e809a4a88100d6)
This commit is contained in:
AetherNo2332
2026-09-25 02:15:33 +08:00
parent 84e5be0bdb
commit 73ca5a5e9f
7 changed files with 412 additions and 12 deletions

View File

@@ -0,0 +1,78 @@
import { agentToolName, agentToolRetryLabel, agentToolStatus, friendlyAgentToolSummary } from "./agent-tool-presentation";
import { agentToolRetry } from "./agent-tool-retry";
/**
* Agent 的"操作信息"(工具调用记录)在消息流里是一条条 `role === "tool"` 的消息。一轮运行动辄
* 十几步,逐条铺开会把正文挤出可视区,所以界面上把它们折成一行:默认只报最新一步,点击展开
* 完整记录(组件见 `canvas-cloud-agent-chat-ui` 的 `AgentOperationFeed`)。
*
* 这里只做纯切分与文案,不引入 React 或组件类型:调用方把 `CloudAgentChatMessage[]` 传进来,
* 结构上满足 `AgentFeedRecord` 即可。
*/
export type AgentFeedRecord = {
id: string;
role: string;
title?: string;
text: string;
detail?: unknown;
planItems?: readonly unknown[];
question?: unknown;
};
export type AgentFeedSegment<T> = { kind: "operations"; key: string; items: T[] } | { kind: "message"; key: string; item: T };
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}
/**
* 纯载体消息:计划清单(`plan_updated`)与提问(`user_question`)各带一条没有正文的消息,
* 内容分别由输入区上方的"本轮待办"条与提问条渲染。它们既不是操作记录,也没有可展示的正文:
* 实测直接渲染会得到"操作画布 / 操作已完成 / 处理中"这样的空卡片。
*/
export function isAgentCarrierRecord(item: AgentFeedRecord): boolean {
if (item.role === "user" || item.text || item.title) return false;
return Boolean(item.planItems?.length) || Boolean(item.question);
}
/** 等用户确认的工具调用(审批卡)必须留在消息流里,不能被折进"操作记录"里藏起来。 */
export function isAgentOperationRecord(item: AgentFeedRecord): boolean {
return item.role === "tool" && !isAgentCarrierRecord(item) && record(item.detail).status !== "pending";
}
/** 折叠行上报的那一步:最新一条操作记录的友好摘要(等待执行时用"准备…"口径)。 */
export function agentOperationLabel(item: AgentFeedRecord): string {
const name = agentToolName(item.title || "工具执行", item.detail);
const retry = agentToolRetry(item.detail);
// 自动纠正分组在卡片里只显示纠正记录,折叠行跟上同一口径,否则会报一个"已完成"的假象。
if (retry) return `${agentToolRetryLabel(retry)} · ${retry.attempt}/${retry.maxAttempts} 次尝试`;
return friendlyAgentToolSummary(name, item.text, item.detail, agentToolStatus(name, item.text, item.detail) === "pending");
}
/** 失败/被拒的操作默认展开:错误不该被折进一行里看不见。 */
export function agentOperationFailed(item: AgentFeedRecord): boolean {
const status = agentToolStatus(agentToolName(item.title || "工具执行", item.detail), item.text, item.detail);
return status === "failed" || status === "rejected";
}
/**
* 把消息流切成「操作段」与「单条消息」:连续的非审批工具记录合成一段,其余消息各自成段。
*
* 段 key 取段内第一条消息的 id:运行中后续步骤是**追加**进同一段的,key 保持稳定,
* 展开状态与滚动位置不会因为新步骤到达而被重置。
*/
export function buildAgentFeedSegments<T extends AgentFeedRecord>(messages: readonly T[]): AgentFeedSegment<T>[] {
const segments: AgentFeedSegment<T>[] = [];
for (const item of messages) {
if (isAgentCarrierRecord(item)) continue;
if (isAgentOperationRecord(item)) {
const last = segments[segments.length - 1];
if (last?.kind === "operations") last.items.push(item);
else segments.push({ kind: "operations", key: item.id, items: [item] });
continue;
}
segments.push({ kind: "message", key: item.id, item });
}
return segments;
}

View File

@@ -1,4 +1,4 @@
import { agentToolRetry } from "./agent-tool-retry";
import { agentToolRetry, type AgentToolRetry } from "./agent-tool-retry";
export const AGENT_TOOL_METADATA: Record<string, { summary: string | ((context: { pending: boolean; detail?: unknown }) => string); failureMessage: string }> = {
canvas_list_node_types: { summary: "已读取可用节点类型", failureMessage: "获取可用节点类型失败" },
@@ -93,6 +93,18 @@ export function friendlyAgentToolSummary(toolName: string, text: string, detail?
return failed ? failure || "操作未完成" : summary || (pending ? "准备执行操作" : "操作已完成");
}
/** 工具事件的调用名:detail.toolName/name/tool 优先,退化到消息标题。 */
export function agentToolName(title: string, detail?: unknown): string {
return String(field(detail, "toolName") || field(detail, "name") || field(detail, "tool") || title);
}
/** 自动纠正分组的固定文案(卡片与折叠行共用,避免两处各写一套)。 */
export function agentToolRetryLabel(retry: AgentToolRetry): string {
if (retry.status === "recovered") return "自动纠正后已恢复";
if (retry.status === "exhausted") return "自动纠正未完成";
return "自动纠正记录";
}
/**
* 工具失败的稳定归类标签(后端 `errorClass`,见 handoff 工作项 B 的第一步)。
* 界面上把"模型自己出的错"和"真实的权限/状态/上游问题"分开显示,用户与排查都不用猜。