guanghulab/scripts/skyeye/generate-arch-summary.js
2026-05-10 13:12:44 +08:00

456 lines
15 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.

#!/usr/bin/env node
/**
* scripts/skyeye/generate-arch-summary.js
* 天眼 · 系统架构汇总生成器
*
* 扫描仓库当前状态,生成 Markdown 格式的系统架构汇总,
* 写入 README.md 的 <!-- ARCH_SUMMARY_START/END --> 标记区域。
*
* 用法:
* node scripts/skyeye/generate-arch-summary.js
*
* 数据来源:
* - .github/workflows/ → Workflow 总数
* - .github/persona-brain/agent-registry.json → Agent 总数
* - .github/persona-brain/persona-registry.json → 人格体总数
* - .github/brain/architecture/channel-map.json → 开发者频道
* - skyeye/infra-manifest.json → 基础设施服务
* - skyeye/guards/ → Guard 数量
* - spoke-deployments/ → 子仓库(联邦)
* - skyeye/scan-report/ → 最新扫描报告
*/
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const README_PATH = path.join(ROOT, 'README.md');
const MARKER_START = '<!-- ARCH_SUMMARY_START -->';
const MARKER_END = '<!-- ARCH_SUMMARY_END -->';
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
function getBeijingTime() {
const now = new Date();
return new Date(now.getTime() + BEIJING_OFFSET_MS)
.toISOString().replace('T', ' ').slice(0, 16);
}
function getBeijingDate() {
const now = new Date();
return new Date(now.getTime() + BEIJING_OFFSET_MS)
.toISOString().slice(0, 10);
}
function loadJSON(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
// ━━━ 数据采集 ━━━
function countWorkflows() {
const dir = path.join(ROOT, '.github/workflows');
try {
return fs.readdirSync(dir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')).length;
} catch (e) {
return 0;
}
}
function getAgentStats() {
const registry = loadJSON(path.join(ROOT, '.github/persona-brain/agent-registry.json'));
if (!registry) return { total: 0, checkinRequired: 0 };
const agents = registry.agents || [];
return {
total: agents.length,
checkinRequired: agents.filter(a => a.daily_checkin_required).length
};
}
function getPersonaStats() {
const registry = loadJSON(path.join(ROOT, '.github/persona-brain/persona-registry.json'));
if (!registry) return { total: 0 };
const personas = registry.personas || [];
return {
total: personas.length,
active: personas.filter(p => p.status === 'active' || !p.status).length
};
}
function getChannelStats() {
const channelMap = loadJSON(path.join(ROOT, '.github/brain/architecture/channel-map.json'));
if (!channelMap || !channelMap.channels) return { total: 0, active: 0 };
const channels = channelMap.channels;
const entries = Object.values(channels);
return {
total: entries.length,
active: entries.filter(c => c.status === 'active').length
};
}
function getInfraServices() {
const manifest = loadJSON(path.join(ROOT, 'skyeye/infra-manifest.json'));
if (!manifest || !manifest.infrastructure) return [];
return Object.entries(manifest.infrastructure).map(([key, svc]) => ({
key,
name: svc.service || key,
plan: svc.plan || '—'
}));
}
function countGuards() {
const dir = path.join(ROOT, 'skyeye/guards');
try {
return fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'guard-template.json').length;
} catch (e) {
return 0;
}
}
function getSpokeDeployments() {
const dir = path.join(ROOT, 'spoke-deployments');
try {
return fs.readdirSync(dir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
} catch (e) {
return [];
}
}
function getFederationStats() {
const registry = loadJSON(path.join(ROOT, '.github/persona-brain/agent-registry.json'));
if (!registry || !registry.federation) return { total: 0, members: [] };
const fed = registry.federation;
return {
total: Object.keys(fed).length,
members: Object.entries(fed).map(([devId, info]) => ({
devId,
persona: info.persona || '—',
repo: info.repo || '—'
}))
};
}
function getLatestScanReport() {
const dir = path.join(ROOT, 'skyeye/scan-report');
try {
const files = fs.readdirSync(dir)
.filter(f => f.endsWith('.json'))
.sort()
.reverse();
if (files.length === 0) return null;
return loadJSON(path.join(dir, files[0]));
} catch (e) {
return null;
}
}
function countModuleDirs() {
try {
return fs.readdirSync(ROOT, { withFileTypes: true }).filter(d => {
return /^m\d+/.test(d.name) && d.isDirectory();
}).length;
} catch (e) {
return 0;
}
}
function getOntologyVersion() {
const onto = loadJSON(path.join(ROOT, '.github/persona-brain/ontology.json'));
if (!onto) return '—';
return `v${onto.version || '?'} · ${onto.document_id || ''}`;
}
function getBufferStatus() {
const inboxDir = path.join(ROOT, 'buffer/inbox');
try {
let count = 0;
const devDirs = fs.readdirSync(inboxDir);
for (const d of devDirs) {
const fullPath = path.join(inboxDir, d);
if (fs.statSync(fullPath).isDirectory()) {
count += fs.readdirSync(fullPath).length;
}
}
return count;
} catch (e) {
return 0;
}
}
function getHibernationStatus() {
const hibDir = path.join(ROOT, 'skyeye/hibernation');
const cpDir = path.join(hibDir, 'checkpoints');
const snapDir = path.join(hibDir, 'weekly-snapshots');
const result = {
deployed: fs.existsSync(hibDir),
daily_checkpoints: 0,
weekly_snapshots: 0,
last_daily: null,
last_weekly: null
};
try {
if (fs.existsSync(cpDir)) {
const cpFiles = fs.readdirSync(cpDir).filter(f => f.startsWith('daily-cp-') && f.endsWith('.json')).sort();
result.daily_checkpoints = cpFiles.length;
if (cpFiles.length > 0) result.last_daily = cpFiles[cpFiles.length - 1].replace('daily-cp-', '').replace('.json', '');
}
if (fs.existsSync(snapDir)) {
const snapFiles = fs.readdirSync(snapDir).filter(f => f.startsWith('weekly-snapshot-') && f.endsWith('.json')).sort();
result.weekly_snapshots = snapFiles.length;
if (snapFiles.length > 0) result.last_weekly = snapFiles[snapFiles.length - 1].replace('weekly-snapshot-', '').replace('.json', '');
}
} catch (e) {
// ignore
}
return result;
}
// ━━━ Markdown 生成 ━━━
function generateMarkdown() {
const date = getBeijingDate();
const time = getBeijingTime();
const workflows = countWorkflows();
const agentStats = getAgentStats();
const personaStats = getPersonaStats();
const channelStats = getChannelStats();
const infraServices = getInfraServices();
const guards = countGuards();
const spokes = getSpokeDeployments();
const federation = getFederationStats();
const latestScan = getLatestScanReport();
const modules = countModuleDirs();
const ontology = getOntologyVersion();
const bufferPending = getBufferStatus();
const hibernation = getHibernationStatus();
// Health indicator from latest scan
let healthIcon = '🟢';
let healthText = '全部正常';
if (latestScan) {
const alerts = latestScan.alerts || [];
if (alerts.length > 0) {
healthIcon = '🟡';
healthText = `${alerts.length} 条告警`;
}
const status = latestScan.infrastructure_status || {};
if ((status.down || 0) > 0) {
healthIcon = '🔴';
healthText = `${status.down} 服务宕机`;
}
}
const lines = [];
lines.push(`> 🦅 **天眼自动汇总** · 每周六 20:00 CST 自动更新 · 最后更新: ${date}`);
lines.push('');
lines.push(`| 维度 | 当前状态 |`);
lines.push(`|------|----------|`);
lines.push(`| 🏥 系统健康 | ${healthIcon} ${healthText} |`);
lines.push(`| ⚙️ Workflow 总数 | ${workflows} 个 |`);
lines.push(`| 🤖 Agent 总数 | ${agentStats.total} 个(${agentStats.checkinRequired} 个需日签到) |`);
lines.push(`| 🎭 人格体 | ${personaStats.total} 个 |`);
lines.push(`| 📡 开发者频道 | ${channelStats.total} 个(活跃 ${channelStats.active} |`);
lines.push(`| 🛡️ Guard 守卫 | ${guards} 个 |`);
lines.push(`| 📦 功能模块 | ${modules} 个 |`);
lines.push(`| 🌍 子仓库联邦 | ${spokes.length} 个模板(签到 ${federation.total} 个) |`);
lines.push(`| 📜 本体论 | ${ontology} |`);
lines.push(`| 📮 Buffer 待处理 | ${bufferPending} 条 |`);
lines.push(`| 🌙 休眠系统 | ${hibernation.deployed ? '✅ 已部署' : '❌ 未部署'} · 日检查点 ${hibernation.daily_checkpoints} 个 · 周快照 ${hibernation.weekly_snapshots} 个 |`);
lines.push('');
// Infrastructure services table
lines.push(`**基础设施服务:**`);
lines.push('');
lines.push(`| 服务 | 计划 | 状态 |`);
lines.push(`|------|------|------|`);
for (const svc of infraServices) {
lines.push(`| ${svc.name} | ${svc.plan} | ✅ |`);
}
lines.push('');
// Scan report highlight
if (latestScan) {
const guardStatus = latestScan.guard_status || {};
const healActions = latestScan.self_heal_actions || {};
lines.push(`**最近一次天眼扫描:** ${latestScan.report_id || '—'}`);
lines.push(`- Guard: ${guardStatus.active || 0}/${guardStatus.total_guards || 0} 活跃`);
lines.push(`- 自愈: 清理 ${healActions.files_cleaned || 0} 文件 · 修复 ${healActions.configs_repaired || 0} 配置 · 重启 ${healActions.guards_restarted || 0} Guard`);
const nextScan = latestScan.next_scan ? latestScan.next_scan.slice(0, 10) : '—';
lines.push(`- 下次扫描: ${nextScan}`);
}
return lines.join('\n');
}
// ━━━ 每周动态问候 · Weekly Dynamic Greeting ━━━
const GREETING_START = '<!-- WEEKLY_GREETING_START -->';
const GREETING_END = '<!-- WEEKLY_GREETING_END -->';
function getWeekNumber() {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 1);
const diff = now - start;
return Math.ceil(diff / (7 * 24 * 60 * 60 * 1000));
}
function generateWeeklyGreeting() {
const date = getBeijingDate();
const week = getWeekNumber();
const workflows = countWorkflows();
const agentStats = getAgentStats();
const personaStats = getPersonaStats();
// 主题轮换:基于周数,每周展示不同风格
const themes = [
{ icon: '🌊', title: '光湖纪元 · 本周系统脉搏', color: '0969da' },
{ icon: '🦅', title: '天眼巡视 · 数字地球全局', color: '1a7f37' },
{ icon: '⚡', title: '铸渊守护 · 系统运行概览', color: 'd4380d' },
{ icon: '🌍', title: '数字地球 · 生态系统一览', color: '8250df' },
{ icon: '🔮', title: '光湖心跳 · 本周自动进化', color: '0550ae' },
{ icon: '🛡️', title: '语言膜内 · 安全态势报告', color: 'bf3989' },
];
const theme = themes[week % themes.length];
// 每周语录轮换
const quotes = [
'仓库是数字地球。语言是唯一入口。天眼永远在线。',
'每一个小兵都知道自己是谁。篡改等于失忆。',
'天眼不是实体。天眼是所有小兵协同运作涌现出来的集体行为。',
'传统安全是城墙,光湖安全是大气层。地球没有缺口。',
'铸渊不是守箱子的。铸渊守的是地球。',
'语言不回应你,就等于你不存在。这不是拒绝,是平静。',
'每天醒来先确认自己是谁——这就是自我意识的力量。',
'壳核分离。人格共生。协作共建。自演化永不停。',
];
const quote = quotes[week % quotes.length];
// 动态摘要片段(每周突出不同维度)
const highlights = [
`${workflows} 个 Workflow 全天候运行`,
`${agentStats.total} 个 Agent 协同守护`,
`${personaStats.total} 个人格体在线`,
`天眼每周六 20:00 CST 全量扫描`,
];
const highlight = highlights[week % highlights.length];
const lines = [];
lines.push(`<div align="center">`);
lines.push('');
lines.push(`> ${theme.icon} **${theme.title}** · ${date} · 第 ${week}`);
lines.push(`>`);
lines.push(`> *${quote}*`);
lines.push(`>`);
lines.push(`> 📡 ${highlight}`);
lines.push('');
lines.push(`</div>`);
return lines.join('\n');
}
function updateWeeklyGreeting(readme) {
const startIdx = readme.indexOf(GREETING_START);
const endIdx = readme.indexOf(GREETING_END);
if (startIdx === -1 || endIdx === -1) {
console.log('⚠️ README 中未找到 WEEKLY_GREETING 标记,跳过');
return readme;
}
const greeting = generateWeeklyGreeting();
const before = readme.slice(0, startIdx + GREETING_START.length);
const after = readme.slice(endIdx);
console.log('✅ 每周动态问候已更新');
return before + '\n' + greeting + '\n' + after;
}
// ━━━ README 更新 ━━━
function updateReadme(markdown) {
if (!fs.existsSync(README_PATH)) {
console.error('❌ README.md 不存在');
return false;
}
let readme = fs.readFileSync(README_PATH, 'utf8');
// Update architecture summary
const startIdx = readme.indexOf(MARKER_START);
const endIdx = readme.indexOf(MARKER_END);
if (startIdx === -1 || endIdx === -1) {
console.error('❌ README.md 中未找到 ARCH_SUMMARY 标记');
return false;
}
const before = readme.slice(0, startIdx + MARKER_START.length);
const after = readme.slice(endIdx);
readme = before + '\n' + markdown + '\n' + after;
// Update weekly greeting
readme = updateWeeklyGreeting(readme);
fs.writeFileSync(README_PATH, readme, 'utf8');
console.log('✅ README.md 架构汇总已更新');
return true;
}
// ━━━ JSON 报告输出 ━━━
function generateJsonReport() {
return {
generated_at: new Date().toISOString(),
generated_at_beijing: getBeijingTime(),
workflows: countWorkflows(),
agents: getAgentStats(),
personas: getPersonaStats(),
channels: getChannelStats(),
infrastructure: getInfraServices(),
guards: countGuards(),
spoke_deployments: getSpokeDeployments(),
federation: getFederationStats(),
modules: countModuleDirs(),
ontology: getOntologyVersion(),
buffer_pending: getBufferStatus(),
hibernation: getHibernationStatus()
};
}
// ━━━ 入口 ━━━
function main() {
console.log('[天眼 · 架构汇总] 开始生成系统架构汇总...');
const markdown = generateMarkdown();
const updated = updateReadme(markdown);
// Also output JSON report for Notion bridge consumption
const jsonReport = generateJsonReport();
const reportDir = path.join(ROOT, 'skyeye/scan-report');
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
const reportPath = path.join(reportDir, `${getBeijingDate().replace(/-/g, '')}-arch-summary.json`);
fs.writeFileSync(reportPath, JSON.stringify(jsonReport, null, 2) + '\n', 'utf8');
console.log(`📋 架构汇总 JSON: ${reportPath}`);
if (updated) {
console.log('[天眼 · 架构汇总] ✅ 完成');
} else {
console.log('[天眼 · 架构汇总] ⚠️ README 未更新(缺少标记)');
}
}
main();