241 lines
7.1 KiB
JavaScript
241 lines
7.1 KiB
JavaScript
#!/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'
|
|
];
|
|
|
|
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;
|
|
}
|
|
|
|
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(),
|
|
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
|
|
};
|
|
|
|
// 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] Log saved: ${logPath}`);
|
|
|
|
console.log('---HEAL_RESULT_JSON---');
|
|
console.log(JSON.stringify(healResult, null, 2));
|
|
|
|
return healResult;
|
|
}
|
|
|
|
run();
|