guanghulab/skyeye/scripts/daily-scan.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

203 lines
6.0 KiB
JavaScript
Raw Permalink 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();