bookworm-smart-assistant/scripts/daily-health-snapshot.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

204 lines
7.3 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.

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