guanghulab/scripts/skyeye/report-generator.js

301 lines
11 KiB
JavaScript
Raw Normal View History

// 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();