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