feat: 指标统计(执行时长/成功率/复审通过率/重试率/额度估算)tsk_lDx6zd-EbA00
从 runs/tasks/events 聚合健康度与成本指标,看板新增指标小面板。 - Store.metrics(projectId?):执行时长(avg/p50/p95,未结束与负时长不计)、 executor 成功率、复审/安全 verdict 通过率、failed→queued 重试率 + needs_attention、按模型额度粗估(时长×档位权重,标注估算) - src/model/metrics.ts:类型 + 纯函数(percentile/ratio/estimateCostUnits/emptyMetrics) - API:GET /api/metrics?project= - web:归档区上方指标面板(数字卡 + 按模型分布),失败/空数据静默隐藏 - test/metrics.test.ts:9 个用例(纯函数/空数据/时长/成功率/复审/重试/按模型/项目过滤) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 指标聚合的类型与纯函数(无 I/O,便于单测)。
|
||||
* 实际从 runs/events/tasks 聚合的逻辑见 Store.metrics()。
|
||||
*/
|
||||
|
||||
/** executor 执行时长统计(仅计入已结束、时长非负的 run;单位毫秒) */
|
||||
export interface DurationStats {
|
||||
count: number; // 计入统计的 run 数
|
||||
avgMs: number; // 平均时长
|
||||
p50Ms: number; // 中位数
|
||||
p95Ms: number; // 95 分位
|
||||
maxMs: number; // 最长
|
||||
}
|
||||
|
||||
/** executor 运行成功率(只统计已落定的 succeeded/failed,started/cancelled 不计) */
|
||||
export interface RunStats {
|
||||
total: number; // succeeded + failed
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
successRate: number | null; // succeeded / total;无样本 → null
|
||||
}
|
||||
|
||||
/** 复审通过率(code review + 安全审计;verdict=approve 占比) */
|
||||
export interface ReviewStats {
|
||||
reviewTotal: number; // 有 review verdict(approve/reject)的任务数
|
||||
reviewApprove: number;
|
||||
reviewRate: number | null; // approve / total;无样本 → null
|
||||
securityTotal: number; // 有 security verdict 的任务数
|
||||
securityApprove: number;
|
||||
securityRate: number | null;
|
||||
}
|
||||
|
||||
/** 重试 / 需人工关注 */
|
||||
export interface RetryStats {
|
||||
retries: number; // failed→queued 重试发生次数(来自 status.changed 事件)
|
||||
taskCount: number; // 范围内任务总数(重试率分母)
|
||||
retryRate: number | null; // retries / taskCount;无任务 → null
|
||||
needsAttention: number; // 当前处于 needs_attention 的任务数
|
||||
}
|
||||
|
||||
/** 按模型的额度估算(无 token 数据时按 时长×档位 粗估,estimated=true) */
|
||||
export interface ModelUsage {
|
||||
model: string;
|
||||
runs: number; // 计入时长的 executor run 数
|
||||
durationMs: number; // 累计执行时长
|
||||
estCostUnits: number; // 估算额度(相对单位:执行分钟数 × 档位权重)
|
||||
estimated: boolean; // 是否为估算(当前恒 true)
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
projectId: string | null; // null = 全部项目
|
||||
taskCount: number;
|
||||
duration: DurationStats;
|
||||
runs: RunStats;
|
||||
review: ReviewStats;
|
||||
retry: RetryStats;
|
||||
byModel: ModelUsage[]; // 按估算额度降序
|
||||
estimated: boolean; // 额度是否为估算(无 token 用量 → true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 各模型档位的“每分钟相对额度权重”(粗估用,非真实计费):
|
||||
* fable-5(hard)最贵 → opus(medium)次之 → sonnet(easy)最省。
|
||||
* 未知模型用 DEFAULT_COST_PER_MIN 兜底。
|
||||
*/
|
||||
export const MODEL_COST_PER_MIN: Record<string, number> = {
|
||||
'claude-fable-5': 1.0,
|
||||
'claude-opus-4-8': 0.6,
|
||||
'claude-sonnet-4-6': 0.2,
|
||||
};
|
||||
export const DEFAULT_COST_PER_MIN = 0.4;
|
||||
|
||||
export function costPerMin(model: string): number {
|
||||
return MODEL_COST_PER_MIN[model] ?? DEFAULT_COST_PER_MIN;
|
||||
}
|
||||
|
||||
/** 最近秩法分位数(sortedAsc 必须升序);空数组 → 0。 */
|
||||
export function percentile(sortedAsc: number[], p: number): number {
|
||||
if (sortedAsc.length === 0) return 0;
|
||||
const rank = Math.ceil((p / 100) * sortedAsc.length);
|
||||
const idx = Math.min(sortedAsc.length - 1, Math.max(0, rank - 1));
|
||||
return sortedAsc[idx];
|
||||
}
|
||||
|
||||
/** 比率(保留 4 位小数);分母 ≤ 0 → null。 */
|
||||
export function ratio(num: number, den: number): number | null {
|
||||
return den > 0 ? Math.round((num / den) * 10000) / 10000 : null;
|
||||
}
|
||||
|
||||
/** 由时长(毫秒)与模型档位估算额度单位(保留 2 位小数)。 */
|
||||
export function estimateCostUnits(durationMs: number, model: string): number {
|
||||
return Math.round((durationMs / 60000) * costPerMin(model) * 100) / 100;
|
||||
}
|
||||
|
||||
/** 空指标(无数据时各项归零,不崩)。 */
|
||||
export function emptyMetrics(projectId: string | null): Metrics {
|
||||
return {
|
||||
projectId,
|
||||
taskCount: 0,
|
||||
duration: { count: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 },
|
||||
runs: { total: 0, succeeded: 0, failed: 0, successRate: null },
|
||||
review: {
|
||||
reviewTotal: 0, reviewApprove: 0, reviewRate: null,
|
||||
securityTotal: 0, securityApprove: 0, securityRate: null,
|
||||
},
|
||||
retry: { retries: 0, taskCount: 0, retryRate: null, needsAttention: 0 },
|
||||
byModel: [],
|
||||
estimated: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user