guanghulab/skyeye/scripts/quota-tracker.js
2026-05-10 13:12:44 +08:00

183 lines
5.6 KiB
JavaScript

#!/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();