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

191 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 };