/** * 指标聚合的类型与纯函数(无 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) } /** * 各模型档位的”每分钟相对额度权重”(粗估用,非真实计费): * opus-4-8(hard/reviewer)最贵 → sonnet(easy)最省。 * 未知模型用 DEFAULT_COST_PER_MIN 兜底。 */ export const MODEL_COST_PER_MIN: Record = { 'claude-opus-4-8': 1.0, '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, }; }