bookworm-smart-assistant/scripts/daily-health-snapshot.js

204 lines
7.3 KiB
JavaScript
Raw Permalink Normal View History

#!/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 {}
// [v6.6] 可选: 记忆文件体检 (memory/_tools/memory-audit.js)
// sentinel: MEMORY_AUDIT_SNAPSHOT_2026_04_25
// 运行 memory-audit --json保存快照到 health-snapshots/memory-audit-<date>.json
// 若 orphan/ghost > 0 或 health.score < 80追加告警到 evolution-log
try {
const memAuditScript = path.join(
CLAUDE_ROOT, 'projects', 'C--Users-leesu', 'memory', '_tools', 'memory-audit.js'
);
if (fs.existsSync(memAuditScript)) {
const memResult = execFileSync(process.execPath, [memAuditScript, '--json'], {
timeout: 10000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
});
const memReport = JSON.parse(memResult);
fs.writeFileSync(
path.join(SNAPSHOT_DIR, `memory-audit-${dateStr}.json`),
JSON.stringify(memReport, null, 2)
);
// 告警: orphan/ghost 或 分数偏低
const h = memReport.health || {};
const needAlert = (h.orphanCount > 0) || (h.ghostCount > 0) || (h.score < 80);
if (needAlert) {
const logFile = path.join(CLAUDE_ROOT, 'evolution-log.jsonl');
const existing = fs.existsSync(logFile)
? fs.readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean) : [];
const lastSeq = existing.length
? (JSON.parse(existing[existing.length - 1]).seq || 0) : 0;
const memEntry = {
seq: lastSeq + 1,
ts: dateStr,
version: 'v6.6',
scope: 'memory-audit',
trigger: 'daily-health-snapshot',
summary: `记忆健康 ${h.score}/100 — orphan=${h.orphanCount} ghost=${h.ghostCount} oversize=${h.oversizeCount}`,
tags: ['memory-alert', 'auto-snapshot'],
};
fs.appendFileSync(logFile, JSON.stringify(memEntry) + '\n');
}
}
} 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();
}