diff --git a/.github/workflows/skyeye-weekly-scan.yml b/.github/workflows/skyeye-weekly-scan.yml new file mode 100644 index 00000000..b1226dcb --- /dev/null +++ b/.github/workflows/skyeye-weekly-scan.yml @@ -0,0 +1,42 @@ +name: "SkyEye Weekly Full Scan" + +on: + schedule: + - cron: '0 12 * * 6' # 20:00 CST (UTC+8) every Saturday + repository_dispatch: + types: [skyeye-full-scan] # 手动触发 + +permissions: + contents: write + +jobs: + weekly-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: "🧐 Wake up SkyEye Core Brain" + run: echo "[SkyEye] Core brain awakened for weekly full scan at $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + + - name: "🔍 Phase 1 - Infrastructure Scan" + run: node skyeye/scripts/scan-engine.js --mode=full + + - name: "💰 Phase 2 - Quota Audit" + run: node skyeye/scripts/quota-tracker.js --audit + + - name: "⚙️ Phase 3 - Optimize Guards" + run: node skyeye/scripts/optimizer.js --apply + + - name: "🩹 Phase 4 - Self-Heal" + run: node skyeye/scripts/self-healer.js + + - name: "📝 Phase 5 - Generate Report" + run: node skyeye/scripts/weekly-scan.js --report + + - name: "Commit scan results" + run: | + git config user.name "天眼 · SkyEye Core" + git config user.email "skyeye@guanghu.system" + git add skyeye/ buffer/config/ + git diff --cached --quiet || git commit -m "[skyeye-weekly] $(date -u +%Y%m%d) full scan complete [skip ci]" + git push diff --git a/skyeye/guards/actions-guard.json b/skyeye/guards/actions-guard.json new file mode 100644 index 00000000..fe05ab6e --- /dev/null +++ b/skyeye/guards/actions-guard.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-ACTIONS", + "target_service": "GitHub Actions", + "status": "active", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/actions-staging/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": 2000, + "current_month_used": 0, + "daily_soft_limit": 67, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "workflow_status_check", + "interval_hours": 6, + "last_check": null, + "last_status": "healthy", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/guards/drive-guard.json b/skyeye/guards/drive-guard.json new file mode 100644 index 00000000..322e980c --- /dev/null +++ b/skyeye/guards/drive-guard.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-DRIVE", + "target_service": "Google Drive API", + "status": "active", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/drive-staging/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": 15, + "current_month_used": 0, + "daily_soft_limit": 0, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "api_ping", + "interval_hours": 6, + "last_check": null, + "last_status": "healthy", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/guards/gemini-guard.json b/skyeye/guards/gemini-guard.json new file mode 100644 index 00000000..6e50cc4c --- /dev/null +++ b/skyeye/guards/gemini-guard.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-GEMINI", + "target_service": "Google Gemini API", + "status": "active", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/gemini-staging/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": 1500, + "current_month_used": 0, + "daily_soft_limit": 50, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "api_ping", + "interval_hours": 6, + "last_check": null, + "last_status": "healthy", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/guards/github-guard.json b/skyeye/guards/github-guard.json new file mode 100644 index 00000000..0b407374 --- /dev/null +++ b/skyeye/guards/github-guard.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-GITHUB", + "target_service": "GitHub API", + "status": "active", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/inbox/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": 2000, + "current_month_used": 0, + "daily_soft_limit": 67, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "api_ping", + "interval_hours": 6, + "last_check": null, + "last_status": "healthy", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/guards/guard-template.json b/skyeye/guards/guard-template.json new file mode 100644 index 00000000..4f9ff7e4 --- /dev/null +++ b/skyeye/guards/guard-template.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-[SERVICE_NAME]", + "target_service": "[Service Name]", + "status": "initializing", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/[service]-staging/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": "[TO_BE_FILLED]", + "current_month_used": 0, + "daily_soft_limit": 0, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "[TO_BE_FILLED]", + "interval_hours": 6, + "last_check": null, + "last_status": "unknown", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/guards/notion-guard.json b/skyeye/guards/notion-guard.json new file mode 100644 index 00000000..d141b9ba --- /dev/null +++ b/skyeye/guards/notion-guard.json @@ -0,0 +1,40 @@ +{ + "guard_id": "GUARD-NOTION", + "target_service": "Notion API", + "status": "active", + "mode": "buffer", + "buffer_policy": { + "enabled": true, + "buffer_path": "buffer/notion-staging/", + "max_buffer_age_hours": 24, + "flush_strategy": "scheduled", + "flush_schedule_cst": ["21:30"] + }, + "quota_policy": { + "monthly_limit": 3, + "current_month_used": 0, + "daily_soft_limit": 0, + "warning_threshold_percent": 70, + "critical_threshold_percent": 90, + "on_warning": "reduce_frequency", + "on_critical": "emergency_only" + }, + "trigger_policy": { + "default_frequency": "3_per_day", + "min_frequency": "1_per_day", + "max_frequency": "6_per_day", + "auto_adjust": true, + "adjust_based_on": ["quota_remaining", "message_volume", "error_rate"] + }, + "health_check": { + "enabled": true, + "method": "api_ping", + "interval_hours": 6, + "last_check": null, + "last_status": "healthy", + "consecutive_failures": 0, + "max_failures_before_alert": 3 + }, + "last_updated_by": "skyeye-init", + "last_updated_at": "2026-03-23T12:00:00Z" +} diff --git a/skyeye/infra-manifest.json b/skyeye/infra-manifest.json new file mode 100644 index 00000000..4e0a571b --- /dev/null +++ b/skyeye/infra-manifest.json @@ -0,0 +1,82 @@ +{ + "version": "1.0.0", + "last_scan": "2026-03-23T14:30:07.186Z", + "last_scan_by": "skyeye-scan-engine", + "infrastructure": { + "github": { + "service": "GitHub", + "plan": "Free", + "repos": [ + { + "name": "qinfendebingshuo/guanghulab", + "role": "main", + "guard": "buffer/" + }, + { + "name": "WENZHUOXI/guanghu-awen", + "role": "sub-repo", + "persona": "PER-ZQ001", + "guard": ".github/persona-brain/" + } + ], + "quotas": { + "actions_minutes_per_month": 2000, + "actions_current_used": 0, + "actions_budget_allocated": { + "buffer_collect": 270, + "buffer_flush": 90, + "skyeye_checkin": 180, + "skyeye_weekly_scan": 30, + "other_workflows": 200, + "emergency_reserve": 1230 + }, + "storage_limit_gb": 0.5, + "lfs_bandwidth_gb": 1 + } + }, + "google_drive": { + "service": "Google Drive", + "plan": "Google One / Free 15GB", + "guard": "buffer/drive-staging/", + "quotas": { + "storage_gb": 15, + "current_used_gb": 0, + "apps_script_daily_triggers": 20, + "apps_script_runtime_min_per_day": 90, + "drive_api_queries_per_100s": 20000 + } + }, + "notion": { + "service": "Notion", + "plan": "Plus", + "guard": "霜砚(PER-SY001)", + "quotas": { + "api_requests_per_second": 3, + "blocks_per_page": 10000, + "file_upload_mb": 5, + "guest_limit": 100 + } + }, + "gemini": { + "service": "Google Gemini", + "plan": "Free / Pro", + "guard": "buffer/gemini-staging/", + "quotas": { + "requests_per_minute": 15, + "requests_per_day": 1500, + "context_window_tokens": 1000000, + "personal_context_files": 100 + } + }, + "github_actions": { + "service": "GitHub Actions", + "plan": "Free", + "guard": "skyeye/guards/actions-guard.json", + "quotas": { + "concurrent_jobs": 20, + "minutes_per_month": 2000, + "job_timeout_hours": 6 + } + } + } +} diff --git a/skyeye/logs/daily/.gitkeep b/skyeye/logs/daily/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/skyeye/logs/weekly/.gitkeep b/skyeye/logs/weekly/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/skyeye/logs/weekly/optimize-20260323.json b/skyeye/logs/weekly/optimize-20260323.json new file mode 100644 index 00000000..d8952e2c --- /dev/null +++ b/skyeye/logs/weekly/optimize-20260323.json @@ -0,0 +1,12 @@ +{ + "optimize_id": "OPT-20260323", + "timestamp": "2026-03-23T14:30:16.154Z", + "mode": "apply", + "guards_checked": 5, + "adjustments": [], + "summary": { + "guards_checked": 5, + "total_adjustments": 0, + "applied": true + } +} diff --git a/skyeye/logs/weekly/quota-audit-20260323.json b/skyeye/logs/weekly/quota-audit-20260323.json new file mode 100644 index 00000000..975af2be --- /dev/null +++ b/skyeye/logs/weekly/quota-audit-20260323.json @@ -0,0 +1,42 @@ +{ + "audit_id": "AUDIT-20260323", + "timestamp": "2026-03-23T14:30:11.691Z", + "mode": "full", + "services": { + "github_actions": { + "service": "github_actions", + "status": "healthy", + "usage_percent": 0, + "alerts": [], + "recommendations": [ + "日均可用预算: 250 分钟 (剩余 8 天)" + ] + }, + "google_drive": { + "service": "google_drive", + "status": "healthy", + "usage_percent": 0, + "alerts": [], + "recommendations": [] + }, + "notion_api": { + "service": "notion_api", + "status": "healthy", + "usage_percent": 0, + "alerts": [], + "recommendations": [] + }, + "gemini": { + "service": "gemini", + "status": "healthy", + "usage_percent": 0, + "alerts": [], + "recommendations": [] + } + }, + "overall_status": "healthy", + "total_alerts": 0, + "recommendations": [ + "日均可用预算: 250 分钟 (剩余 8 天)" + ] +} diff --git a/skyeye/logs/weekly/scan-20260323.json b/skyeye/logs/weekly/scan-20260323.json new file mode 100644 index 00000000..1132f64e --- /dev/null +++ b/skyeye/logs/weekly/scan-20260323.json @@ -0,0 +1,125 @@ +{ + "scan_id": "SCAN-20260323", + "mode": "full", + "timestamp": "2026-03-23T14:30:07.185Z", + "guard_scan": { + "total": 5, + "valid": 5, + "invalid": [], + "missing_fields": [] + }, + "workflow_scan": { + "total": 79, + "files": [ + "agent-checkin.yml", + "auto-deploy-drive-bridge.yml", + "auto-reply-discussions.yml", + "bingshuo-deploy-agent.yml", + "bingshuo-neural-system.yml", + "brain-sync.yml", + "bridge-broadcast-pdf.yml", + "bridge-changes-to-notion.yml", + "bridge-heartbeat.yml", + "bridge-session-summary.yml", + "bridge-syslog-intake.yml", + "bridge-syslog-to-notion.yml", + "buffer-collect.yml", + "buffer-flush.yml", + "check-structure.yml", + "daily-maintenance.yml", + "daily-report.yml", + "deploy-pages.yml", + "deploy-to-server.yml", + "distribute-broadcasts.yml", + "esp-signal-processor.yml", + "execution-sync.yml", + "federation-bridge.yml", + "feishu-syslog-bridge.yml", + "generate-module-doc.yml", + "grid-db-archive.yml", + "grid-db-processor.yml", + "grid-db-training-extract.yml", + "hli-contract-check.yml", + "meta-watchdog.yml", + "notion-callback-pipeline.yml", + "notion-connectivity-test.yml", + "notion-heartbeat.yml", + "notion-page-reader.yml", + "notion-poll.yml", + "notion-wake-listener.yml", + "openclaw-wake-loop.yml", + "persona-invoke.yml", + "pm2-server-diagnose.yml", + "process-notion-orders.yml", + "ps-on-build.yml", + "ps-on-chat.yml", + "ps-on-complete.yml", + "ps-on-login.yml", + "psp-daily-inspection.yml", + "pull-sync-awen.yml", + "push-broadcast-feishu.yml", + "push-broadcast.yml", + "receive-spoke-checkin.yml", + "receive-syslog.yml", + "sandbox-deploy.yml", + "server-patrol.yml", + "skyeye-checkin-audit.yml", + "skyeye-checkin-receiver.yml", + "skyeye-weekly-scan.yml", + "staging-preview.yml", + "sync-deploy-to-notion.yml", + "sync-dev-status.yml", + "sync-griddb-to-drive.yml", + "sync-griddb-to-notion.yml", + "sync-login-entry.yml", + "sync-notion-to-griddb.yml", + "sync-persona-studio.yml", + "syslog-auto-pipeline.yml", + "syslog-issue-pipeline.yml", + "syslog-pipeline.yml", + "test-notion-bridge.yml", + "tianyan-daily-patrol.yml", + "update-readme-bulletin.yml", + "update-repo-map.yml", + "zhuyuan-brain-sync.yml", + "zhuyuan-commander.yml", + "zhuyuan-daily-agent.yml", + "zhuyuan-daily-inspection.yml", + "zhuyuan-daily-selfcheck.yml", + "zhuyuan-gate-guard.yml", + "zhuyuan-issue-reply.yml", + "zhuyuan-pr-review.yml", + "zhuyuan-skyeye.yml" + ], + "errors": [] + }, + "directory_scan": { + "checked": 14, + "present": 14, + "missing": [] + }, + "sub_repo_scan": { + "total": 2, + "verified": [ + { + "name": "qinfendebingshuo/guanghulab", + "role": "main", + "guard_path_exists": true + }, + { + "name": "WENZHUOXI/guanghu-awen", + "role": "sub-repo", + "guard_path_exists": true + } + ] + }, + "issues": [], + "summary": { + "total_issues": 0, + "errors": 0, + "warnings": 0, + "guards_healthy": "5/5", + "workflows_found": 79, + "directories_ok": "14/14" + } +} diff --git a/skyeye/logs/weekly/self-heal-20260323.json b/skyeye/logs/weekly/self-heal-20260323.json new file mode 100644 index 00000000..38f752c5 --- /dev/null +++ b/skyeye/logs/weekly/self-heal-20260323.json @@ -0,0 +1,32 @@ +{ + "heal_id": "HEAL-20260323", + "timestamp": "2026-03-23T14:30:20.461Z", + "guard_repairs": { + "repaired": [], + "failed": [] + }, + "buffer_cleanup": { + "files_cleaned": 0, + "directories": [ + { + "path": "buffer/processed", + "cleaned": 0 + } + ] + }, + "log_cleanup": { + "files_cleaned": 0 + }, + "guard_restarts": { + "restarted": [] + }, + "directory_fixes": { + "created": [] + }, + "summary": { + "configs_repaired": 0, + "files_cleaned": 0, + "guards_restarted": 0, + "directories_created": 0 + } +} diff --git a/skyeye/quota-ledger.json b/skyeye/quota-ledger.json new file mode 100644 index 00000000..55fb88e9 --- /dev/null +++ b/skyeye/quota-ledger.json @@ -0,0 +1,43 @@ +{ + "ledger_version": "1.0.0", + "billing_cycle": { + "start": "2026-03-01", + "end": "2026-03-31" + }, + "services": { + "github_actions": { + "limit": 2000, + "used": 0, + "remaining": 2000, + "allocated": { + "buffer_system": 360, + "skyeye_daily": 180, + "skyeye_weekly": 30, + "sub_repo_checkins": 200, + "emergency": 1230 + }, + "daily_entries": [] + }, + "google_drive": { + "storage_limit_gb": 15, + "storage_used_gb": 0, + "apps_script_triggers_today": 0, + "apps_script_triggers_limit": 20 + }, + "notion_api": { + "rate_limit_per_second": 3, + "requests_today": 0, + "errors_today": 0 + }, + "gemini": { + "daily_limit": 1500, + "daily_used": 0, + "per_minute_limit": 15 + } + }, + "health_summary": { + "overall": "healthy", + "alerts": [], + "last_updated": "2026-03-23T14:30:11.691Z" + } +} diff --git a/skyeye/scan-report/.gitkeep b/skyeye/scan-report/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/skyeye/scan-report/20260323-weekly-scan.json b/skyeye/scan-report/20260323-weekly-scan.json new file mode 100644 index 00000000..0c1af001 --- /dev/null +++ b/skyeye/scan-report/20260323-weekly-scan.json @@ -0,0 +1,68 @@ +{ + "report_id": "SKYEYE-SCAN-20260323", + "scan_time": "2026-03-23T14:30:25.666Z", + "scan_time_beijing": "2026/3/23 22:30:25", + "scan_duration_seconds": 0, + "infrastructure_status": { + "total_services": 5, + "healthy": 5, + "degraded": 0, + "down": 0 + }, + "quota_status": { + "github_actions": { + "used_percent": 0, + "remaining_minutes": 2000, + "trend": "stable", + "recommendation": "maintain_current_frequency" + }, + "google_drive": { + "used_percent": 0, + "recommendation": "no_action_needed" + }, + "notion_api": { + "status": "healthy", + "recommendation": "no_action_needed" + }, + "gemini": { + "used_percent": 0, + "recommendation": "no_action_needed" + } + }, + "guard_status": { + "total_guards": 5, + "active": 5, + "suspended": 0, + "adjustments_made": [] + }, + "self_heal_actions": { + "files_cleaned": 0, + "configs_repaired": 0, + "guards_restarted": 0 + }, + "phase_details": { + "phase1_scan": { + "total_issues": 0, + "errors": 0, + "warnings": 0, + "guards_healthy": "5/5", + "workflows_found": 79, + "directories_ok": "14/14" + }, + "phase2_audit": "healthy", + "phase3_optimize": { + "guards_checked": 5, + "total_adjustments": 0, + "applied": true + }, + "phase4_heal": { + "configs_repaired": 0, + "files_cleaned": 0, + "guards_restarted": 0, + "directories_created": 0 + } + }, + "alerts": [], + "human_action_required": [], + "next_scan": "2026-03-28T12:00:00.000Z" +} diff --git a/skyeye/scripts/guard-factory.js b/skyeye/scripts/guard-factory.js new file mode 100644 index 00000000..47fcb389 --- /dev/null +++ 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 new file mode 100644 index 00000000..557ee8b7 --- /dev/null +++ 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 new file mode 100644 index 00000000..42bdaeca --- /dev/null +++ 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 new file mode 100644 index 00000000..5962787a --- /dev/null +++ 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 new file mode 100644 index 00000000..b24e6e6c --- /dev/null +++ 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 new file mode 100644 index 00000000..e45e0954 --- /dev/null +++ b/skyeye/scripts/weekly-scan.js @@ -0,0 +1,197 @@ +#!/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.status === 'suspended' || guard.mode === 'suspended') suspended++; + } + } + + return { total: files.length, active, suspended }; +} + +function generateReport() { + 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();