- 问答日志从「每轮 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>
172 lines
5.2 KiB
TypeScript
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);
|
|
}
|