162 lines
5.4 KiB
JavaScript
162 lines
5.4 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* 每日健康快照 (Daily Health Snapshot)
|
|||
|
|
*
|
|||
|
|
* 由 Stop hook 触发,23h 冷却机制。
|
|||
|
|
* 运行 health-check --json,保存快照到 debug/health-snapshots/
|
|||
|
|
* 若 overall < 70,追加告警到 evolution-log.jsonl
|
|||
|
|
*
|
|||
|
|
* 用法:
|
|||
|
|
* node scripts/daily-health-snapshot.js # 正常运行 (23h 冷却)
|
|||
|
|
* node scripts/daily-health-snapshot.js --force # 强制运行,忽略冷却
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const { execFileSync } = require('child_process');
|
|||
|
|
|
|||
|
|
const detectClaudeRoot = () => require('./paths.config.js').PATHS.root;
|
|||
|
|
|
|||
|
|
const CLAUDE_ROOT = detectClaudeRoot();
|
|||
|
|
const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug');
|
|||
|
|
const SNAPSHOT_DIR = path.join(DEBUG_DIR, 'health-snapshots');
|
|||
|
|
const STAMP_FILE = path.join(DEBUG_DIR, '.last-health-snapshot');
|
|||
|
|
const COOLDOWN_MS = 23 * 3600 * 1000; // 23 小时冷却
|
|||
|
|
const PREDICT_STAMP = path.join(DEBUG_DIR, '.last-predictive-audit');
|
|||
|
|
const PREDICT_COOLDOWN_MS = 7 * 24 * 3600 * 1000; // 7 天冷却
|
|||
|
|
/**
|
|||
|
|
* @param {object} [opts]
|
|||
|
|
* @param {boolean} [opts.force] - 强制运行,忽略冷却
|
|||
|
|
*/
|
|||
|
|
function main(opts) {
|
|||
|
|
const force = (opts && opts.force) || process.argv.includes('--force');
|
|||
|
|
try {
|
|||
|
|
// 冷却检查
|
|||
|
|
if (!force && fs.existsSync(STAMP_FILE)) {
|
|||
|
|
const lastRun = fs.statSync(STAMP_FILE).mtimeMs;
|
|||
|
|
if (Date.now() - lastRun < COOLDOWN_MS) {
|
|||
|
|
return; // 静默跳过
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 运行 health-check --json
|
|||
|
|
const healthScript = path.join(CLAUDE_ROOT, 'scripts', 'health-check.js');
|
|||
|
|
if (!fs.existsSync(healthScript)) return;
|
|||
|
|
|
|||
|
|
const result = execFileSync(process.execPath, [healthScript, '--json'], {
|
|||
|
|
timeout: 30000,
|
|||
|
|
encoding: 'utf8',
|
|||
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let report;
|
|||
|
|
try { report = JSON.parse(result); } catch { return; }
|
|||
|
|
|
|||
|
|
// 保存快照
|
|||
|
|
fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
|
|||
|
|
const dateStr = new Date().toISOString().slice(0, 10);
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(SNAPSHOT_DIR, `health-${dateStr}.json`),
|
|||
|
|
JSON.stringify(report, null, 2)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 更新冷却戳
|
|||
|
|
fs.mkdirSync(DEBUG_DIR, { recursive: true });
|
|||
|
|
fs.writeFileSync(STAMP_FILE, dateStr);
|
|||
|
|
|
|||
|
|
// F2: 低频触发 predictive-audit (7天冷却)
|
|||
|
|
try {
|
|||
|
|
let runPredict = force;
|
|||
|
|
if (!runPredict) {
|
|||
|
|
if (!fs.existsSync(PREDICT_STAMP)) {
|
|||
|
|
runPredict = true;
|
|||
|
|
} else {
|
|||
|
|
const lastPredict = fs.statSync(PREDICT_STAMP).mtimeMs;
|
|||
|
|
if (Date.now() - lastPredict >= PREDICT_COOLDOWN_MS) runPredict = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (runPredict) {
|
|||
|
|
const predictScript = path.join(CLAUDE_ROOT, 'scripts', 'predictive-audit.js');
|
|||
|
|
if (fs.existsSync(predictScript)) {
|
|||
|
|
const predictResult = execFileSync(process.execPath, [predictScript, '--json'], {
|
|||
|
|
timeout: 15000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|||
|
|
});
|
|||
|
|
const predictReport = JSON.parse(predictResult);
|
|||
|
|
// 保存预测报告到健康快照目录
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(SNAPSHOT_DIR, `predict-${dateStr}.json`),
|
|||
|
|
JSON.stringify(predictReport, null, 2)
|
|||
|
|
);
|
|||
|
|
fs.writeFileSync(PREDICT_STAMP, dateStr);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// [v6.1] 可选: 域容量分析 (domain-capacity-manager CLI 工具)
|
|||
|
|
// 在健康快照中附加域容量报告,不阻断主流程
|
|||
|
|
try {
|
|||
|
|
const dcm = require('./domain-capacity-manager.js');
|
|||
|
|
if (dcm && typeof dcm.analyze === 'function') {
|
|||
|
|
const dcmReport = dcm.analyze();
|
|||
|
|
if (dcmReport) {
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(SNAPSHOT_DIR, `domain-capacity-${dateStr}.json`),
|
|||
|
|
JSON.stringify(dcmReport, null, 2)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// [v6.1] 可选: 技能退役建议 (skill-retirement-advisor CLI 工具)
|
|||
|
|
// 在健康快照中附加退役建议报告,不阻断主流程
|
|||
|
|
try {
|
|||
|
|
const sra = require('./skill-retirement-advisor.js');
|
|||
|
|
if (sra && typeof sra.analyze === 'function') {
|
|||
|
|
const sraReport = sra.analyze();
|
|||
|
|
if (sraReport) {
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(SNAPSHOT_DIR, `skill-retirement-${dateStr}.json`),
|
|||
|
|
JSON.stringify(sraReport, null, 2)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
// 若 overall < 70,追加告警到 evolution-log
|
|||
|
|
if (report.overallScore != null && report.overallScore < 70) {
|
|||
|
|
const failedDims = (report.dimensions || [])
|
|||
|
|
.filter(d => d.status === 'FAIL')
|
|||
|
|
.map(d => d.id)
|
|||
|
|
.join(',');
|
|||
|
|
|
|||
|
|
const logFile = path.join(CLAUDE_ROOT, 'evolution-log.jsonl');
|
|||
|
|
const lines = fs.existsSync(logFile)
|
|||
|
|
? fs.readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean) : [];
|
|||
|
|
const lastSeq = lines.length > 0
|
|||
|
|
? (JSON.parse(lines[lines.length - 1]).seq || 0) : 0;
|
|||
|
|
|
|||
|
|
const entry = {
|
|||
|
|
seq: lastSeq + 1,
|
|||
|
|
ts: dateStr,
|
|||
|
|
version: 'v5.9',
|
|||
|
|
scope: 'health-snapshot',
|
|||
|
|
trigger: 'daily-health-snapshot',
|
|||
|
|
summary: `健康评分 ${report.overallScore}/100 (${report.overallStatus || 'DEGRADED'}) — 失败维度: ${failedDims || '无'}`,
|
|||
|
|
tags: ['health-alert', 'auto-snapshot'],
|
|||
|
|
};
|
|||
|
|
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// 不阻断 Stop hook
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模块导出 (供 dispatcher 和测试使用)
|
|||
|
|
if (typeof module !== 'undefined') {
|
|||
|
|
module.exports = { main };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
main();
|
|||
|
|
}
|