Files
deerflow-code/frontend-web/src/core/tool-metrics/api.ts
T
吉鑫升andClaude Opus 4.8 5ecf28e60c feat(y-log/knowledge): 日志前缀改 CM助手-全量 + 入库 nodeName 按标题去时间戳保持一致
1. Y-Log 上报 optDetail 前缀 B1-CM助手 → CM助手-全量(全量替换 44 个前端文件 + 文档)
2. 知识库入库 nodeName 改为只取 md5(标题).md(去掉时间戳),同一篇 md 每次入库一致,避免重复记录

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:46:51 +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: "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: "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: "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);
}