167 lines
5.1 KiB
JavaScript
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();
|