From a000e0d6bcf41a018aae373bf920286a8814de79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:27:23 +0000 Subject: [PATCH] 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> --- skyeye/scripts/guard-factory.js | 121 ++++++++++++++++ skyeye/scripts/optimizer.js | 209 +++++++++++++++++++++++++++ skyeye/scripts/quota-tracker.js | 182 ++++++++++++++++++++++++ skyeye/scripts/scan-engine.js | 208 +++++++++++++++++++++++++++ skyeye/scripts/self-healer.js | 240 ++++++++++++++++++++++++++++++++ skyeye/scripts/weekly-scan.js | 200 ++++++++++++++++++++++++++ 6 files changed, 1160 insertions(+) diff --git a/skyeye/scripts/guard-factory.js b/skyeye/scripts/guard-factory.js index e69de29b..47fcb389 100644 --- a/skyeye/scripts/guard-factory.js +++ b/skyeye/scripts/guard-factory.js @@ -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 [--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 Create a new guard from template'); + console.log(' --force Overwrite existing guard'); + listGuards(); +} + +run(); diff --git a/skyeye/scripts/optimizer.js b/skyeye/scripts/optimizer.js index e69de29b..557ee8b7 100644 --- a/skyeye/scripts/optimizer.js +++ b/skyeye/scripts/optimizer.js @@ -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(); diff --git a/skyeye/scripts/quota-tracker.js b/skyeye/scripts/quota-tracker.js index e69de29b..42bdaeca 100644 --- a/skyeye/scripts/quota-tracker.js +++ b/skyeye/scripts/quota-tracker.js @@ -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(); diff --git a/skyeye/scripts/scan-engine.js b/skyeye/scripts/scan-engine.js index e69de29b..5962787a 100644 --- a/skyeye/scripts/scan-engine.js +++ b/skyeye/scripts/scan-engine.js @@ -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(); diff --git a/skyeye/scripts/self-healer.js b/skyeye/scripts/self-healer.js index e69de29b..b24e6e6c 100644 --- a/skyeye/scripts/self-healer.js +++ b/skyeye/scripts/self-healer.js @@ -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(); diff --git a/skyeye/scripts/weekly-scan.js b/skyeye/scripts/weekly-scan.js index e69de29b..291b37a6 100644 --- a/skyeye/scripts/weekly-scan.js +++ b/skyeye/scripts/weekly-scan.js @@ -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();