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

(cherry picked from commit fce39c847b046ec8b3f11a83f1e809a4a88100d6)
This commit is contained in:
AetherNo2332 committed 2026-09-25 20:57:09 +08:00
1 parent 84e5be0bdb
commit 73ca5a5e9f
7 files changed
+412 -12

No files matched your search

@@ -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;
}