157 lines
4.5 KiB
JavaScript
157 lines
4.5 KiB
JavaScript
// scripts/skyeye/scan-soldier-health.js
|
||
// 天眼·扫描模块D15 · 小兵健康全局视图
|
||
//
|
||
// 扫描内容:
|
||
// ① 读取铸渊将军仪表盘 commander-dashboard.json
|
||
// ② 小兵故障率 > 20% → P0 工单
|
||
// ③ 小兵连续7天同一错误未修复 → P1 工单
|
||
// ④ 全局小兵健康统计
|
||
//
|
||
// 输出:JSON → stdout
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '../..');
|
||
const DASHBOARD_PATH = path.join(ROOT, 'data/bulletin-board/dashboard.json');
|
||
const SKYEYE_REPORTS_DIR = path.join(ROOT, 'data/skyeye-reports');
|
||
|
||
const FAILURE_RATE_THRESHOLD = 20;
|
||
const CONSECUTIVE_FAILURE_THRESHOLD_DAYS = 7;
|
||
|
||
// ━━━ 安全读取 JSON ━━━
|
||
function readJSON(filePath) {
|
||
try {
|
||
if (!fs.existsSync(filePath)) return null;
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ━━━ 获取最近N天的天眼报告 ━━━
|
||
function getRecentReports(days) {
|
||
const reports = [];
|
||
try {
|
||
if (!fs.existsSync(SKYEYE_REPORTS_DIR)) return reports;
|
||
const files = fs.readdirSync(SKYEYE_REPORTS_DIR)
|
||
.filter(f => f.startsWith('skyeye-') && f.endsWith('.json'))
|
||
.sort()
|
||
.reverse()
|
||
.slice(0, days);
|
||
|
||
for (const file of files) {
|
||
const report = readJSON(path.join(SKYEYE_REPORTS_DIR, file));
|
||
if (report) reports.push(report);
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
return reports;
|
||
}
|
||
|
||
// ━━━ D15 主扫描 ━━━
|
||
function scanSoldierHealth() {
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
const now = new Date();
|
||
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString()
|
||
.replace('T', ' ').slice(0, 19) + '+08:00';
|
||
|
||
const result = {
|
||
dimension: 'D15',
|
||
name: '小兵健康全局视图',
|
||
scan_time: bjTime,
|
||
status: '✅',
|
||
dashboard_exists: false,
|
||
total_soldiers: 0,
|
||
healthy: 0,
|
||
failed: 0,
|
||
needs_optimization: 0,
|
||
failure_rate: 0,
|
||
issues: [],
|
||
recurring_errors: [],
|
||
recommendations: []
|
||
};
|
||
|
||
// 读取将军仪表盘
|
||
const dashboard = readJSON(DASHBOARD_PATH);
|
||
if (!dashboard) {
|
||
result.dashboard_exists = false;
|
||
result.status = '⚠️';
|
||
result.issues.push({
|
||
type: 'dashboard_missing',
|
||
message: '将军仪表盘不存在 · 需先运行 commander-dashboard.js',
|
||
action: 'P1工单 · 生成仪表盘'
|
||
});
|
||
} else {
|
||
result.dashboard_exists = true;
|
||
const gv = dashboard.global_view || {};
|
||
result.total_soldiers = gv.total_soldiers || 0;
|
||
result.healthy = gv.healthy || 0;
|
||
result.failed = gv.failed || 0;
|
||
result.needs_optimization = gv.needs_optimization || 0;
|
||
|
||
// 计算故障率
|
||
if (result.total_soldiers > 0) {
|
||
result.failure_rate = Math.round((result.failed / result.total_soldiers) * 100);
|
||
}
|
||
|
||
// 故障率 > 20% → P0
|
||
if (result.failure_rate > FAILURE_RATE_THRESHOLD) {
|
||
result.status = '❌';
|
||
result.issues.push({
|
||
type: 'high_failure_rate',
|
||
message: `小兵故障率 ${result.failure_rate}% > ${FAILURE_RATE_THRESHOLD}%`,
|
||
action: 'P0工单 · 全局小兵修复'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 检查连续7天同一错误
|
||
const recentReports = getRecentReports(CONSECUTIVE_FAILURE_THRESHOLD_DAYS);
|
||
if (recentReports.length >= 2) {
|
||
// 收集各报告中的失败 workflow
|
||
const errorMap = {};
|
||
for (const report of recentReports) {
|
||
if (report.workflow_health && report.workflow_health.details) {
|
||
for (const wf of report.workflow_health.details) {
|
||
if (wf.status === 'failed') {
|
||
const key = wf.name || wf.file;
|
||
errorMap[key] = (errorMap[key] || 0) + 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 连续7天同一错误
|
||
for (const [name, count] of Object.entries(errorMap)) {
|
||
if (count >= CONSECUTIVE_FAILURE_THRESHOLD_DAYS) {
|
||
result.recurring_errors.push({
|
||
soldier: name,
|
||
consecutive_failures: count,
|
||
action: 'P1工单 · 建议铸渊介入'
|
||
});
|
||
if (result.status === '✅') result.status = '⚠️';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 生成建议
|
||
if (result.failed > 0) {
|
||
result.recommendations.push(`修复 ${result.failed} 个故障小兵`);
|
||
}
|
||
if (result.recurring_errors.length > 0) {
|
||
result.recommendations.push(`${result.recurring_errors.length} 个小兵存在持续性故障,需铸渊介入`);
|
||
}
|
||
if (result.total_soldiers === 0) {
|
||
result.recommendations.push('仪表盘无小兵数据 · 需先生成仪表盘');
|
||
}
|
||
|
||
console.log(JSON.stringify(result, null, 2));
|
||
return result;
|
||
}
|
||
|
||
scanSoldierHealth();
|