335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
#!/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;
|
||
}
|
||
}
|
||
|
||
// ━━━ 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();
|
||
|
||
// 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('');
|
||
|
||
// 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');
|
||
}
|
||
|
||
// ━━━ README 更新 ━━━
|
||
|
||
function updateReadme(markdown) {
|
||
if (!fs.existsSync(README_PATH)) {
|
||
console.error('❌ README.md 不存在');
|
||
return false;
|
||
}
|
||
|
||
let readme = fs.readFileSync(README_PATH, 'utf8');
|
||
|
||
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;
|
||
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()
|
||
};
|
||
}
|
||
|
||
// ━━━ 入口 ━━━
|
||
|
||
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();
|