bookworm-smart-assistant/scripts/skill-effectiveness.js

191 lines
7.1 KiB
JavaScript
Raw Permalink Normal View History

/* 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-*.jsonlevent === '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 };