Files
deerflow-code/frontend-web/src/core/tool-metrics/api.ts
T
AdministratorandClaude Opus 4.8 df9c59e21c feat(y-log): 问答按 LLM 调用各记一条 + 全量补齐各 API 操作埋点
- 问答日志从「每轮 onFinish 一条」改为 useThreadStream onLangChainEvent
  监听 on_chat_model_end,每次大模型调用结束各上报一条(含子代理)
- 广覆盖各 API client 成功路径:查看(view)/查询(search)/新增(create)/
  编辑(edit)/删除(delete)/启停(enabled·disabled)/导入导出,补齐此前
  整模块零埋点与漏掉的单条详情 GET;跳过高频 typeahead/轮询/bootstrap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:42:54 +08:00

172 lines
5.2 KiB
TypeScript

import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config";
import { OptType, writeYLog } from "@/core/log/y-log";
/** Coarse failure buckets, mirrored from the backend classifier. */
export const TOOL_ERROR_CATEGORIES = [
"rate_limited",
"ip_blocked",
"auth_error",
"timeout",
"network_error",
"not_found",
"empty_result",
"tool_error",
] as const;
export type ToolErrorCategory = (typeof TOOL_ERROR_CATEGORIES)[number];
/** Human-readable Chinese labels for each failure bucket. */
export const TOOL_ERROR_CATEGORY_LABELS: Record<string, string> = {
rate_limited: "限流 / 频率超限",
ip_blocked: "IP 被封 / 拒绝访问",
auth_error: "鉴权失败",
timeout: "超时",
network_error: "网络错误",
not_found: "资源不存在",
empty_result: "检索为空",
tool_error: "工具内部错误",
};
export interface ToolMetric {
id: string;
created_at?: string | null;
user_id?: string | null;
user_name?: string | null;
thread_id?: string | null;
run_id?: string | null;
agent_name?: string | null;
tool_name?: string | null;
/** The Agent Skill this call targets (skill tool calls only). */
skill_name?: string | null;
duration_ms: number;
status: "success" | "error" | string;
error_category?: string | null;
error_message?: string | null;
}
export interface ToolMetricsListResponse {
items: ToolMetric[];
total: number;
limit: number;
offset: number;
}
/** Per-skill aggregate — was the skill invoked, and how often did it succeed. */
export interface SkillStat {
skill_name: string;
total: number;
success: number;
error: number;
}
export interface ToolMetricsSummary {
total: number;
error_total: number;
by_category: Record<string, number>;
by_skill: SkillStat[];
}
export interface ToolMetricsFilters {
user_id?: string;
user_name?: string;
tool_name?: string;
/** Coarse call kind: "skill" keeps only Agent-Skill invocations; omit for all. */
kind?: "skill" | "all";
/** Filter to one specific Agent Skill by name. */
skill_name?: string;
status?: "success" | "error";
error_category?: string;
since?: string;
until?: string;
}
export interface ToolMetricsListQuery extends ToolMetricsFilters {
limit?: number;
offset?: number;
}
function buildQuery(filters: ToolMetricsListQuery): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(filters)) {
if (value === undefined || value === null || value === "") continue;
params.set(key, String(value));
}
const qs = params.toString();
return qs ? `?${qs}` : "";
}
export async function loadToolMetrics(
query: ToolMetricsListQuery,
): Promise<ToolMetricsListResponse> {
const url = `${getBackendBaseURL()}/api/admin/tool-metrics${buildQuery(query)}`;
const response = await apiFetch(url);
if (!response.ok) {
const detail = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(detail.detail ?? "Failed to load tool metrics");
}
const result = (await response.json()) as ToolMetricsListResponse;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工具指标-查询调用列表",
url,
reqParam: query,
returnParam: { total: result.total, count: result.items.length },
});
return result;
}
export async function loadToolMetricsSummary(
filters: ToolMetricsFilters,
): Promise<ToolMetricsSummary> {
const url = `${getBackendBaseURL()}/api/admin/tool-metrics/summary${buildQuery(filters)}`;
const response = await apiFetch(url);
if (!response.ok) {
const detail = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(detail.detail ?? "Failed to load tool metrics summary");
}
const result = (await response.json()) as ToolMetricsSummary;
writeYLog({
optType: OptType.search,
optDetail: "B1-CM助手-工具指标-查询汇总统计",
url,
reqParam: filters,
returnParam: { total: result.total, error_total: result.error_total },
});
return result;
}
export async function downloadToolMetricsExcel(
filters: ToolMetricsFilters,
): Promise<void> {
const requestUrl = `${getBackendBaseURL()}/api/admin/tool-metrics/export${buildQuery(filters)}`;
const response = await apiFetch(requestUrl);
if (!response.ok) {
const detail = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(detail.detail ?? "Failed to export tool metrics");
}
writeYLog({
optType: OptType.export,
optDetail: "B1-CM助手-工具指标-导出Excel",
url: requestUrl,
reqParam: filters,
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const filename = (() => {
const header = response.headers.get("content-disposition") ?? "";
const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(header);
if (utf8?.[1]) return decodeURIComponent(utf8[1]);
const plain = /filename="?([^";]+)"?/i.exec(header);
return plain?.[1] ?? `tool_call_metrics.xlsx`;
})();
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.rel = "noopener";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
}