187 lines
6.5 KiB
JavaScript
187 lines
6.5 KiB
JavaScript
|
|
// scripts/neural/analyze-digest.js
|
|||
|
|
// 🧬 天眼日报分析引擎
|
|||
|
|
// 输入:最新日报 + 历史日报(最近7天)+ 分析规则
|
|||
|
|
// 输出:工单列表(如果有需要处理的问题)
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const DIGEST_DIR = 'data/neural-reports/daily-digest';
|
|||
|
|
const RULES_PATH = 'skyeye/neural-analysis-rules.json';
|
|||
|
|
const WORK_ORDER_DIR = 'data/neural-reports/work-orders';
|
|||
|
|
const TERMINAL_STATUSES = ['CLOSED', 'ESCALATED', 'VERIFIED'];
|
|||
|
|
|
|||
|
|
function loadJSON(p) {
|
|||
|
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return null; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getRecentDigests(days) {
|
|||
|
|
days = days || 7;
|
|||
|
|
if (!fs.existsSync(DIGEST_DIR)) return [];
|
|||
|
|
return fs.readdirSync(DIGEST_DIR)
|
|||
|
|
.filter(function(f) { return f.endsWith('.json') && f !== '.gitkeep'; })
|
|||
|
|
.sort().reverse().slice(0, days)
|
|||
|
|
.map(function(f) { return loadJSON(path.join(DIGEST_DIR, f)); })
|
|||
|
|
.filter(Boolean);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function evaluateCondition(digest, condition) {
|
|||
|
|
var field = condition.field;
|
|||
|
|
var operator = condition.operator;
|
|||
|
|
var value = condition.value;
|
|||
|
|
|
|||
|
|
var fieldValue = field.split('.').reduce(function(obj, key) {
|
|||
|
|
if (key === '*') return obj;
|
|||
|
|
return obj ? obj[key] : undefined;
|
|||
|
|
}, digest);
|
|||
|
|
|
|||
|
|
switch (operator) {
|
|||
|
|
case 'equals': return fieldValue === value;
|
|||
|
|
case 'not_empty': return Array.isArray(fieldValue) ? fieldValue.length > 0 : !!fieldValue;
|
|||
|
|
case 'greater_than': return (fieldValue || 0) > value;
|
|||
|
|
case 'greater_than_percent': {
|
|||
|
|
var ofField = condition.of.split('.').reduce(function(o, k) { return o ? o[k] : undefined; }, digest);
|
|||
|
|
return ofField > 0 && (fieldValue / ofField * 100) > value;
|
|||
|
|
}
|
|||
|
|
case 'contains': return JSON.stringify(fieldValue || '').indexOf(value) !== -1;
|
|||
|
|
default: return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function detectTrends(digests, trendRules) {
|
|||
|
|
var alerts = [];
|
|||
|
|
if (digests.length < 3) return alerts;
|
|||
|
|
|
|||
|
|
for (var i = 0; i < trendRules.length; i++) {
|
|||
|
|
var rule = trendRules[i];
|
|||
|
|
if (rule.name === 'failure_rate_rising') {
|
|||
|
|
var rates = digests.slice(0, 3).map(function(d) { return d.failure_rate_percent; });
|
|||
|
|
if (rates[0] > rates[1] && rates[1] > rates[2]) {
|
|||
|
|
alerts.push({
|
|||
|
|
type: 'trend_alert', rule: rule.name, severity: rule.severity,
|
|||
|
|
description: rule.description, data: { rates_last_3_days: rates }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (rule.name === 'workflow_going_silent') {
|
|||
|
|
var latestMap = digests[0] ? digests[0].brain_summary : null;
|
|||
|
|
if (latestMap) {
|
|||
|
|
var brains = Object.entries(latestMap);
|
|||
|
|
for (var b = 0; b < brains.length; b++) {
|
|||
|
|
var brain = brains[b][0];
|
|||
|
|
var summary = brains[b][1];
|
|||
|
|
var workflows = summary.workflows || [];
|
|||
|
|
for (var w = 0; w < workflows.length; w++) {
|
|||
|
|
var wf = workflows[w];
|
|||
|
|
var allSilent = digests.slice(0, 3).every(function(d) {
|
|||
|
|
var bSummary = d.brain_summary ? d.brain_summary[brain] : null;
|
|||
|
|
var bwf = bSummary ? (bSummary.workflows || []).find(function(x) { return x.id === wf.id; }) : null;
|
|||
|
|
return !bwf || bwf.runs === 0;
|
|||
|
|
});
|
|||
|
|
if (allSilent && wf.runs === 0) {
|
|||
|
|
alerts.push({
|
|||
|
|
type: 'silence_alert', rule: rule.name, severity: rule.severity,
|
|||
|
|
description: wf.name + ' 连续 3 天无运行记录',
|
|||
|
|
data: { workflow_id: wf.id, brain: brain }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return alerts;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function generateWorkOrders(digest, rules, trendAlerts) {
|
|||
|
|
var workOrders = [];
|
|||
|
|
var now = new Date().toISOString();
|
|||
|
|
|
|||
|
|
var severityEntries = Object.entries(rules.severity_rules);
|
|||
|
|
for (var s = 0; s < severityEntries.length; s++) {
|
|||
|
|
var severity = severityEntries[s][0];
|
|||
|
|
var rule = severityEntries[s][1];
|
|||
|
|
for (var c = 0; c < rule.conditions.length; c++) {
|
|||
|
|
var condition = rule.conditions[c];
|
|||
|
|
if (evaluateCondition(digest, condition)) {
|
|||
|
|
workOrders.push({
|
|||
|
|
id: 'WO-' + Date.now() + '-' + crypto.randomBytes(2).toString('hex'),
|
|||
|
|
created: now,
|
|||
|
|
source: 'neural-analysis-engine',
|
|||
|
|
source_digest: digest.digest_id,
|
|||
|
|
severity: severity.split('_')[0].toUpperCase(),
|
|||
|
|
title: '[' + severity.split('_')[0].toUpperCase() + '] ' + condition.field + ' 触发 ' + rule.description,
|
|||
|
|
description: '日报 ' + digest.digest_id + ' 中 ' + condition.field + ' 触发了 ' + rule.description + ' 规则',
|
|||
|
|
action: rule.action,
|
|||
|
|
notify: rule.notify,
|
|||
|
|
timeout_hours: rule.timeout_hours,
|
|||
|
|
status: 'pending'
|
|||
|
|
});
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (var t = 0; t < trendAlerts.length; t++) {
|
|||
|
|
var alert = trendAlerts[t];
|
|||
|
|
workOrders.push({
|
|||
|
|
id: 'WO-TREND-' + Date.now() + '-' + crypto.randomBytes(2).toString('hex'),
|
|||
|
|
created: now,
|
|||
|
|
source: 'neural-trend-detection',
|
|||
|
|
source_digest: digest.digest_id,
|
|||
|
|
severity: alert.severity,
|
|||
|
|
title: '[趋势] ' + alert.description,
|
|||
|
|
description: JSON.stringify(alert.data),
|
|||
|
|
action: 'investigate_trend',
|
|||
|
|
status: 'pending'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return workOrders;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
console.log('\n━━━ 🧬 天眼日报分析引擎启动 ━━━\n');
|
|||
|
|
|
|||
|
|
var rules = loadJSON(RULES_PATH);
|
|||
|
|
if (!rules) {
|
|||
|
|
console.log('❌ 分析规则文件不存在,跳过');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var digests = getRecentDigests(7);
|
|||
|
|
if (digests.length === 0) {
|
|||
|
|
console.log('⚠️ 无日报数据,跳过');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var latest = digests[0];
|
|||
|
|
console.log('📊 分析日报: ' + latest.digest_id);
|
|||
|
|
console.log(' 整体健康: ' + latest.overall_health);
|
|||
|
|
console.log(' 历史对比: ' + digests.length + ' 天数据\n');
|
|||
|
|
|
|||
|
|
var trendAlerts = detectTrends(digests, (rules.trend_detection || {}).rules || []);
|
|||
|
|
console.log('📈 趋势告警: ' + trendAlerts.length + ' 个');
|
|||
|
|
|
|||
|
|
var workOrders = generateWorkOrders(latest, rules, trendAlerts);
|
|||
|
|
console.log('📋 生成工单: ' + workOrders.length + ' 个\n');
|
|||
|
|
|
|||
|
|
if (workOrders.length > 0) {
|
|||
|
|
fs.mkdirSync(WORK_ORDER_DIR, { recursive: true });
|
|||
|
|
var date = latest.digest_id.replace('NEURAL-DIGEST-', '');
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(WORK_ORDER_DIR, 'work-orders-' + date + '.json'),
|
|||
|
|
JSON.stringify({ date: date, work_orders: workOrders }, null, 2)
|
|||
|
|
);
|
|||
|
|
console.log('💾 工单已保存到 ' + WORK_ORDER_DIR + '/');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (var i = 0; i < workOrders.length; i++) {
|
|||
|
|
console.log(' ' + workOrders[i].severity + ' | ' + workOrders[i].title);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('\n━━━ 分析完成 ━━━\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main();
|