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:
co-authored by
Claude Sonnet 4.6
parent
4895ff5465
commit
107069ea06
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user