From 107069ea06c87b8b486f09ce946f47640f739bcb Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sun, 12 Apr 2026 15:28:27 +0800 Subject: [PATCH] feat: execution_log full fields in ToolCallStatus, index matching, ExecutionLogPanel overview badges Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../src/components/ExecutionLogPanel.tsx | 62 ++++++++++++++- langgraph/src/components/ToolCallStatus.tsx | 76 ++++++++++++++++--- 2 files changed, 127 insertions(+), 11 deletions(-) diff --git a/langgraph/src/components/ExecutionLogPanel.tsx b/langgraph/src/components/ExecutionLogPanel.tsx index fb15e19..c4d4b91 100644 --- a/langgraph/src/components/ExecutionLogPanel.tsx +++ b/langgraph/src/components/ExecutionLogPanel.tsx @@ -2,6 +2,12 @@ import { useState } from "react"; import { ChevronRight, Activity, CheckCircle2, XCircle, AlertCircle, CornerDownRight } from "lucide-react"; import { cn } from "@/lib/utils"; +const SOURCE_MAP: Record = { + kb_search: "知识库", ticket_list: "工单系统", ticket_detail: "工单系统", + google_search: "网络搜索", web_search_deep: "深度搜索", + code_execute: "代码执行", sandbox_run: "代码沙盒", +}; + interface LogEntry { tool: string; status: string; @@ -41,6 +47,16 @@ export function ExecutionLogPanel({ executionLog }: ExecutionLogPanelProps) { const hasFallback = executionLog.some((e) => !!e.fallbackFrom); const hasError = executionLog.some((e) => e.status === "error"); + const successCount = executionLog.filter(e => e.status === "success").length; + const fallbackCount = executionLog.filter(e => e.status === "fallback_success").length; + const errorCount = executionLog.filter(e => e.status === "error").length; + const partialCount = executionLog.filter(e => e.status === "partial_success").length; + + const primarySource = executionLog + .filter(e => e.status === "success" || e.status === "fallback_success") + .map(e => SOURCE_MAP[e.tool]) + .filter(Boolean)[0] ?? null; + const summaryText = [ `调用了 ${executionLog.length} 个工具`, totalDuration > 0 && `耗时 ${(totalDuration / 1000).toFixed(1)}s`, @@ -61,7 +77,50 @@ export function ExecutionLogPanel({ executionLog }: ExecutionLogPanelProps) { {expanded && ( -
+
+ {/* 总览 badge 行 — 仅 2 个以上工具时显示 */} + {executionLog.length >= 2 && ( +
+ {successCount > 0 && ( + + + {successCount} 成功 + + )} + {fallbackCount > 0 && ( + + + {fallbackCount} 备用切换 + + )} + {partialCount > 0 && ( + + + {partialCount} 部分成功 + + )} + {errorCount > 0 && ( + + + {errorCount} 失败 + + )} + {totalDuration > 0 && ( + + + {(totalDuration / 1000).toFixed(1)}s + + )} + {primarySource && ( + + 主要来自{primarySource} + + )} +
+ )} + + {/* 明细列表 */} +
{executionLog.map((entry, i) => (
{entry.status === "success" && } @@ -89,6 +148,7 @@ export function ExecutionLogPanel({ executionLog }: ExecutionLogPanelProps) { )}
))} +
)}
diff --git a/langgraph/src/components/ToolCallStatus.tsx b/langgraph/src/components/ToolCallStatus.tsx index cb29aab..511a979 100644 --- a/langgraph/src/components/ToolCallStatus.tsx +++ b/langgraph/src/components/ToolCallStatus.tsx @@ -57,7 +57,18 @@ interface ToolCallStatusProps { uiItems?: UIMsgLocal[]; stream?: Parameters[0]["stream"]; components?: Parameters[0]["components"]; - executionLog?: Array<{ tool: string; status: string; inputSummary?: string }>; + executionLog?: Array<{ + tool: string; + toolCallId?: string; + status: string; + inputSummary?: string; + durationMs?: number; + resultCount?: number; + retryCount?: number; + errorCode?: string; + errorMessage?: string; + fallbackFrom?: string; + }>; } interface ToolCallRowProps { @@ -69,6 +80,14 @@ interface ToolCallRowProps { uiItem?: UIMsgLocal; stream?: ToolCallStatusProps["stream"]; components?: ToolCallStatusProps["components"]; + logEntry?: { + durationMs?: number; + resultCount?: number; + retryCount?: number; + errorCode?: string; + errorMessage?: string; + fallbackFrom?: string; + }; // When globalExpanded is not undefined, the parent controls expand state globalExpanded?: boolean; } @@ -82,6 +101,7 @@ function ToolCallRow({ uiItem, stream, components, + logEntry, globalExpanded, }: ToolCallRowProps) { // Local state used only when globalExpanded is undefined (single tool or no global toggle) @@ -105,18 +125,21 @@ function ToolCallRow({ } function renderLabel() { - if (isFailed) return `${label} · 失败`; + const durationStr = logEntry?.durationMs != null + ? ` · ${logEntry.durationMs < 1000 ? `${logEntry.durationMs}ms` : `${(logEntry.durationMs / 1000).toFixed(1)}s`}` + : ""; + if (isFailed) return `${label} · 失败${durationStr}`; if (isFallback) { - if (canExpand) return `${label} · 已切换到备用 · ${expanded ? "收起" : "查看结果"}`; - return `${label} · 已切换到备用`; + if (canExpand) return `${label} · 已切换到备用${durationStr} · ${expanded ? "收起" : "查看结果"}`; + return `${label} · 已切换到备用${durationStr}`; } if (isPartial) { - if (canExpand) return `${label} · 结果不完整 · ${expanded ? "收起" : "查看结果"}`; - return `${label} · 结果不完整`; + if (canExpand) return `${label} · 结果不完整${durationStr} · ${expanded ? "收起" : "查看结果"}`; + return `${label} · 结果不完整${durationStr}`; } if (isDone) { - if (canExpand) return `${label} · ${expanded ? "收起" : "查看结果"}`; - return `${label} · 已完成`; + if (canExpand) return `${label}${durationStr} · ${expanded ? "收起" : "查看结果"}`; + return `${label} · 已完成${durationStr}`; } return tc.name ? (TOOL_LOADING_MAP[tc.name] ?? `正在${label}...`) : "思考中..."; } @@ -158,6 +181,35 @@ function ToolCallRow({ 主工具执行失败,已自动切换至备选方案 )} + {expanded && logEntry && ( +
+ {logEntry.resultCount != null && ( + + 结果数:{logEntry.resultCount} + + )} + {(logEntry.retryCount ?? 0) > 0 && ( + + 重试:{logEntry.retryCount} 次 + + )} + {logEntry.fallbackFrom && ( + + 主工具:{logEntry.fallbackFrom} + + )} + {logEntry.errorCode && ( + + 错误码:{logEntry.errorCode} + + )} + {logEntry.errorMessage && ( + + 原因:{logEntry.errorMessage} + + )} +
+ )} {expanded && !isFailed && uiItem && stream && components && (
@@ -252,8 +304,11 @@ export default function ToolCallStatus({ uiItem = candidates[matchIdx]; } - // Look up execution_log status by tool name - const logEntry = tc.name ? executionLog.find((e) => e.tool === tc.name) : undefined; + // 优先按 toolCallId 精确匹配,降级按 index + const logEntry = executionLog + ? (tc.id && executionLog.find(e => e.toolCallId === tc.id)) + || (i < executionLog.length ? executionLog[i] : undefined) + : undefined; const isPartial = logEntry?.status === "partial_success"; const isFallbackStatus = logEntry?.status === "fallback_success"; @@ -268,6 +323,7 @@ export default function ToolCallStatus({ uiItem={uiItem} stream={stream} components={components} + logEntry={logEntry ?? undefined} globalExpanded={showGlobalToggle ? globalExpanded : undefined} /> );