feat: execution_log full fields in ToolCallStatus, index matching, ExecutionLogPanel overview badges

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-12 15:28:27 +08:00
co-authored by Claude Sonnet 4.6
parent 4895ff5465
commit 107069ea06
2 changed files with 127 additions and 11 deletions
+61 -1
View File
@@ -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<string, string> = {
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) {
</button>
{expanded && (
<div className="ml-5 mt-1.5 border-l-2 border-border pl-3 space-y-1.5">
<div className="ml-5 mt-1.5">
{/* 总览 badge 行 — 仅 2 个以上工具时显示 */}
{executionLog.length >= 2 && (
<div className="flex items-center gap-2 flex-wrap mb-2 pb-2 border-b border-border">
{successCount > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400">
<CheckCircle2 className="size-2.5" />
{successCount} 成功
</span>
)}
{fallbackCount > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400">
<CornerDownRight className="size-2.5" />
{fallbackCount} 备用切换
</span>
)}
{partialCount > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-50 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400">
<AlertCircle className="size-2.5" />
{partialCount} 部分成功
</span>
)}
{errorCount > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-red-50 text-red-600 dark:bg-red-900/30 dark:text-red-400">
<XCircle className="size-2.5" />
{errorCount} 失败
</span>
)}
{totalDuration > 0 && (
<span className="inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
<Activity className="size-2.5" />
{(totalDuration / 1000).toFixed(1)}s
</span>
)}
{primarySource && (
<span className="ml-auto text-[10px] text-muted-foreground italic">
主要来自{primarySource}
</span>
)}
</div>
)}
{/* 明细列表 */}
<div className="border-l-2 border-border pl-3 space-y-1.5">
{executionLog.map((entry, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
{entry.status === "success" && <CheckCircle2 className="size-3 text-green-500 shrink-0" />}
@@ -90,6 +149,7 @@ export function ExecutionLogPanel({ executionLog }: ExecutionLogPanelProps) {
</div>
))}
</div>
</div>
)}
</div>
);
+66 -10
View File
@@ -57,7 +57,18 @@ interface ToolCallStatusProps {
uiItems?: UIMsgLocal[];
stream?: Parameters<typeof LoadExternalComponent>[0]["stream"];
components?: Parameters<typeof LoadExternalComponent>[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({
主工具执行失败,已自动切换至备选方案
</div>
)}
{expanded && logEntry && (
<div className="mt-2 ml-7 flex flex-wrap gap-x-4 gap-y-1">
{logEntry.resultCount != null && (
<span className="text-xs text-muted-foreground">
结果数:<span className="text-foreground font-medium">{logEntry.resultCount}</span>
</span>
)}
{(logEntry.retryCount ?? 0) > 0 && (
<span className="text-xs text-muted-foreground">
重试:<span className="text-yellow-600 font-medium">{logEntry.retryCount} 次</span>
</span>
)}
{logEntry.fallbackFrom && (
<span className="text-xs text-muted-foreground">
主工具:<span className="text-foreground font-medium">{logEntry.fallbackFrom}</span>
</span>
)}
{logEntry.errorCode && (
<span className="text-xs text-muted-foreground">
错误码:<span className="text-red-500 font-medium font-mono">{logEntry.errorCode}</span>
</span>
)}
{logEntry.errorMessage && (
<span className="text-xs text-muted-foreground w-full">
原因:<span className="text-red-600">{logEntry.errorMessage}</span>
</span>
)}
</div>
)}
{expanded && !isFailed && uiItem && stream && components && (
<div className="mt-2 ml-7">
<div className="animate-in fade-in duration-300">
@@ -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}
/>
);