191 lines
7.1 KiB
JavaScript
191 lines
7.1 KiB
JavaScript
/* eslint-disable no-console */
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// ── 路径常量 ─────────────────────────────────────────────────────────────────
|
||
const CLAUDE_ROOT = path.resolve(__dirname, '..');
|
||
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
|
||
|
||
// ── JSONL 工具函数 ────────────────────────────────────────────────────────────
|
||
/** 安全解析单文件中的每一行 JSONL,遇到损坏行跳过 */
|
||
function readJsonl(filePath) {
|
||
try {
|
||
return fs.readFileSync(filePath, 'utf8')
|
||
.split('\n')
|
||
.filter(Boolean)
|
||
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
||
.filter(Boolean);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/** 读取 DEBUG_DIR 下所有匹配 glob 前缀的 .jsonl 文件 */
|
||
function readGlobJsonl(prefix) {
|
||
try {
|
||
return fs.readdirSync(DEBUG_DIR)
|
||
.filter(f => f.startsWith(prefix) && f.endsWith('.jsonl'))
|
||
.flatMap(f => readJsonl(path.join(DEBUG_DIR, f)));
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// ── 日期截止计算 ──────────────────────────────────────────────────────────────
|
||
function cutoffTs(days) {
|
||
const d = new Date();
|
||
d.setDate(d.getDate() - days);
|
||
return d.toISOString();
|
||
}
|
||
|
||
// ── 核心分析函数 ──────────────────────────────────────────────────────────────
|
||
/**
|
||
* @param {{ days?: number, save?: boolean, json?: boolean }} opts
|
||
* @returns {{ skills: object[], summary: object }}
|
||
*/
|
||
function analyze({ days = 7, save = false } = {}) {
|
||
const since = cutoffTs(days);
|
||
|
||
// 1. 读取路由数据(route-stats-daily-*.jsonl)
|
||
const routeRows = readGlobJsonl('route-stats-daily-')
|
||
.filter(r => r.ts >= since && r.skill);
|
||
|
||
// 2. 读取 outcome 数据(skill-outcome.jsonl)
|
||
const outcomeRows = readJsonl(path.join(DEBUG_DIR, 'skill-outcome.jsonl'))
|
||
.filter(r => r.ts >= since && r.skill);
|
||
|
||
// 3. 读取 activity skill 事件(activity-*.jsonl,event === 'skill')
|
||
const activityRows = readGlobJsonl('activity-')
|
||
.filter(r => r.ts >= since && r.event === 'skill' && r.detail);
|
||
|
||
// 4. 汇总每个 Skill 的指标
|
||
const skillMap = {};
|
||
|
||
function getOrCreate(name) {
|
||
if (!skillMap[name]) {
|
||
skillMap[name] = { routeCount: 0, mustInvokeCount: 0, actualInvocations: 0,
|
||
successCount: 0, outcomeCount: 0 };
|
||
}
|
||
return skillMap[name];
|
||
}
|
||
|
||
for (const r of routeRows) {
|
||
const s = getOrCreate(r.skill);
|
||
s.routeCount++;
|
||
if (r.mustInvoke) s.mustInvokeCount++;
|
||
}
|
||
|
||
for (const r of outcomeRows) {
|
||
const s = getOrCreate(r.skill);
|
||
s.outcomeCount++;
|
||
if (r.success) s.successCount++;
|
||
}
|
||
|
||
for (const r of activityRows) {
|
||
getOrCreate(r.detail).actualInvocations++;
|
||
}
|
||
|
||
// 5. 计算派生指标
|
||
const allRouteCounts = Object.values(skillMap).map(s => s.routeCount);
|
||
const maxRouteCount = Math.max(...allRouteCounts, 1); // 避免除零
|
||
|
||
const skills = Object.entries(skillMap).map(([name, s]) => {
|
||
// 合规率:实际调用 / MUST_INVOKE 路由次数(无强制路由时用实际/总路由)
|
||
const complianceDenom = s.mustInvokeCount > 0 ? s.mustInvokeCount : s.routeCount;
|
||
const invocationCompliance = complianceDenom > 0
|
||
? Math.min(s.actualInvocations / complianceDenom, 1)
|
||
: 0;
|
||
|
||
// 成功率:有 outcome 数据才计算,否则用 0.5 作中性值
|
||
const successRate = s.outcomeCount > 0
|
||
? s.successCount / s.outcomeCount
|
||
: 0.5;
|
||
|
||
// 路由量归一化
|
||
const normalizedRoute = s.routeCount / maxRouteCount;
|
||
|
||
// 综合分(满分 100)
|
||
const overall = (invocationCompliance * 0.5 + successRate * 0.3 + normalizedRoute * 0.2) * 100;
|
||
|
||
return {
|
||
name,
|
||
routeCount: s.routeCount,
|
||
mustInvokeCount: s.mustInvokeCount,
|
||
actualInvocations: s.actualInvocations,
|
||
outcomeCount: s.outcomeCount,
|
||
invocationCompliance: parseFloat((invocationCompliance * 100).toFixed(1)),
|
||
successRate: parseFloat((successRate * 100).toFixed(1)),
|
||
overall: parseFloat(overall.toFixed(1)),
|
||
};
|
||
});
|
||
|
||
// 按综合分降序排列
|
||
skills.sort((a, b) => b.overall - a.overall);
|
||
|
||
const summary = {
|
||
days,
|
||
totalRoutes: routeRows.length,
|
||
mustInvokeRoutes: routeRows.filter(r => r.mustInvoke).length,
|
||
actualInvocations: activityRows.length,
|
||
};
|
||
|
||
const result = { skills, summary };
|
||
|
||
// 6. 可选持久化
|
||
if (save) {
|
||
const outPath = path.join(DEBUG_DIR, 'skill-effectiveness-report.json');
|
||
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
||
console.error(`[saved] ${outPath}`);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// ── 人类可读输出 ──────────────────────────────────────────────────────────────
|
||
function printReport({ skills, summary }) {
|
||
const { days, totalRoutes, mustInvokeRoutes, actualInvocations } = summary;
|
||
|
||
console.log('\n═══ Skill Effectiveness Report ═══');
|
||
console.log(`分析窗口: 最近 ${days} 天`);
|
||
console.log(`总路由: ${totalRoutes} | MUST_INVOKE: ${mustInvokeRoutes} | 实际调用: ${actualInvocations}\n`);
|
||
|
||
if (skills.length === 0) {
|
||
console.log('(无数据)');
|
||
return;
|
||
}
|
||
|
||
// 表头
|
||
const hdr = '排名 技能名 路由 强制 实际调用 合规率 成功率 综合分';
|
||
console.log(hdr);
|
||
console.log('─'.repeat(hdr.length));
|
||
|
||
skills.forEach((s, i) => {
|
||
const rank = String(i + 1).padStart(3) + '.';
|
||
const name = s.name.padEnd(30);
|
||
const route = String(s.routeCount).padStart(4);
|
||
const must = String(s.mustInvokeCount).padStart(4);
|
||
const actual = String(s.actualInvocations).padStart(8);
|
||
const comply = `${s.invocationCompliance}%`.padStart(8);
|
||
const success = `${s.successRate}%`.padStart(8);
|
||
const score = String(s.overall).padStart(8);
|
||
console.log(`${rank} ${name}${route} ${must}${actual} ${comply} ${success} ${score}`);
|
||
});
|
||
|
||
console.log('');
|
||
}
|
||
|
||
// ── CLI 入口 ──────────────────────────────────────────────────────────────────
|
||
if (require.main === module) {
|
||
const args = process.argv.slice(2);
|
||
const days = parseInt(args.find((a, i, arr) => arr[i - 1] === '--days') || '7');
|
||
const json = args.includes('--json');
|
||
const save = args.includes('--save');
|
||
const result = analyze({ days, save });
|
||
if (json) console.log(JSON.stringify(result, null, 2));
|
||
else printReport(result);
|
||
}
|
||
|
||
module.exports = { analyze };
|