2026-04-21 17:57:05 +08:00
|
|
|
|
#!/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 {}
|
|
|
|
|
|
|
2026-04-27 17:59:44 +08:00
|
|
|
|
|
|
|
|
|
|
// [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 {}
|
|
|
|
|
|
|
2026-04-21 17:57:05 +08:00
|
|
|
|
// 若 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();
|
|
|
|
|
|
}
|