Merge pull request #166 from qinfendebingshuo/copilot/zy-sk-eyew-redefine-core-architecture

SkyEye Governance Layer — Guard system, quota tracking, weekly scan pipeline
This commit is contained in:
冰朔 2026-03-23 22:38:25 +08:00 committed by GitHub
commit 5cec3bfd55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1843 additions and 0 deletions

View File

@ -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

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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
}
}
}
}

View File

View File

View File

@ -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
}
}

View File

@ -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 天)"
]
}

View File

@ -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"
}
}

View File

@ -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
}
}

43
skyeye/quota-ledger.json Normal file
View File

@ -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"
}
}

View File

View File

@ -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"
}

View File

@ -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();

209
skyeye/scripts/optimizer.js Normal file
View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();