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();
|
||
}
|