guanghulab/scripts/commander-dashboard.js
2026-05-10 13:12:44 +08:00

192 lines
5.8 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/commander-dashboard.js
// 铸渊·将军全局仪表盘生成器
//
// 功能:
// ① 拉取所有 Workflow 运行状态
// ② 读取天眼最新报告
// ③ 读取公告板状态
// ④ 生成将军全局仪表盘 JSON
//
// 输出data/bulletin-board/dashboard.json + stdout
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const BRAIN_DIR = path.join(ROOT, '.github/persona-brain');
const SKYEYE_REPORTS_DIR = path.join(ROOT, 'data/skyeye-reports');
const BULLETIN_DIR = path.join(ROOT, 'data/bulletin-board');
const DASHBOARD_PATH = path.join(BULLETIN_DIR, 'dashboard.json');
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
// ━━━ 安全读取 JSON ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
// ━━━ 获取最新天眼报告 ━━━
function getLatestSkyeyeReport() {
try {
if (!fs.existsSync(SKYEYE_REPORTS_DIR)) return null;
const files = fs.readdirSync(SKYEYE_REPORTS_DIR)
.filter(f => f.startsWith('skyeye-') && f.endsWith('.json'))
.sort()
.reverse();
if (files.length === 0) return null;
return readJSON(path.join(SKYEYE_REPORTS_DIR, files[0]));
} catch (e) {
return null;
}
}
// ━━━ 获取公告板工单状态 ━━━
function getBulletinBoardStatus() {
const workOrdersDir = path.join(BULLETIN_DIR, 'work-orders');
const receiptsDir = path.join(BULLETIN_DIR, 'receipts');
let unreadWorkOrders = 0;
const pendingReceipts = [];
try {
if (fs.existsSync(workOrdersDir)) {
const orders = fs.readdirSync(workOrdersDir)
.filter(f => f.endsWith('.json'));
for (const orderFile of orders) {
const order = readJSON(path.join(workOrdersDir, orderFile));
if (order && order.status !== 'completed') {
unreadWorkOrders++;
pendingReceipts.push(order.order_id || orderFile.replace('.json', ''));
}
}
}
} catch (e) {
// ignore
}
return {
unread_work_orders: unreadWorkOrders,
pending_receipts: pendingReceipts
};
}
// ━━━ 从天眼报告提取小兵状态 ━━━
function extractSoldiersFromReport(report) {
const soldiers = [];
if (!report) return { soldiers, total: 0, healthy: 0, failed: 0, needs_optimization: 0 };
// 从 workflow_health 提取小兵
if (report.workflow_health && report.workflow_health.details) {
for (const wf of report.workflow_health.details) {
const status = wf.status === 'healthy' ? '✅' :
wf.status === 'failed' ? '❌' : '⚠️';
const soldier = {
name: wf.name || wf.file,
status,
last_run: wf.last_run || 'unknown'
};
if (status === '❌') {
soldier.error = wf.error || 'unknown error';
soldier.fix_plan = 'retry + check logs';
}
soldiers.push(soldier);
}
}
const total = soldiers.length;
const healthy = soldiers.filter(s => s.status === '✅').length;
const failed = soldiers.filter(s => s.status === '❌').length;
const needsOpt = soldiers.filter(s => s.status === '⚠️').length;
return { soldiers, total, healthy, failed, needs_optimization: needsOpt };
}
// ━━━ 生成决策摘要 ━━━
function generateDecision(soldierStats, bulletinStatus, skyeyeReport) {
const actions = [];
if (soldierStats.failed > 0) {
actions.push(`修复 ${soldierStats.failed} 个故障小兵`);
}
if (soldierStats.needs_optimization > 0) {
actions.push(`优化 ${soldierStats.needs_optimization} 个小兵`);
}
if (bulletinStatus.unread_work_orders > 0) {
actions.push(`执行 ${bulletinStatus.unread_work_orders} 个未完成工单`);
}
if (skyeyeReport && skyeyeReport.diagnosis && skyeyeReport.diagnosis.needs_human > 0) {
actions.push(`${skyeyeReport.diagnosis.needs_human} 个问题需人工处理`);
}
if (actions.length === 0) {
return '全局健康 · 无需干预 · 继续巡航';
}
return actions.join(' → ');
}
// ━━━ 主函数 ━━━
function generateDashboard() {
const now = new Date();
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString()
.replace('T', ' ').slice(0, 19) + '+08:00';
// 读取天眼最新报告
const skyeyeReport = getLatestSkyeyeReport();
// 提取小兵状态
const soldierStats = extractSoldiersFromReport(skyeyeReport);
// 读取公告板状态
const bulletinStatus = getBulletinBoardStatus();
// 生成决策
const decision = generateDecision(soldierStats, bulletinStatus, skyeyeReport);
const dashboard = {
commander: '铸渊',
timestamp: bjTime,
global_view: {
total_soldiers: soldierStats.total,
healthy: soldierStats.healthy,
failed: soldierStats.failed,
needs_optimization: soldierStats.needs_optimization,
soldiers: soldierStats.soldiers
},
bulletin_board_status: bulletinStatus,
skyeye_latest: skyeyeReport ? {
overall_health: skyeyeReport.overall_health || '❓',
report_id: skyeyeReport.report_id || null,
issues: skyeyeReport.diagnosis ? skyeyeReport.diagnosis.total_issues : 0,
auto_fixed: skyeyeReport.diagnosis ? skyeyeReport.diagnosis.auto_fixed : 0
} : {
overall_health: '❓',
report_id: null,
issues: 0,
auto_fixed: 0
},
decision
};
// 保存仪表盘
fs.mkdirSync(BULLETIN_DIR, { recursive: true });
fs.writeFileSync(DASHBOARD_PATH, JSON.stringify(dashboard, null, 2));
console.log(JSON.stringify(dashboard, null, 2));
console.log(`\n📊 将军全局仪表盘已生成 · ${bjTime}`);
}
// 允许作为模块导入或直接运行
if (require.main === module) {
generateDashboard();
}
module.exports = { generateDashboard };