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

203 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* skyeye/scripts/daily-scan.js
* 天眼日常扫描脚本 — 轻量级每日巡检(感知 + 护卫 + 精算)
* Covers Phase 1 steps ①②③ of the five-sense cycle
*/
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 LEDGER_PATH = path.join(SKYEYE_DIR, 'quota-ledger.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() {
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');
}
// ① 感知Sense— 扫描 infra-manifest确认节点状态
function sense() {
const result = { status: 'ok', nodes: [], issues: [] };
const manifest = loadJSON(MANIFEST_PATH);
if (!manifest) {
result.status = 'error';
result.issues.push('infra-manifest.json 不可读');
return result;
}
// Check last scan freshness (48h threshold)
if (manifest.last_scan) {
const lastScanTime = new Date(manifest.last_scan).getTime();
if (isNaN(lastScanTime)) {
result.issues.push('infra-manifest.last_scan 日期格式无效');
}
const lastScanAge = isNaN(lastScanTime) ? 0 : Date.now() - lastScanTime;
const hours = Math.round(lastScanAge / (1000 * 60 * 60));
if (!isNaN(lastScanTime) && hours > 48) {
result.issues.push(`infra-manifest 最后扫描距今 ${hours}h超过 48h 阈值`);
}
}
// Enumerate registered infrastructure nodes
if (manifest.infrastructure) {
for (const [key, svc] of Object.entries(manifest.infrastructure)) {
result.nodes.push({
id: key,
service: svc.service || key,
plan: svc.plan || 'unknown'
});
}
}
return result;
}
// ② 护卫Guard— 逐一检查五个 Guard 守卫状态
function guardCheck() {
const result = { total: 0, healthy: 0, issues: [], guards: [] };
if (!fs.existsSync(GUARDS_DIR)) {
result.issues.push('guards/ 目录不存在');
return result;
}
const files = fs.readdirSync(GUARDS_DIR)
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
result.total = files.length;
for (const file of files) {
const guard = loadJSON(path.join(GUARDS_DIR, file));
if (!guard) {
result.issues.push(`${file}: JSON 解析失败`);
result.guards.push({ file, status: 'error', reason: 'invalid_json' });
continue;
}
const entry = {
file,
guard_id: guard.guard_id,
status: guard.status,
mode: guard.mode,
last_check: guard.health_check?.last_check || null,
consecutive_failures: guard.health_check?.consecutive_failures || 0
};
if (guard.status === 'active' && (guard.health_check?.consecutive_failures || 0) < 3) {
result.healthy++;
} else {
result.issues.push(`${guard.guard_id}: status=${guard.status}, failures=${guard.health_check?.consecutive_failures || 0}`);
}
result.guards.push(entry);
}
return result;
}
// ③ 精算Audit— 读 quota-ledger检查配额消耗
function auditQuotas() {
const result = { status: 'healthy', alerts: [], services: {} };
const ledger = loadJSON(LEDGER_PATH);
if (!ledger) {
result.status = 'error';
result.alerts.push('quota-ledger.json 不可读');
return result;
}
if (ledger.services) {
for (const [name, data] of Object.entries(ledger.services)) {
let usagePct = 0;
if (data.limit && data.used !== undefined && data.used !== null) {
usagePct = Math.round((data.used / data.limit) * 100 * 10) / 10;
}
const entry = { usage_percent: usagePct, status: 'normal' };
if (usagePct > 95) {
entry.status = 'critical';
result.alerts.push(`${name}: 配额消耗 ${usagePct}% > 95%,需立即降频`);
result.status = 'critical';
} else if (usagePct > 80) {
entry.status = 'warning';
result.alerts.push(`${name}: 配额消耗 ${usagePct}% > 80%,配额告警`);
if (result.status !== 'critical') result.status = 'warning';
}
result.services[name] = entry;
}
}
return result;
}
function run() {
console.log(`[SkyEye Daily Scan] Starting daily scan`);
console.log(`[SkyEye Daily Scan] Timestamp: ${getTimestamp()}`);
const scanResult = {
scan_id: `DAILY-SCAN-${getDateStr()}`,
timestamp: getTimestamp(),
type: 'daily',
sense: sense(),
guard_check: guardCheck(),
quota_audit: auditQuotas(),
summary: {}
};
// Aggregate summary
const allIssues = [
...scanResult.sense.issues,
...scanResult.guard_check.issues,
...scanResult.quota_audit.alerts
];
scanResult.summary = {
nodes_registered: scanResult.sense.nodes.length,
guards_healthy: `${scanResult.guard_check.healthy}/${scanResult.guard_check.total}`,
quota_status: scanResult.quota_audit.status,
total_issues: allIssues.length,
issues: allIssues
};
// Write daily log
const logDir = path.join(LOGS_DIR, 'daily');
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
const logPath = path.join(logDir, `daily-scan-${getDateStr()}.json`);
saveJSON(logPath, scanResult);
console.log(`[SkyEye Daily Scan] Nodes: ${scanResult.summary.nodes_registered}`);
console.log(`[SkyEye Daily Scan] Guards: ${scanResult.summary.guards_healthy}`);
console.log(`[SkyEye Daily Scan] Quota: ${scanResult.summary.quota_status}`);
console.log(`[SkyEye Daily Scan] Issues: ${scanResult.summary.total_issues}`);
console.log(`[SkyEye Daily Scan] Log saved: ${logPath}`);
console.log('---DAILY_SCAN_JSON---');
console.log(JSON.stringify(scanResult, null, 2));
return scanResult;
}
run();