guanghulab/skyeye/scripts/guard-health.js
2026-05-10 13:12:44 +08:00

167 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
/**
* skyeye/scripts/guard-health.js
* 天眼守卫健康检查脚本 — 逐一验证五个 Guard 进程状态
* Activates and checks all Guard processes: GitHub / Notion / Actions / Gemini / Drive
*/
const fs = require('fs');
const path = require('path');
const SKYEYE_DIR = path.resolve(__dirname, '..');
const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards');
const LOGS_DIR = path.join(SKYEYE_DIR, 'logs');
function getTimestamp() {
return new Date().toISOString();
}
function getDateStr() {
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
function loadJSON(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function saveJSON(filePath, data) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
}
function checkGuardHealth(filePath) {
const guard = loadJSON(filePath);
if (!guard) {
return { status: 'error', reason: 'invalid_json', healthy: false };
}
const result = {
guard_id: guard.guard_id,
target_service: guard.target_service,
status: guard.status,
mode: guard.mode,
healthy: false,
checks: {}
};
// Check 1: Status is active
result.checks.status_active = guard.status === 'active';
// Check 2: Mode is not suspended
result.checks.mode_operational = guard.mode !== 'suspended';
// Check 3: Required fields present
const requiredFields = ['guard_id', 'status', 'mode', 'quota_policy', 'health_check'];
const missing = requiredFields.filter(f => !(f in guard));
result.checks.fields_complete = missing.length === 0;
if (missing.length > 0) {
result.checks.missing_fields = missing;
}
// Check 4: Consecutive failures below threshold
const failures = guard.health_check?.consecutive_failures || 0;
const maxFailures = guard.health_check?.max_failures_before_alert || 3;
result.checks.failures_within_limit = failures < maxFailures;
result.checks.consecutive_failures = failures;
// Check 5: Last heartbeat freshness (if available)
if (guard.health_check?.last_check) {
const lastCheckTime = new Date(guard.health_check.last_check).getTime();
if (!isNaN(lastCheckTime)) {
const lastCheckAge = Date.now() - lastCheckTime;
const intervalMs = (guard.health_check.interval_hours || 6) * 60 * 60 * 1000;
result.checks.heartbeat_fresh = lastCheckAge < intervalMs * 3; // 3x tolerance
} else {
result.checks.heartbeat_fresh = null; // Invalid date
}
} else {
result.checks.heartbeat_fresh = null; // No heartbeat recorded yet
}
// Overall health determination
result.healthy = result.checks.status_active
&& result.checks.mode_operational
&& result.checks.fields_complete
&& result.checks.failures_within_limit;
return result;
}
function run() {
console.log(`[SkyEye Guard Health] Starting guard health check`);
console.log(`[SkyEye Guard Health] Timestamp: ${getTimestamp()}`);
if (!fs.existsSync(GUARDS_DIR)) {
console.error('[SkyEye Guard Health] ERROR: guards/ directory not found');
process.exit(1);
}
const files = fs.readdirSync(GUARDS_DIR)
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
const healthResult = {
check_id: `GUARD-HEALTH-${getDateStr()}`,
timestamp: getTimestamp(),
total: files.length,
healthy: 0,
unhealthy: 0,
guards: [],
issues: []
};
for (const file of files) {
const filePath = path.join(GUARDS_DIR, file);
const health = checkGuardHealth(filePath);
health.file = file;
healthResult.guards.push(health);
if (health.healthy) {
healthResult.healthy++;
console.log(`${health.guard_id || file}: healthy`);
} else {
healthResult.unhealthy++;
const reason = !health.checks.status_active ? 'inactive'
: !health.checks.mode_operational ? 'suspended'
: !health.checks.fields_complete ? 'incomplete_config'
: !health.checks.failures_within_limit ? 'too_many_failures'
: 'unknown';
healthResult.issues.push({
guard: health.guard_id || file,
file,
reason,
detail: health.checks
});
console.log(` 🔴 ${health.guard_id || file}: ${reason}`);
}
}
healthResult.summary = {
status: healthResult.unhealthy === 0 ? 'all_healthy'
: healthResult.healthy >= 3 ? 'mostly_healthy'
: 'degraded',
healthy_ratio: `${healthResult.healthy}/${healthResult.total}`,
issues_count: healthResult.issues.length
};
// Write log
const logDir = path.join(LOGS_DIR, 'daily');
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
const logPath = path.join(logDir, `guard-health-${getDateStr()}.json`);
saveJSON(logPath, healthResult);
console.log(`[SkyEye Guard Health] Result: ${healthResult.summary.status}`);
console.log(`[SkyEye Guard Health] Healthy: ${healthResult.summary.healthy_ratio}`);
console.log(`[SkyEye Guard Health] Issues: ${healthResult.summary.issues_count}`);
console.log(`[SkyEye Guard Health] Log saved: ${logPath}`);
console.log('---GUARD_HEALTH_JSON---');
console.log(JSON.stringify(healthResult, null, 2));
return healthResult;
}
run();