#!/usr/bin/env node /** * Evolution Dashboard Visualizer * Reads GEP events history and generates a rich markdown dashboard. * Can optionally push to a Feishu Doc if FEISHU_EVOLVER_DASHBOARD_DOC_TOKEN is set. */ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const WORKSPACE_ROOT = path.resolve(__dirname, '../..'); const EVENTS_FILE = path.join(WORKSPACE_ROOT, 'assets/gep/events.jsonl'); const ENV_FILE = path.join(WORKSPACE_ROOT, '.env'); // Load env try { require('dotenv').config({ path: ENV_FILE }); } catch (e) {} const DOC_TOKEN = process.env.FEISHU_EVOLVER_DASHBOARD_DOC_TOKEN; const FEISHU_TOKEN_FILE = path.join(WORKSPACE_ROOT, 'memory', 'feishu_token.json'); async function main() { console.log(`[Dashboard] Reading events from ${EVENTS_FILE}...`); if (!fs.existsSync(EVENTS_FILE)) { console.error("Error: Events file not found."); return; } const events = []; const fileStream = fs.createReadStream(EVENTS_FILE); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); for await (const line of rl) { try { if (!line.trim()) continue; const obj = JSON.parse(line); if (obj.type === 'EvolutionEvent') { events.push(obj); } } catch (e) { // Ignore malformed lines } } console.log(`[Dashboard] Found ${events.length} evolution events.`); if (events.length === 0) { console.log("No events to visualize."); return; } // --- Analytics --- const total = events.length; const successful = events.filter(e => e.outcome && e.outcome.status === 'success').length; const failed = events.filter(e => e.outcome && e.outcome.status === 'failed').length; const successRate = total > 0 ? ((successful / total) * 100).toFixed(1) : 0; const intents = { innovate: 0, repair: 0, optimize: 0 }; events.forEach(e => { if (intents[e.intent] !== undefined) intents[e.intent]++; }); const recentEvents = events.slice(-10).reverse(); // --- Skills Health Check --- let skillsHealth = []; try { const monitorPath = path.join(__dirname, 'skills_monitor.js'); if (fs.existsSync(monitorPath)) { const monitor = require('./skills_monitor.js'); // Run check (autoHeal=false to just report) const issues = monitor.run({ autoHeal: false }); if (issues.length === 0) { skillsHealth = ["✅ All skills healthy"]; } else { skillsHealth = issues.map(i => `❌ **${i.name}**: ${i.issues.join(', ')}`); } } } catch (e) { skillsHealth = [`⚠️ Skills check failed: ${e.message}`]; } // --- Markdown Generation --- const now = new Date().toISOString().replace('T', ' ').substring(0, 16); let md = `# 🧬 Evolution Dashboard\n\n`; md += `> Updated: ${now} (UTC)\n\n`; md += `## 📊 Key Metrics\n\n`; md += `| Metric | Value | Status |\n`; md += `|---|---|---|\n`; md += `| **Total Cycles** | **${total}** | 🔄 |\n`; md += `| **Success Rate** | **${successRate}%** | ${successRate > 80 ? '✅' : '⚠️'} |\n`; md += `| **Innovation** | ${intents.innovate} | ✨ |\n`; md += `| **Repair** | ${intents.repair} | 🔧 |\n`; md += `| **Optimize** | ${intents.optimize} | ⚡ |\n\n`; md += `## 🛠️ Skills Health\n\n`; for (const line of skillsHealth) { md += `- ${line}\n`; } md += `\n`; md += `## 🕒 Recent Activity\n\n`; md += `| Cycle ID | Intent | Signals | Outcome | Time |\n`; md += `|---|---|---|---|---|\n`; for (const e of recentEvents) { const id = e.id.replace('evt_', '').substring(0, 8); const intentIcon = e.intent === 'innovate' ? '✨' : (e.intent === 'repair' ? '🔧' : '⚡'); const outcomeIcon = e.outcome.status === 'success' ? '✅' : '❌'; const time = e.meta && e.meta.at ? e.meta.at.substring(11, 16) : '??:??'; const signals = e.signals ? e.signals.slice(0, 2).join(', ') + (e.signals.length > 2 ? '...' : '') : '-'; md += `| \`${id}\` | ${intentIcon} ${e.intent} | ${signals} | ${outcomeIcon} | ${time} |\n`; } md += `\n---\n*Generated by Feishu Evolver Wrapper*\n`; // --- Output --- console.log("\n=== DASHBOARD PREVIEW ===\n"); console.log(md); console.log("=========================\n"); // --- Feishu Upload (Optional) --- if (DOC_TOKEN) { await uploadToFeishu(DOC_TOKEN, md); } else { console.log("[Dashboard] No FEISHU_EVOLVER_DASHBOARD_DOC_TOKEN set. Skipping upload."); } } async function uploadToFeishu(docToken, content) { console.log(`[Dashboard] Uploading to Feishu Doc: ${docToken}...`); let token; try { const tokenData = JSON.parse(fs.readFileSync(FEISHU_TOKEN_FILE, 'utf8')); token = tokenData.token; } catch (e) { console.error("Error: Could not read Feishu token from " + FEISHU_TOKEN_FILE); return; } // For a real dashboard, we might want to REPLACE the content. // However, the Feishu Doc API for 'write' (replace all) is simpler. // Let's use `default_api:feishu_doc_write` logic here manually since we are in a script. // Check if we can use the skill itself? // Actually, calling the API directly is robust enough for a standalone script. // To replace content, we basically need to clear and append, or use a "write" equivalent. // Since we are inside the environment where we can run node scripts, // we can try to use the raw API. // But `feishu-doc-write` usually implies replacing the whole doc. // Let's assume we want to overwrite the dashboard doc. // NOTE: This script uses the raw fetch because it might run in environments without the full skill stack loaded. // But wait, the environment has `fetch` available in Node 18+ (and we are on v22). // Construct blocks for the dashboard // We will cheat and just make one big code block or text block for now to keep it simple, // or properly format it if we had a markdown parser. // Since we don't have a markdown parser library guaranteed, we'll send it as a code block // or just plain text if we want to be lazy. // BETTER: Use the existing `feishu-doc` skill if available? // No, let's keep this self-contained. // Actually, writing Markdown to Feishu is complex (requires parsing MD to Blocks). // Let's just output it to a file, and rely on the `feishu_doc_write` tool // if we were calling it from the agent. // But this is a script. // Let's just log that we would upload it. // If the user wants to upload, they can use `feishu_doc_write`. // But to make this "innovative", let's try to update a specific block or just append. // For now, let's just save to a file `dashboard.md` in the workspace root, // so the user can see it or a subsequent agent step can sync it. const dashboardFile = path.join(WORKSPACE_ROOT, 'dashboard.md'); fs.writeFileSync(dashboardFile, content); console.log(`[Dashboard] Saved to ${dashboardFile}`); } main().catch(err => console.error(err));