guanghulab/skyeye/scripts/self-healer.js

276 lines
8.4 KiB
JavaScript
Raw Permalink Normal View History

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