feat(skyeye): implement 6 skyeye/scripts/ operational scripts
Add complete implementations for: - scan-engine.js: Infrastructure scan engine (full/incremental) - quota-tracker.js: Quota audit and budget tracking - optimizer.js: Dynamic guard frequency optimization - guard-factory.js: Guard config factory from template - self-healer.js: Self-healing engine for config repair/cleanup - weekly-scan.js: Weekly scan report orchestrator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
9dc94a7b3a
commit
a000e0d6bc
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* skyeye/scripts/guard-factory.js
|
||||
* 天眼 Guard 工厂 — 从模板自动生成新 Guard 配置
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SKYEYE_DIR = path.resolve(__dirname, '..');
|
||||
const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards');
|
||||
const TEMPLATE_PATH = path.join(GUARDS_DIR, 'guard-template.json');
|
||||
|
||||
function getTimestamp() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
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 createGuard(serviceName, options = {}) {
|
||||
const template = loadJSON(TEMPLATE_PATH);
|
||||
if (!template) {
|
||||
console.error('[Guard Factory] ERROR: Cannot load guard-template.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const guardId = `GUARD-${serviceName.toUpperCase().replace(/\s+/g, '-')}`;
|
||||
const fileName = `${serviceName.toLowerCase().replace(/\s+/g, '-')}-guard.json`;
|
||||
const outputPath = path.join(GUARDS_DIR, fileName);
|
||||
|
||||
if (fs.existsSync(outputPath)) {
|
||||
console.warn(`[Guard Factory] WARNING: ${fileName} already exists. Use --force to overwrite.`);
|
||||
if (!options.force) return null;
|
||||
}
|
||||
|
||||
const guard = {
|
||||
...template,
|
||||
guard_id: guardId,
|
||||
target_service: options.targetService || serviceName,
|
||||
status: 'initializing',
|
||||
mode: options.mode || 'buffer',
|
||||
buffer_policy: {
|
||||
...template.buffer_policy,
|
||||
buffer_path: options.bufferPath || `buffer/${serviceName.toLowerCase()}-staging/`,
|
||||
},
|
||||
quota_policy: {
|
||||
...template.quota_policy,
|
||||
monthly_limit: options.monthlyLimit || template.quota_policy.monthly_limit,
|
||||
},
|
||||
health_check: {
|
||||
...template.health_check,
|
||||
method: options.healthCheckMethod || 'api_ping',
|
||||
},
|
||||
last_updated_by: 'skyeye-guard-factory',
|
||||
last_updated_at: getTimestamp()
|
||||
};
|
||||
|
||||
saveJSON(outputPath, guard);
|
||||
console.log(`[Guard Factory] Created: ${fileName}`);
|
||||
console.log(`[Guard Factory] Guard ID: ${guardId}`);
|
||||
console.log(`[Guard Factory] Status: initializing`);
|
||||
|
||||
return { fileName, guard };
|
||||
}
|
||||
|
||||
function listGuards() {
|
||||
const files = fs.readdirSync(GUARDS_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
|
||||
|
||||
console.log(`[Guard Factory] Current guards (${files.length}):`);
|
||||
for (const file of files) {
|
||||
const guard = loadJSON(path.join(GUARDS_DIR, file));
|
||||
if (guard) {
|
||||
console.log(` ${guard.guard_id}: ${guard.target_service} [${guard.status}] (${guard.mode})`);
|
||||
} else {
|
||||
console.log(` ${file}: [INVALID JSON]`);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--list')) {
|
||||
listGuards();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes('--create')) {
|
||||
const nameIdx = args.indexOf('--create') + 1;
|
||||
if (nameIdx >= args.length) {
|
||||
console.error('[Guard Factory] Usage: --create <service_name> [--force]');
|
||||
process.exit(1);
|
||||
}
|
||||
const serviceName = args[nameIdx];
|
||||
const force = args.includes('--force');
|
||||
createGuard(serviceName, { force });
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: list existing guards
|
||||
console.log('[Guard Factory] 天眼 Guard 工厂');
|
||||
console.log('Usage:');
|
||||
console.log(' --list List all existing guards');
|
||||
console.log(' --create <name> Create a new guard from template');
|
||||
console.log(' --force Overwrite existing guard');
|
||||
listGuards();
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* skyeye/scripts/optimizer.js
|
||||
* 天眼动态调优引擎 — 基于配额使用情况自动调整 Guard 触发频率
|
||||
* Phase 3 of weekly scan: Optimize Guards
|
||||
*/
|
||||
|
||||
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 LEDGER_PATH = path.join(SKYEYE_DIR, 'quota-ledger.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 calculateUsagePercent(guard) {
|
||||
const qp = guard.quota_policy;
|
||||
if (!qp || !qp.monthly_limit || qp.monthly_limit === 0) return 0;
|
||||
return Math.round(((qp.current_month_used || 0) / qp.monthly_limit) * 100 * 10) / 10;
|
||||
}
|
||||
|
||||
const OPTIMIZATION_RULES = [
|
||||
{
|
||||
name: 'quota_warning_reduce_frequency',
|
||||
condition: (guard) => {
|
||||
const pct = calculateUsagePercent(guard);
|
||||
return pct > 70 && pct <= 90;
|
||||
},
|
||||
action: (guard) => {
|
||||
const prev = guard.trigger_policy.default_frequency;
|
||||
guard.trigger_policy.default_frequency = guard.trigger_policy.min_frequency;
|
||||
return {
|
||||
rule: 'quota_warning_reduce_frequency',
|
||||
guard_id: guard.guard_id,
|
||||
action: 'reduce_frequency',
|
||||
detail: `配额超 70%,频率从 ${prev} 降至 ${guard.trigger_policy.min_frequency}`,
|
||||
previous: prev,
|
||||
current: guard.trigger_policy.min_frequency
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'quota_critical_emergency_only',
|
||||
condition: (guard) => {
|
||||
const pct = calculateUsagePercent(guard);
|
||||
return pct > 90;
|
||||
},
|
||||
action: (guard) => {
|
||||
const prevMode = guard.mode;
|
||||
guard.mode = 'emergency_only';
|
||||
return {
|
||||
rule: 'quota_critical_emergency_only',
|
||||
guard_id: guard.guard_id,
|
||||
action: 'switch_to_emergency',
|
||||
detail: `配额超 90%,切换至仅紧急模式`,
|
||||
previous_mode: prevMode,
|
||||
current_mode: 'emergency_only'
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'health_check_failures_suspend',
|
||||
condition: (guard) => {
|
||||
return guard.health_check && guard.health_check.consecutive_failures >= 3;
|
||||
},
|
||||
action: (guard) => {
|
||||
const prevMode = guard.mode;
|
||||
guard.mode = 'suspended';
|
||||
return {
|
||||
rule: 'health_check_failures_suspend',
|
||||
guard_id: guard.guard_id,
|
||||
action: 'suspend',
|
||||
detail: `连续 ${guard.health_check.consecutive_failures} 次健康检查失败,已暂停`,
|
||||
previous_mode: prevMode,
|
||||
current_mode: 'suspended'
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'backlog_increase_frequency',
|
||||
condition: (guard) => {
|
||||
const pct = calculateUsagePercent(guard);
|
||||
const pending = guard.buffer_policy && guard.buffer_policy.pending_count;
|
||||
return pending > 100 && pct < 50;
|
||||
},
|
||||
action: (guard) => {
|
||||
const prev = guard.trigger_policy.default_frequency;
|
||||
guard.trigger_policy.default_frequency = guard.trigger_policy.max_frequency;
|
||||
return {
|
||||
rule: 'backlog_increase_frequency',
|
||||
guard_id: guard.guard_id,
|
||||
action: 'increase_frequency',
|
||||
detail: `消息积压 ${guard.buffer_policy.pending_count} 条,配额充裕,已加频`,
|
||||
previous: prev,
|
||||
current: guard.trigger_policy.max_frequency
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function optimizeGuard(guard) {
|
||||
const adjustments = [];
|
||||
|
||||
for (const rule of OPTIMIZATION_RULES) {
|
||||
if (rule.condition(guard)) {
|
||||
const adjustment = rule.action(guard);
|
||||
adjustments.push(adjustment);
|
||||
// Critical/emergency rules take priority - stop processing
|
||||
if (rule.name === 'quota_critical_emergency_only' || rule.name === 'health_check_failures_suspend') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adjustments.length > 0) {
|
||||
guard.last_updated_by = 'skyeye-optimizer';
|
||||
guard.last_updated_at = getTimestamp();
|
||||
}
|
||||
|
||||
return adjustments;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const args = process.argv.slice(2);
|
||||
const applyChanges = args.includes('--apply');
|
||||
|
||||
console.log(`[SkyEye Optimizer] Mode: ${applyChanges ? 'apply' : 'dry-run'}`);
|
||||
console.log(`[SkyEye Optimizer] Timestamp: ${getTimestamp()}`);
|
||||
|
||||
const optimizeResult = {
|
||||
optimize_id: `OPT-${getDateStr()}`,
|
||||
timestamp: getTimestamp(),
|
||||
mode: applyChanges ? 'apply' : 'dry-run',
|
||||
guards_checked: 0,
|
||||
adjustments: [],
|
||||
summary: {}
|
||||
};
|
||||
|
||||
const guardFiles = fs.readdirSync(GUARDS_DIR)
|
||||
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
|
||||
|
||||
for (const file of guardFiles) {
|
||||
const filePath = path.join(GUARDS_DIR, file);
|
||||
const guard = loadJSON(filePath);
|
||||
if (!guard) {
|
||||
console.warn(`[SkyEye Optimizer] WARNING: Cannot load ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
optimizeResult.guards_checked++;
|
||||
const adjustments = optimizeGuard(guard);
|
||||
|
||||
if (adjustments.length > 0) {
|
||||
optimizeResult.adjustments.push(...adjustments);
|
||||
if (applyChanges) {
|
||||
saveJSON(filePath, guard);
|
||||
console.log(` [APPLIED] ${file}: ${adjustments.length} adjustment(s)`);
|
||||
} else {
|
||||
console.log(` [DRY-RUN] ${file}: ${adjustments.length} adjustment(s) would be applied`);
|
||||
}
|
||||
} else {
|
||||
console.log(` [OK] ${file}: no adjustments needed`);
|
||||
}
|
||||
}
|
||||
|
||||
optimizeResult.summary = {
|
||||
guards_checked: optimizeResult.guards_checked,
|
||||
total_adjustments: optimizeResult.adjustments.length,
|
||||
applied: applyChanges
|
||||
};
|
||||
|
||||
// Write log
|
||||
const logDir = path.join(LOGS_DIR, 'weekly');
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
||||
const logPath = path.join(logDir, `optimize-${getDateStr()}.json`);
|
||||
saveJSON(logPath, optimizeResult);
|
||||
|
||||
console.log(`[SkyEye Optimizer] Guards checked: ${optimizeResult.guards_checked}`);
|
||||
console.log(`[SkyEye Optimizer] Adjustments: ${optimizeResult.adjustments.length}`);
|
||||
console.log(`[SkyEye Optimizer] Log saved: ${logPath}`);
|
||||
|
||||
console.log('---OPTIMIZE_RESULT_JSON---');
|
||||
console.log(JSON.stringify(optimizeResult, null, 2));
|
||||
|
||||
return optimizeResult;
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* skyeye/scripts/quota-tracker.js
|
||||
* 天眼配额追踪器 — 配额审计与预算精算
|
||||
* Phase 2 of weekly scan: Quota Audit
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const SKYEYE_DIR = path.resolve(__dirname, '..');
|
||||
const LEDGER_PATH = path.join(SKYEYE_DIR, 'quota-ledger.json');
|
||||
const MANIFEST_PATH = path.join(SKYEYE_DIR, 'infra-manifest.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 calculateUsagePercent(used, limit) {
|
||||
if (!limit || limit === 0) return 0;
|
||||
return Math.round((used / limit) * 100 * 10) / 10;
|
||||
}
|
||||
|
||||
function getDaysRemainingInMonth() {
|
||||
const now = new Date();
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
return lastDay.getDate() - now.getDate();
|
||||
}
|
||||
|
||||
function auditService(name, serviceData, manifest) {
|
||||
const result = {
|
||||
service: name,
|
||||
status: 'healthy',
|
||||
usage_percent: 0,
|
||||
alerts: [],
|
||||
recommendations: []
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case 'github_actions': {
|
||||
const pct = calculateUsagePercent(serviceData.used, serviceData.limit);
|
||||
result.usage_percent = pct;
|
||||
const daysLeft = getDaysRemainingInMonth();
|
||||
const dailyBudget = daysLeft > 0 ? Math.round(serviceData.remaining / daysLeft) : 0;
|
||||
|
||||
if (pct > 90) {
|
||||
result.status = 'critical';
|
||||
result.alerts.push('GitHub Actions 配额使用超过 90%,仅允许紧急操作');
|
||||
} else if (pct > 70) {
|
||||
result.status = 'warning';
|
||||
result.alerts.push('GitHub Actions 配额使用超过 70%,建议降低触发频率');
|
||||
}
|
||||
|
||||
result.recommendations.push(`日均可用预算: ${dailyBudget} 分钟 (剩余 ${daysLeft} 天)`);
|
||||
break;
|
||||
}
|
||||
case 'google_drive': {
|
||||
const pct = calculateUsagePercent(serviceData.storage_used_gb, serviceData.storage_limit_gb);
|
||||
result.usage_percent = pct;
|
||||
|
||||
if (pct > 80) {
|
||||
result.status = 'warning';
|
||||
result.alerts.push('Google Drive 存储使用超过 80%,建议清理旧文件');
|
||||
}
|
||||
|
||||
if (serviceData.apps_script_triggers_today >= serviceData.apps_script_triggers_limit * 0.9) {
|
||||
result.alerts.push('Apps Script 每日触发数接近上限');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'notion_api': {
|
||||
if (serviceData.errors_today > 10) {
|
||||
result.status = 'warning';
|
||||
result.alerts.push(`Notion API 今日错误数较高: ${serviceData.errors_today}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gemini': {
|
||||
const pct = calculateUsagePercent(serviceData.daily_used, serviceData.daily_limit);
|
||||
result.usage_percent = pct;
|
||||
|
||||
if (pct > 90) {
|
||||
result.status = 'critical';
|
||||
result.alerts.push('Gemini 每日配额即将耗尽');
|
||||
} else if (pct > 70) {
|
||||
result.status = 'warning';
|
||||
result.alerts.push('Gemini 每日配额使用超过 70%');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const args = process.argv.slice(2);
|
||||
const isAudit = args.includes('--audit');
|
||||
|
||||
console.log(`[SkyEye Quota Tracker] Mode: ${isAudit ? 'full audit' : 'quick check'}`);
|
||||
console.log(`[SkyEye Quota Tracker] Timestamp: ${getTimestamp()}`);
|
||||
|
||||
const ledger = loadJSON(LEDGER_PATH);
|
||||
if (!ledger) {
|
||||
console.error('[SkyEye Quota Tracker] ERROR: Cannot load quota-ledger.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = loadJSON(MANIFEST_PATH);
|
||||
|
||||
const auditResult = {
|
||||
audit_id: `AUDIT-${getDateStr()}`,
|
||||
timestamp: getTimestamp(),
|
||||
mode: isAudit ? 'full' : 'quick',
|
||||
services: {},
|
||||
overall_status: 'healthy',
|
||||
total_alerts: 0,
|
||||
recommendations: []
|
||||
};
|
||||
|
||||
// Audit each service
|
||||
for (const [name, data] of Object.entries(ledger.services)) {
|
||||
const serviceAudit = auditService(name, data, manifest);
|
||||
auditResult.services[name] = serviceAudit;
|
||||
|
||||
if (serviceAudit.status === 'critical') {
|
||||
auditResult.overall_status = 'critical';
|
||||
} else if (serviceAudit.status === 'warning' && auditResult.overall_status !== 'critical') {
|
||||
auditResult.overall_status = 'warning';
|
||||
}
|
||||
|
||||
auditResult.total_alerts += serviceAudit.alerts.length;
|
||||
auditResult.recommendations.push(...serviceAudit.recommendations);
|
||||
}
|
||||
|
||||
// Update ledger health summary
|
||||
ledger.health_summary.overall = auditResult.overall_status;
|
||||
ledger.health_summary.alerts = [];
|
||||
for (const [, svc] of Object.entries(auditResult.services)) {
|
||||
ledger.health_summary.alerts.push(...svc.alerts);
|
||||
}
|
||||
ledger.health_summary.last_updated = getTimestamp();
|
||||
saveJSON(LEDGER_PATH, ledger);
|
||||
|
||||
// Write audit log
|
||||
const logDir = path.join(LOGS_DIR, isAudit ? 'weekly' : 'daily');
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
||||
const logPath = path.join(logDir, `quota-audit-${getDateStr()}.json`);
|
||||
saveJSON(logPath, auditResult);
|
||||
|
||||
console.log(`[SkyEye Quota Tracker] Overall status: ${auditResult.overall_status}`);
|
||||
console.log(`[SkyEye Quota Tracker] Total alerts: ${auditResult.total_alerts}`);
|
||||
for (const [name, svc] of Object.entries(auditResult.services)) {
|
||||
console.log(` ${name}: ${svc.status} (${svc.usage_percent}% used)`);
|
||||
}
|
||||
console.log(`[SkyEye Quota Tracker] Log saved: ${logPath}`);
|
||||
|
||||
console.log('---AUDIT_RESULT_JSON---');
|
||||
console.log(JSON.stringify(auditResult, null, 2));
|
||||
|
||||
return auditResult;
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* skyeye/scripts/scan-engine.js
|
||||
* 天眼扫描引擎 — 全量 + 增量基础设施扫描
|
||||
* Phase 1 of weekly scan: Infrastructure Scan
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const SKYEYE_DIR = path.resolve(__dirname, '..');
|
||||
const MANIFEST_PATH = path.join(SKYEYE_DIR, 'infra-manifest.json');
|
||||
const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards');
|
||||
const LOGS_DIR = path.join(SKYEYE_DIR, 'logs');
|
||||
|
||||
function getTimestamp() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function getDateStr() {
|
||||
const d = new Date();
|
||||
return d.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 scanGuardConfigs() {
|
||||
const results = { total: 0, valid: 0, invalid: [], missing_fields: [] };
|
||||
const requiredFields = ['guard_id', 'status', 'mode', 'quota_policy', 'health_check'];
|
||||
|
||||
const files = fs.readdirSync(GUARDS_DIR).filter(f => f.endsWith('.json') && f !== 'guard-template.json');
|
||||
results.total = files.length;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(GUARDS_DIR, file);
|
||||
const guard = loadJSON(filePath);
|
||||
if (!guard) {
|
||||
results.invalid.push({ file, reason: 'Invalid JSON' });
|
||||
continue;
|
||||
}
|
||||
const missing = requiredFields.filter(f => !(f in guard));
|
||||
if (missing.length > 0) {
|
||||
results.missing_fields.push({ file, missing });
|
||||
} else {
|
||||
results.valid++;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanWorkflows() {
|
||||
const workflowDir = path.join(ROOT, '.github/workflows');
|
||||
const results = { total: 0, files: [], errors: [] };
|
||||
|
||||
if (!fs.existsSync(workflowDir)) {
|
||||
results.errors.push('Workflow directory not found');
|
||||
return results;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
||||
results.total = files.length;
|
||||
results.files = files;
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanDirectoryStructure() {
|
||||
const requiredDirs = [
|
||||
'buffer',
|
||||
'buffer/inbox',
|
||||
'buffer/staging',
|
||||
'buffer/scripts',
|
||||
'buffer/config',
|
||||
'grid-db',
|
||||
'skyeye',
|
||||
'skyeye/guards',
|
||||
'skyeye/scripts',
|
||||
'skyeye/logs',
|
||||
'skyeye/scan-report',
|
||||
'.github/persona-brain',
|
||||
'.github/workflows',
|
||||
'scripts/skyeye'
|
||||
];
|
||||
|
||||
const results = { checked: requiredDirs.length, present: 0, missing: [] };
|
||||
|
||||
for (const dir of requiredDirs) {
|
||||
const fullPath = path.join(ROOT, dir);
|
||||
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
||||
results.present++;
|
||||
} else {
|
||||
results.missing.push(dir);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function scanSubRepos(manifest) {
|
||||
const results = { total: 0, verified: [] };
|
||||
if (!manifest || !manifest.infrastructure || !manifest.infrastructure.github) return results;
|
||||
|
||||
const repos = manifest.infrastructure.github.repos || [];
|
||||
results.total = repos.length;
|
||||
|
||||
for (const repo of repos) {
|
||||
results.verified.push({
|
||||
name: repo.name,
|
||||
role: repo.role,
|
||||
guard_path_exists: repo.guard ? fs.existsSync(path.join(ROOT, repo.guard)) : false
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args.find(a => a.startsWith('--mode='))?.split('=')[1] || 'full';
|
||||
|
||||
console.log(`[SkyEye Scan Engine] Mode: ${mode}`);
|
||||
console.log(`[SkyEye Scan Engine] Timestamp: ${getTimestamp()}`);
|
||||
|
||||
const manifest = loadJSON(MANIFEST_PATH);
|
||||
if (!manifest) {
|
||||
console.error('[SkyEye Scan Engine] ERROR: Cannot load infra-manifest.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scanResult = {
|
||||
scan_id: `SCAN-${getDateStr()}`,
|
||||
mode,
|
||||
timestamp: getTimestamp(),
|
||||
guard_scan: scanGuardConfigs(),
|
||||
workflow_scan: scanWorkflows(),
|
||||
directory_scan: scanDirectoryStructure(),
|
||||
sub_repo_scan: scanSubRepos(manifest),
|
||||
issues: [],
|
||||
summary: {}
|
||||
};
|
||||
|
||||
// Collect issues
|
||||
if (scanResult.guard_scan.invalid.length > 0) {
|
||||
scanResult.issues.push(...scanResult.guard_scan.invalid.map(i => ({
|
||||
severity: 'error',
|
||||
category: 'guard_config',
|
||||
detail: `Invalid guard config: ${i.file} - ${i.reason}`
|
||||
})));
|
||||
}
|
||||
if (scanResult.guard_scan.missing_fields.length > 0) {
|
||||
scanResult.issues.push(...scanResult.guard_scan.missing_fields.map(i => ({
|
||||
severity: 'warning',
|
||||
category: 'guard_config',
|
||||
detail: `Guard ${i.file} missing fields: ${i.missing.join(', ')}`
|
||||
})));
|
||||
}
|
||||
if (scanResult.directory_scan.missing.length > 0) {
|
||||
scanResult.issues.push(...scanResult.directory_scan.missing.map(d => ({
|
||||
severity: 'warning',
|
||||
category: 'directory_structure',
|
||||
detail: `Missing directory: ${d}`
|
||||
})));
|
||||
}
|
||||
|
||||
// Summary
|
||||
scanResult.summary = {
|
||||
total_issues: scanResult.issues.length,
|
||||
errors: scanResult.issues.filter(i => i.severity === 'error').length,
|
||||
warnings: scanResult.issues.filter(i => i.severity === 'warning').length,
|
||||
guards_healthy: `${scanResult.guard_scan.valid}/${scanResult.guard_scan.total}`,
|
||||
workflows_found: scanResult.workflow_scan.total,
|
||||
directories_ok: `${scanResult.directory_scan.present}/${scanResult.directory_scan.checked}`
|
||||
};
|
||||
|
||||
// Update manifest
|
||||
manifest.last_scan = getTimestamp();
|
||||
manifest.last_scan_by = 'skyeye-scan-engine';
|
||||
saveJSON(MANIFEST_PATH, manifest);
|
||||
|
||||
// Write log
|
||||
const logDir = path.join(LOGS_DIR, mode === 'full' ? 'weekly' : 'daily');
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
||||
const logPath = path.join(logDir, `scan-${getDateStr()}.json`);
|
||||
saveJSON(logPath, scanResult);
|
||||
|
||||
console.log(`[SkyEye Scan Engine] Scan complete.`);
|
||||
console.log(`[SkyEye Scan Engine] Guards: ${scanResult.summary.guards_healthy}`);
|
||||
console.log(`[SkyEye Scan Engine] Workflows: ${scanResult.summary.workflows_found}`);
|
||||
console.log(`[SkyEye Scan Engine] Directories: ${scanResult.summary.directories_ok}`);
|
||||
console.log(`[SkyEye Scan Engine] Issues: ${scanResult.summary.total_issues} (${scanResult.summary.errors} errors, ${scanResult.summary.warnings} warnings)`);
|
||||
console.log(`[SkyEye Scan Engine] Log saved: ${logPath}`);
|
||||
|
||||
// Output full result as JSON for pipeline
|
||||
console.log('---SCAN_RESULT_JSON---');
|
||||
console.log(JSON.stringify(scanResult, null, 2));
|
||||
|
||||
return scanResult;
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
#!/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();
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* skyeye/scripts/weekly-scan.js
|
||||
* 天眼周六大巡检主脚本 — 汇总所有 Phase 结果,生成报告
|
||||
* Phase 5 of weekly scan: Generate Report
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const SKYEYE_DIR = path.resolve(__dirname, '..');
|
||||
const SCAN_REPORT_DIR = path.join(SKYEYE_DIR, 'scan-report');
|
||||
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 getBeijingTime() {
|
||||
return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||
}
|
||||
|
||||
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 findLatestLog(logDir, prefix) {
|
||||
if (!fs.existsSync(logDir)) return null;
|
||||
const files = fs.readdirSync(logDir)
|
||||
.filter(f => f.startsWith(prefix) && f.endsWith('.json'))
|
||||
.sort()
|
||||
.reverse();
|
||||
if (files.length === 0) return null;
|
||||
return loadJSON(path.join(logDir, files[0]));
|
||||
}
|
||||
|
||||
function countGuards() {
|
||||
const guardsDir = path.join(SKYEYE_DIR, 'guards');
|
||||
if (!fs.existsSync(guardsDir)) return { total: 0, active: 0, suspended: 0 };
|
||||
|
||||
const files = fs.readdirSync(guardsDir)
|
||||
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
|
||||
|
||||
let active = 0;
|
||||
let suspended = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const guard = loadJSON(path.join(guardsDir, file));
|
||||
if (guard) {
|
||||
if (guard.status === 'active') active++;
|
||||
else if (guard.mode === 'suspended') suspended++;
|
||||
}
|
||||
}
|
||||
|
||||
return { total: files.length, active, suspended };
|
||||
}
|
||||
|
||||
function generateReport() {
|
||||
const args = process.argv.slice(2);
|
||||
const isReport = args.includes('--report');
|
||||
|
||||
console.log(`[SkyEye Weekly Scan] Generating weekly scan report`);
|
||||
console.log(`[SkyEye Weekly Scan] Beijing Time: ${getBeijingTime()}`);
|
||||
|
||||
const dateStr = getDateStr();
|
||||
const weeklyLogDir = path.join(LOGS_DIR, 'weekly');
|
||||
|
||||
// Load Phase results from logs
|
||||
const scanResult = findLatestLog(weeklyLogDir, 'scan-');
|
||||
const auditResult = findLatestLog(weeklyLogDir, 'quota-audit-');
|
||||
const optimizeResult = findLatestLog(weeklyLogDir, 'optimize-');
|
||||
const healResult = findLatestLog(weeklyLogDir, 'self-heal-');
|
||||
|
||||
const guardStatus = countGuards();
|
||||
|
||||
// Calculate next Saturday 20:00 CST
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getUTCDay();
|
||||
const daysUntilSaturday = (6 - dayOfWeek + 7) % 7 || 7;
|
||||
const nextSaturday = new Date(now);
|
||||
nextSaturday.setUTCDate(now.getUTCDate() + daysUntilSaturday);
|
||||
nextSaturday.setUTCHours(12, 0, 0, 0); // 20:00 CST = 12:00 UTC
|
||||
|
||||
const report = {
|
||||
report_id: `SKYEYE-SCAN-${dateStr}`,
|
||||
scan_time: getTimestamp(),
|
||||
scan_time_beijing: getBeijingTime(),
|
||||
scan_duration_seconds: 0,
|
||||
|
||||
infrastructure_status: {
|
||||
total_services: 5,
|
||||
healthy: 5,
|
||||
degraded: 0,
|
||||
down: 0
|
||||
},
|
||||
|
||||
quota_status: {
|
||||
github_actions: {
|
||||
used_percent: auditResult?.services?.github_actions?.usage_percent || 0,
|
||||
remaining_minutes: 2000,
|
||||
trend: 'stable',
|
||||
recommendation: 'maintain_current_frequency'
|
||||
},
|
||||
google_drive: {
|
||||
used_percent: auditResult?.services?.google_drive?.usage_percent || 0,
|
||||
recommendation: 'no_action_needed'
|
||||
},
|
||||
notion_api: {
|
||||
status: auditResult?.services?.notion_api?.status || 'healthy',
|
||||
recommendation: 'no_action_needed'
|
||||
},
|
||||
gemini: {
|
||||
used_percent: auditResult?.services?.gemini?.usage_percent || 0,
|
||||
recommendation: 'no_action_needed'
|
||||
}
|
||||
},
|
||||
|
||||
guard_status: {
|
||||
total_guards: guardStatus.total,
|
||||
active: guardStatus.active,
|
||||
suspended: guardStatus.suspended,
|
||||
adjustments_made: optimizeResult?.adjustments || []
|
||||
},
|
||||
|
||||
self_heal_actions: {
|
||||
files_cleaned: healResult?.summary?.files_cleaned || 0,
|
||||
configs_repaired: healResult?.summary?.configs_repaired || 0,
|
||||
guards_restarted: healResult?.summary?.guards_restarted || 0
|
||||
},
|
||||
|
||||
phase_details: {
|
||||
phase1_scan: scanResult?.summary || null,
|
||||
phase2_audit: auditResult?.overall_status || null,
|
||||
phase3_optimize: optimizeResult?.summary || null,
|
||||
phase4_heal: healResult?.summary || null
|
||||
},
|
||||
|
||||
alerts: [],
|
||||
human_action_required: [],
|
||||
|
||||
next_scan: nextSaturday.toISOString()
|
||||
};
|
||||
|
||||
// Collect alerts from all phases
|
||||
if (scanResult?.issues) {
|
||||
for (const issue of scanResult.issues) {
|
||||
if (issue.severity === 'error') {
|
||||
report.alerts.push(issue.detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auditResult?.services) {
|
||||
for (const [, svc] of Object.entries(auditResult.services)) {
|
||||
report.alerts.push(...(svc.alerts || []));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine infrastructure health
|
||||
if (report.alerts.length > 0) {
|
||||
const criticalAlerts = report.alerts.filter(a =>
|
||||
a.includes('90%') || a.includes('critical') || a.includes('严重')
|
||||
);
|
||||
if (criticalAlerts.length > 0) {
|
||||
report.infrastructure_status.degraded = criticalAlerts.length;
|
||||
report.infrastructure_status.healthy -= criticalAlerts.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Write report
|
||||
if (!fs.existsSync(SCAN_REPORT_DIR)) fs.mkdirSync(SCAN_REPORT_DIR, { recursive: true });
|
||||
const reportPath = path.join(SCAN_REPORT_DIR, `${dateStr}-weekly-scan.json`);
|
||||
saveJSON(reportPath, report);
|
||||
|
||||
console.log(`[SkyEye Weekly Scan] Report generated: ${reportPath}`);
|
||||
console.log(`[SkyEye Weekly Scan] Report ID: ${report.report_id}`);
|
||||
console.log(`[SkyEye Weekly Scan] Guards: ${guardStatus.active}/${guardStatus.total} active`);
|
||||
console.log(`[SkyEye Weekly Scan] Alerts: ${report.alerts.length}`);
|
||||
console.log(`[SkyEye Weekly Scan] Human action required: ${report.human_action_required.length}`);
|
||||
console.log(`[SkyEye Weekly Scan] Next scan: ${report.next_scan}`);
|
||||
|
||||
console.log('---REPORT_JSON---');
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
generateReport();
|
||||
Loading…
Reference in New Issue