guanghulab/scripts/skyeye/report-generator.js
2026-05-10 13:12:44 +08:00

301 lines
11 KiB
JavaScript
Raw 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.

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