276 lines
8.4 KiB
JavaScript
276 lines
8.4 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',
|
||
'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();
|