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

276 lines
8.4 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
/**
* skyeye/scripts/self-healer.js
* 天眼自愈引擎 — 自动修复损坏配置、清理过期数据
* Phase 4 of weekly scan: Self-Heal
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const SKYEYE_DIR = path.resolve(__dirname, '..');
const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards');
const TEMPLATE_PATH = path.join(GUARDS_DIR, 'guard-template.json');
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 repairGuardConfigs() {
const results = { repaired: [], failed: [] };
const template = loadJSON(TEMPLATE_PATH);
if (!template) {
results.failed.push({ file: 'guard-template.json', reason: 'Template missing or invalid' });
return results;
}
const requiredFields = ['guard_id', 'status', 'mode', 'buffer_policy', 'quota_policy', 'trigger_policy', 'health_check'];
const files = fs.readdirSync(GUARDS_DIR)
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
for (const file of files) {
const filePath = path.join(GUARDS_DIR, file);
let guard = loadJSON(filePath);
if (!guard) {
// File is corrupted - rebuild from template
console.log(` [REPAIR] ${file}: Corrupted JSON, rebuilding from template`);
guard = { ...template };
guard.guard_id = `GUARD-${file.replace('-guard.json', '').toUpperCase()}`;
guard.status = 'repaired';
guard.last_updated_by = 'skyeye-self-healer';
guard.last_updated_at = getTimestamp();
saveJSON(filePath, guard);
results.repaired.push({ file, action: 'rebuilt_from_template' });
continue;
}
// Check for missing fields and fill from template
let modified = false;
for (const field of requiredFields) {
if (!(field in guard)) {
guard[field] = template[field];
modified = true;
console.log(` [REPAIR] ${file}: Added missing field '${field}'`);
}
}
if (modified) {
guard.last_updated_by = 'skyeye-self-healer';
guard.last_updated_at = getTimestamp();
saveJSON(filePath, guard);
results.repaired.push({ file, action: 'filled_missing_fields' });
}
}
return results;
}
function cleanExpiredBuffers() {
const results = { files_cleaned: 0, directories: [] };
const processedDir = path.join(ROOT, 'buffer/processed');
if (!fs.existsSync(processedDir)) return results;
const maxAgeDays = 7;
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
try {
const files = fs.readdirSync(processedDir);
for (const file of files) {
const filePath = path.join(processedDir, file);
const stat = fs.statSync(filePath);
if (stat.mtimeMs < cutoff) {
if (stat.isFile()) {
fs.unlinkSync(filePath);
results.files_cleaned++;
}
}
}
results.directories.push({ path: 'buffer/processed', cleaned: results.files_cleaned });
} catch (e) {
// Ignore errors during cleanup
}
return results;
}
function cleanExpiredLogs() {
const results = { files_cleaned: 0 };
const maxAgeDays = 30;
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
const logDirs = [
path.join(LOGS_DIR, 'daily'),
path.join(LOGS_DIR, 'weekly')
];
for (const dir of logDirs) {
if (!fs.existsSync(dir)) continue;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (file === '.gitkeep') continue;
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.mtimeMs < cutoff && stat.isFile()) {
fs.unlinkSync(filePath);
results.files_cleaned++;
}
}
} catch (e) {
// Ignore errors during cleanup
}
}
return results;
}
function restartSuspendedGuards() {
const results = { restarted: [] };
const files = fs.readdirSync(GUARDS_DIR)
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
for (const file of files) {
const filePath = path.join(GUARDS_DIR, file);
const guard = loadJSON(filePath);
if (!guard) continue;
// Restart suspended guards with healthy checks
if (guard.mode === 'suspended' && guard.health_check) {
// Reset health check status for retry
guard.health_check.consecutive_failures = 0;
guard.health_check.last_status = 'pending_restart';
guard.mode = 'buffer';
guard.status = 'active';
guard.last_updated_by = 'skyeye-self-healer';
guard.last_updated_at = getTimestamp();
saveJSON(filePath, guard);
results.restarted.push({ file, guard_id: guard.guard_id });
console.log(` [RESTART] ${file}: Guard restarted`);
}
}
return results;
}
function ensureDirectories() {
const results = { created: [] };
const requiredDirs = [
'skyeye/scan-report',
'skyeye/logs/daily',
'skyeye/logs/weekly',
'buffer/inbox',
'buffer/staging',
'buffer/processed',
'System_Logs'
];
for (const dir of requiredDirs) {
const fullPath = path.join(ROOT, dir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
results.created.push(dir);
console.log(` [CREATE] Directory: ${dir}`);
}
}
return results;
}
/**
* 天眼 OAuth2 凭据校验 — 验证 GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
* 仅在环境变量可用时执行CI 环境)
*/
function validateCredentials() {
const result = { status: 'skipped', issues: [] };
const hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
if (!hasAnyVar) {
result.reason = 'OAuth2 credentials not available (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)';
return result;
}
try {
const { validateOAuth2Credentials } = require('../../scripts/skyeye/credential-validator');
const validation = validateOAuth2Credentials();
result.status = validation.valid ? 'pass' : 'fail';
result.issues = validation.issues;
if (!validation.valid) {
console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`);
} else {
console.log(' [CREDENTIAL] ✅ OAuth2 credentials valid');
}
} catch (e) {
result.status = 'error';
result.reason = `Validator load error: ${e.message}`;
}
return result;
}
function run() {
console.log(`[SkyEye Self-Healer] Starting self-heal process`);
console.log(`[SkyEye Self-Healer] Timestamp: ${getTimestamp()}`);
const healResult = {
heal_id: `HEAL-${getDateStr()}`,
timestamp: getTimestamp(),
guard_repairs: repairGuardConfigs(),
buffer_cleanup: cleanExpiredBuffers(),
log_cleanup: cleanExpiredLogs(),
guard_restarts: restartSuspendedGuards(),
directory_fixes: ensureDirectories(),
credential_check: validateCredentials(),
summary: {}
};
healResult.summary = {
configs_repaired: healResult.guard_repairs.repaired.length,
files_cleaned: healResult.buffer_cleanup.files_cleaned + healResult.log_cleanup.files_cleaned,
guards_restarted: healResult.guard_restarts.restarted.length,
directories_created: healResult.directory_fixes.created.length,
credential_status: healResult.credential_check.status
};
// Write log
const logDir = path.join(LOGS_DIR, 'weekly');
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
const logPath = path.join(logDir, `self-heal-${getDateStr()}.json`);
saveJSON(logPath, healResult);
console.log(`[SkyEye Self-Healer] Repairs: ${healResult.summary.configs_repaired}`);
console.log(`[SkyEye Self-Healer] Files cleaned: ${healResult.summary.files_cleaned}`);
console.log(`[SkyEye Self-Healer] Guards restarted: ${healResult.summary.guards_restarted}`);
console.log(`[SkyEye Self-Healer] Dirs created: ${healResult.summary.directories_created}`);
console.log(`[SkyEye Self-Healer] Credential status: ${healResult.summary.credential_status}`);
console.log(`[SkyEye Self-Healer] Log saved: ${logPath}`);
console.log('---HEAL_RESULT_JSON---');
console.log(JSON.stringify(healResult, null, 2));
return healResult;
}
run();