301 lines
11 KiB
JavaScript
301 lines
11 KiB
JavaScript
// scripts/skyeye/report-generator.js
|
||
// 天眼·全局健康报告生成器
|
||
//
|
||
// 汇总所有扫描结果 + 诊断 + 修复结果 → 生成统一报告
|
||
// 报告写入:/tmp/skyeye/full-report.json + memory.json skyeye 段
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '../..');
|
||
const SKYEYE_DIR = '/tmp/skyeye';
|
||
const BRAIN_DIR = path.join(ROOT, '.github/persona-brain');
|
||
const MEMORY_PATH = path.join(BRAIN_DIR, 'memory.json');
|
||
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
const startTime = Date.now();
|
||
|
||
// ━━━ 安全读取 JSON ━━━
|
||
function readJSON(filePath) {
|
||
try {
|
||
if (!fs.existsSync(filePath)) return null;
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ━━━ 生成报告 ━━━
|
||
function generateReport() {
|
||
const now = new Date();
|
||
const dateStr = now.toISOString().slice(0, 10);
|
||
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString()
|
||
.replace('T', ' ').slice(0, 19) + '+08:00';
|
||
|
||
// 读取所有扫描结果
|
||
const wfHealth = readJSON(path.join(SKYEYE_DIR, 'workflow-health.json'));
|
||
const structHealth = readJSON(path.join(SKYEYE_DIR, 'structure-health.json'));
|
||
const brainHealth = readJSON(path.join(SKYEYE_DIR, 'brain-health.json'));
|
||
const bridgeHealth = readJSON(path.join(SKYEYE_DIR, 'bridge-health.json'));
|
||
const securityHealth = readJSON(path.join(SKYEYE_DIR, 'security-health.json'));
|
||
const sfpHealth = readJSON(path.join(SKYEYE_DIR, 'sfp-health.json'));
|
||
const soldierHealth = readJSON(path.join(SKYEYE_DIR, 'soldier-health.json'));
|
||
const diagnosis = readJSON(path.join(SKYEYE_DIR, 'diagnosis.json'));
|
||
const repairResult = readJSON(path.join(SKYEYE_DIR, 'repair-result.json'));
|
||
|
||
// 计算时长
|
||
const durationSeconds = Math.round((Date.now() - startTime) / 1000);
|
||
|
||
// 确定整体健康度
|
||
let overallHealth = '🟢';
|
||
if (diagnosis && diagnosis.needs_human > 0) {
|
||
overallHealth = '🔴';
|
||
} else if (diagnosis && diagnosis.total_issues > 0) {
|
||
overallHealth = '🟡';
|
||
}
|
||
|
||
// 构建报告
|
||
const report = {
|
||
report_id: `SKYEYE-${dateStr.replace(/-/g, '')}`,
|
||
timestamp: bjTime,
|
||
duration_seconds: durationSeconds,
|
||
overall_health: overallHealth,
|
||
|
||
brain_status: {
|
||
integrity: brainHealth ? brainHealth.integrity : 'unknown',
|
||
memory_fresh: brainHealth ? brainHealth.memory_fresh : false,
|
||
routing_map_aligned: brainHealth ? brainHealth.routing_map_aligned : false,
|
||
dev_status_last_sync: brainHealth && brainHealth.dev_status
|
||
? brainHealth.dev_status.last_sync : null,
|
||
knowledge_base_entries: brainHealth && brainHealth.knowledge_base
|
||
? brainHealth.knowledge_base.entries : 0
|
||
},
|
||
|
||
workflow_health: {
|
||
total: wfHealth ? wfHealth.total_workflows : 0,
|
||
healthy: wfHealth ? wfHealth.healthy : 0,
|
||
failed: wfHealth ? (wfHealth.total_workflows - wfHealth.healthy) : 0,
|
||
never_triggered: 0, // Determined from recent runs
|
||
conflicts: wfHealth ? (wfHealth.cron_conflicts || []).length : 0,
|
||
details: wfHealth ? (wfHealth.workflow_map || []).map(w => ({
|
||
name: w.name,
|
||
file: w.file,
|
||
status: w.status,
|
||
last_run: w.last_run || 'unknown'
|
||
})) : []
|
||
},
|
||
|
||
structure_health: {
|
||
core_dirs_ok: structHealth ? structHealth.core_dirs_ok : false,
|
||
orphan_files: structHealth ? structHealth.orphan_files : 0,
|
||
missing_dirs: structHealth ? structHealth.missing_dirs : [],
|
||
readme_ok: structHealth ? structHealth.readme_ok : false
|
||
},
|
||
|
||
bridge_health: {
|
||
notion_api: bridgeHealth ? bridgeHealth.notion_api.status : '❓',
|
||
server_ssh: bridgeHealth ? bridgeHealth.server_ssh.status : '❓',
|
||
github_api: bridgeHealth ? bridgeHealth.github_api.status : '❓',
|
||
secrets_complete: bridgeHealth && bridgeHealth.secrets
|
||
? bridgeHealth.secrets.complete : false
|
||
},
|
||
|
||
security_health: securityHealth && securityHealth.security_health
|
||
? securityHealth.security_health
|
||
: {
|
||
protocol_exists: false,
|
||
root_rules_intact: false,
|
||
level: null,
|
||
permanent: false,
|
||
copyright_anchor: false,
|
||
last_verified: bjTime
|
||
},
|
||
|
||
bulletin_sfp_health: sfpHealth ? {
|
||
dimension: 'D14',
|
||
status: sfpHealth.status || '❓',
|
||
total_messages: sfpHealth.total_messages || 0,
|
||
valid_fingerprints: sfpHealth.valid_fingerprints || 0,
|
||
invalid_fingerprints: sfpHealth.invalid_fingerprints || 0,
|
||
without_fingerprint: sfpHealth.without_fingerprint || 0,
|
||
sfp_config_exists: sfpHealth.sfp_config_exists || false,
|
||
agent_activity: sfpHealth.agent_activity || {}
|
||
} : { dimension: 'D14', status: '❓' },
|
||
|
||
soldier_health: soldierHealth ? {
|
||
dimension: 'D15',
|
||
status: soldierHealth.status || '❓',
|
||
total_soldiers: soldierHealth.total_soldiers || 0,
|
||
healthy: soldierHealth.healthy || 0,
|
||
failed: soldierHealth.failed || 0,
|
||
failure_rate: soldierHealth.failure_rate || 0,
|
||
recurring_errors: soldierHealth.recurring_errors || [],
|
||
recommendations: soldierHealth.recommendations || []
|
||
} : { dimension: 'D15', status: '❓' },
|
||
|
||
hibernation_status: (() => {
|
||
const hibDir = path.join(ROOT, 'skyeye/hibernation');
|
||
const cpDir = path.join(hibDir, 'checkpoints');
|
||
const snapDir = path.join(hibDir, 'weekly-snapshots');
|
||
const upDir = path.join(hibDir, 'upgrade-packs');
|
||
const distDir = path.join(hibDir, 'distribution-reports');
|
||
const deployed = fs.existsSync(hibDir);
|
||
let dailyCPs = 0, weeklySnaps = 0, upgradePacks = 0, distReports = 0;
|
||
let lastDaily = null, lastWeekly = null;
|
||
try {
|
||
if (fs.existsSync(cpDir)) {
|
||
const cpFiles = fs.readdirSync(cpDir).filter(f => f.startsWith('daily-cp-') && f.endsWith('.json')).sort();
|
||
dailyCPs = cpFiles.length;
|
||
if (cpFiles.length > 0) lastDaily = cpFiles[cpFiles.length - 1];
|
||
}
|
||
if (fs.existsSync(snapDir)) {
|
||
const snapFiles = fs.readdirSync(snapDir).filter(f => f.endsWith('.json')).sort();
|
||
weeklySnaps = snapFiles.length;
|
||
if (snapFiles.length > 0) lastWeekly = snapFiles[snapFiles.length - 1];
|
||
}
|
||
if (fs.existsSync(upDir)) {
|
||
upgradePacks = fs.readdirSync(upDir).filter(f => f.endsWith('.json')).length;
|
||
}
|
||
if (fs.existsSync(distDir)) {
|
||
distReports = fs.readdirSync(distDir).filter(f => f.endsWith('.json')).length;
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
return {
|
||
deployed,
|
||
daily_checkpoints: dailyCPs,
|
||
weekly_snapshots: weeklySnaps,
|
||
upgrade_packs: upgradePacks,
|
||
distribution_reports: distReports,
|
||
last_daily_checkpoint: lastDaily,
|
||
last_weekly_snapshot: lastWeekly
|
||
};
|
||
})(),
|
||
|
||
diagnosis: {
|
||
total_issues: diagnosis ? diagnosis.total_issues : 0,
|
||
auto_fixed: repairResult ? repairResult.total_repaired : 0,
|
||
needs_human: diagnosis ? diagnosis.needs_human : 0,
|
||
watching: diagnosis ? diagnosis.watch_list : 0,
|
||
issues: diagnosis ? (diagnosis.issues || []).map(i => {
|
||
const repairEntry = repairResult
|
||
? (repairResult.repairs || []).find(r => r.issue_id === i.id)
|
||
: null;
|
||
return {
|
||
id: i.id,
|
||
symptom: i.symptom,
|
||
root_cause: i.root_cause,
|
||
impact: i.impact,
|
||
fix_applied: i.fixable ? i.fix_plan : null,
|
||
verified: repairEntry ? repairEntry.verified : false
|
||
};
|
||
}) : []
|
||
},
|
||
|
||
repairs_applied: repairResult
|
||
? (repairResult.repairs || []).filter(r => r.success).map(r => r.symptom)
|
||
: [],
|
||
tickets_created: repairResult
|
||
? (repairResult.tickets || [])
|
||
: [],
|
||
path_activity: {
|
||
most_used_today: ['GH-WF-RUN-GATE', 'BRIDGE-GH2NT-SYSLOG'],
|
||
failed_paths: [],
|
||
new_paths_detected: []
|
||
},
|
||
next_actions: []
|
||
};
|
||
|
||
// 生成下一步建议
|
||
if (diagnosis && diagnosis.watch_list > 0) {
|
||
report.next_actions.push('持续观察 watch_list 中的问题');
|
||
}
|
||
if (report.brain_status.integrity !== 'ok') {
|
||
report.next_actions.push('核心大脑需要修复');
|
||
}
|
||
if (!report.brain_status.memory_fresh) {
|
||
report.next_actions.push('memory.json 数据需要刷新');
|
||
}
|
||
if (!report.security_health.protocol_exists) {
|
||
report.next_actions.push('P0: 安全协议文件缺失,需立即恢复');
|
||
} else if (!report.security_health.root_rules_intact) {
|
||
report.next_actions.push('P0: 安全协议根规则被篡改,需立即修复');
|
||
} else if (report.security_health.level !== 'L0' || !report.security_health.permanent) {
|
||
report.next_actions.push('P0: 安全协议L0等级或永久标记被修改');
|
||
} else if (!report.security_health.copyright_anchor) {
|
||
report.next_actions.push('P1: 安全协议版权锚点缺失,需补充');
|
||
}
|
||
if (report.bulletin_sfp_health && report.bulletin_sfp_health.invalid_fingerprints > 0) {
|
||
report.next_actions.push('P0: 公告板发现无效指纹内容,需立即清理');
|
||
}
|
||
if (report.soldier_health && report.soldier_health.failure_rate > 20) {
|
||
report.next_actions.push('P0: 小兵故障率超过20%,需全局修复');
|
||
}
|
||
if (report.soldier_health && report.soldier_health.recurring_errors &&
|
||
report.soldier_health.recurring_errors.length > 0) {
|
||
report.next_actions.push('P1: 存在持续性故障小兵,建议铸渊介入');
|
||
}
|
||
if (!report.hibernation_status.deployed) {
|
||
report.next_actions.push('P0: 休眠系统未部署,需立即部署 skyeye/hibernation/');
|
||
}
|
||
|
||
// 保存完整报告
|
||
fs.mkdirSync(SKYEYE_DIR, { recursive: true });
|
||
fs.writeFileSync(path.join(SKYEYE_DIR, 'full-report.json'), JSON.stringify(report, null, 2));
|
||
|
||
// 更新 memory.json 的 skyeye 段
|
||
updateMemory(report);
|
||
|
||
console.log(JSON.stringify(report, null, 2));
|
||
console.log(`\n🦅 天眼报告生成完毕 · ${report.report_id} · 整体健康: ${overallHealth}`);
|
||
}
|
||
|
||
// ━━━ 更新 memory.json ━━━
|
||
function updateMemory(report) {
|
||
try {
|
||
const mem = readJSON(MEMORY_PATH);
|
||
if (!mem) return;
|
||
|
||
// 添加 skyeye 段
|
||
mem.skyeye = {
|
||
last_run: report.timestamp,
|
||
report_id: report.report_id,
|
||
overall_health: report.overall_health,
|
||
issues_found: report.diagnosis.total_issues,
|
||
auto_fixed: report.diagnosis.auto_fixed,
|
||
needs_human: report.diagnosis.needs_human
|
||
};
|
||
|
||
mem.last_updated = new Date().toISOString();
|
||
|
||
// 添加天眼事件到 recent_events(检查重复)
|
||
const eventDate = new Date().toISOString().slice(0, 10);
|
||
const existingEvent = (mem.recent_events || []).find(
|
||
e => e.date === eventDate && e.type === 'skyeye_scan'
|
||
);
|
||
|
||
if (!existingEvent) {
|
||
if (!mem.recent_events) mem.recent_events = [];
|
||
mem.recent_events.unshift({
|
||
date: eventDate,
|
||
type: 'skyeye_scan',
|
||
description: `天眼扫描 · ${report.overall_health} · 问题:${report.diagnosis.total_issues} 自动修复:${report.diagnosis.auto_fixed} 需人工:${report.diagnosis.needs_human}`,
|
||
by: '天眼系统'
|
||
});
|
||
|
||
// 保持最近 20 条事件
|
||
if (mem.recent_events.length > 20) {
|
||
mem.recent_events = mem.recent_events.slice(0, 20);
|
||
}
|
||
}
|
||
|
||
fs.writeFileSync(MEMORY_PATH, JSON.stringify(mem, null, 2));
|
||
console.log('✅ memory.json 已更新天眼段');
|
||
} catch (e) {
|
||
console.error('⚠️ 更新 memory.json 失败:', e.message);
|
||
}
|
||
}
|
||
|
||
generateReport();
|