guanghulab/scripts/generate-repo-map.js

530 lines
19 KiB
JavaScript
Raw Permalink Normal View History

// scripts/generate-repo-map.js
// 铸渊图书馆目录生成器 · Library Catalog Generator
//
// 功能:
// 1. 扫描整个仓库结构
// 2. 生成 .github/brain/repo-map.json ← 机器可读路由索引
// 3. 生成 .github/brain/repo-snapshot.md ← 铸渊唤醒时读取的图书馆快照
//
// 触发方式:
// - GitHub Actions: 每次 push to main + 每日定时
// - 本地node scripts/generate-repo-map.js
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const BRAIN_DIR = path.join(ROOT, '.github/brain');
const MAP_PATH = path.join(BRAIN_DIR, 'repo-map.json');
const SNAPSHOT_PATH = path.join(BRAIN_DIR, 'repo-snapshot.md');
const now = new Date();
const nowISO = now.toISOString();
const nowDate = nowISO.slice(0, 10);
const nowBJ = new Date(now.getTime() + 8 * 3600 * 1000).toISOString()
.replace('T', ' ').slice(0, 16) + ' CST';
// ── 工具函数 ────────────────────────────────────────────────────────────────
function safeReadJson(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (err) {
if (err.code !== 'ENOENT') {
console.warn(`⚠️ JSON读取失败: ${filePath}${err.message}`);
}
return null;
}
}
function existsDir(p) {
try { return fs.statSync(p).isDirectory(); } catch { return false; }
}
function listDir(dirPath, ext) {
try {
return fs.readdirSync(dirPath)
.filter(f => !ext || f.endsWith(ext))
.filter(f => !f.startsWith('.') || f === '.gitkeep');
} catch { return []; }
}
function countFiles(dirPath, ext) {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
let count = 0;
for (const e of entries) {
if (e.isDirectory()) {
count += countFiles(path.join(dirPath, e.name), ext);
} else if (!ext || e.name.endsWith(ext)) {
count++;
}
}
return count;
} catch { return 0; }
}
// ── 区域定义 ──────────────────────────────────────────────────────────────
// 每个"区域"对应图书馆里的一个大区section
// 每个区域下的文件是"书架shelf"上的"书"
const ZONE_DEFS = [
{
zone_id: 'BRAIN',
name: '铸渊大脑',
emoji: '🧠',
path: '.github/brain',
description: '铸渊核心记忆 · 路由映射 · 唤醒协议 · 图书馆目录',
keywords: ['brain', 'memory', 'routing', 'wake', '大脑', '记忆', '路由']
},
{
zone_id: 'PERSONA_BRAIN',
name: '人格大脑',
emoji: '🎭',
path: '.github/persona-brain',
description: '铸渊人格记忆 · 开发者状态 · 知识库 · 成长日记',
keywords: ['persona', 'identity', 'dev-status', '人格', '开发者状态']
},
{
zone_id: 'WORKFLOWS',
name: '自动化工作流',
emoji: '⚡',
path: '.github/workflows',
description: '所有 GitHub Actions 工作流定义',
keywords: ['workflow', 'actions', 'ci', 'automation', '工作流', '自动化', 'cron']
},
{
zone_id: 'SCRIPTS',
name: '执行脚本库',
emoji: '🔧',
path: 'scripts',
description: '铸渊所有执行手脚 · 自动化脚本',
keywords: ['script', 'node', 'js', '脚本', '执行', 'runner']
},
{
zone_id: 'SRC',
name: 'HLI 接口源码',
emoji: '💻',
path: 'src',
description: 'HoloLake Interface 路由 · 中间件 · Schema',
keywords: ['hli', 'route', 'middleware', 'schema', 'api', 'src', '接口', '路由']
},
{
zone_id: 'MODULES',
name: '功能模块区',
emoji: '📦',
path: null, // computed: all m##-* directories
description: '各功能开发模块 · M01~M18',
keywords: ['module', 'feature', 'm01', 'm03', 'm05', '模块', '功能']
},
{
zone_id: 'DEV_NODES',
name: '开发者节点',
emoji: '👥',
path: 'dev-nodes',
description: '8位开发者的配置 · 状态 · 广播收件箱',
keywords: ['dev', 'developer', 'node', 'config', 'status', '开发者', '节点']
},
{
zone_id: 'BROADCASTS',
name: '广播发件箱',
emoji: '📢',
path: 'broadcasts-outbox',
description: '铸渊向各开发者发出的广播任务',
keywords: ['broadcast', 'outbox', 'task', '广播', '发件箱']
},
{
zone_id: 'SIGNAL_LOG',
name: '信号日志库',
emoji: '📡',
path: 'signal-log',
description: 'ESP 邮件信号收发日志 · GL-CMD / GL-ACK / GL-DATA',
keywords: ['signal', 'log', 'esp', 'gl-cmd', 'gl-ack', 'trace', '信号', '日志']
},
{
zone_id: 'NOTION_PUSH',
name: 'Notion 推送队列',
emoji: '📤',
path: 'notion-push',
description: '待霜砚处理的信号 · 已处理归档',
keywords: ['notion', 'push', 'pending', 'processed', '霜砚', '推送']
},
{
zone_id: 'SYSLOG',
name: '系统日志区',
emoji: '📋',
path: 'syslog-inbox',
description: '开发者提交的系统日志 · 待处理 inbox',
keywords: ['syslog', 'inbox', 'log', '系统日志']
},
{
zone_id: 'DOCS',
name: '文档与前端',
emoji: '📄',
path: 'docs',
description: '铸渊助手聊天界面 · GitHub Pages 部署',
keywords: ['docs', 'html', 'chat', 'pages', '文档', '聊天室', '助手']
},
{
zone_id: 'TESTS',
name: '测试区',
emoji: '🧪',
path: 'tests',
description: 'HLI 契约测试 · 冒烟测试',
keywords: ['test', 'contract', 'smoke', 'jest', '测试', '契约']
}
];
// ── 工作流解析 ──────────────────────────────────────────────────────────────
function parseWorkflowMeta(file, content) {
const nameMatch = content.match(/^name:\s*(.+)/m);
const schedMatch = content.match(/cron:\s*['"]?([^'"#\n]+)/);
// Handle both block (`on:\n push:`) and inline (`on: [push, pull_request]`) syntax
const onInline = content.match(/^on:\s*\[([^\]]+)\]/m);
const onBlock = content.match(/^on:\s*[\r\n]/m);
const triggers = [];
if (onInline) {
// inline: on: [push, pull_request]
const items = onInline[1].split(',').map(s => s.trim());
triggers.push(...items);
} else if (onBlock) {
if (/^\s{2,4}push:/m.test(content)) triggers.push('push');
if (/^\s{2,4}pull_request:/m.test(content)) triggers.push('pull_request');
if (/^\s{2,4}issues:/m.test(content)) triggers.push('issues');
if (/^\s{2,4}issue_comment:/m.test(content)) triggers.push('issue_comment');
}
if (schedMatch) triggers.push(`schedule(${schedMatch[1].trim()})`);
if (content.includes('workflow_dispatch')) triggers.push('manual');
return {
file,
name: nameMatch ? nameMatch[1].trim() : file,
triggers: triggers.length ? triggers : ['unknown']
};
}
function buildWorkflowShelves() {
const wfDir = path.join(ROOT, '.github/workflows');
const files = listDir(wfDir, '.yml');
return files.map(f => {
try {
const content = fs.readFileSync(path.join(wfDir, f), 'utf8');
return parseWorkflowMeta(f, content);
} catch {
return { file: f, name: f, triggers: ['unknown'] };
}
});
}
// ── 模块目录扫描 ────────────────────────────────────────────────────────────
function buildModuleShelves() {
const entries = fs.readdirSync(ROOT, { withFileTypes: true });
return entries
.filter(e => e.isDirectory() && /^m\d{2}-/.test(e.name))
.map(e => {
const modPath = path.join(ROOT, e.name);
const files = listDir(modPath);
const readme = files.find(f => f.toLowerCase() === 'readme.md');
return {
module_id: e.name.slice(0, 3).toUpperCase(), // "m01" → "M01"
dir: e.name,
files: files.length,
has_readme: !!readme
};
})
.sort((a, b) => a.dir.localeCompare(b.dir));
}
// ── HLI 接口统计 ────────────────────────────────────────────────────────────
function buildHLIStats(routingMap) {
if (!routingMap) return { total: 0, implemented: 0, pending: 0, domains: [] };
let total = 0, implemented = 0;
const domains = [];
for (const [name, data] of Object.entries(routingMap.domains || {})) {
const t = data.interfaces.length;
const i = data.interfaces.filter(x => x.status === 'implemented').length;
total += t;
implemented += i;
domains.push({
name,
module: data.module,
prefix: data.route_prefix,
total: t,
implemented: i,
interfaces: data.interfaces.map(x => ({
id: x.id,
path: x.path,
status: x.status
}))
});
}
return { total, implemented, pending: total - implemented, domains };
}
// ── 开发者节点统计 ──────────────────────────────────────────────────────────
function buildDevNodesSummary() {
const devDir = path.join(ROOT, 'dev-nodes');
if (!existsDir(devDir)) return [];
return fs.readdirSync(devDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('DEV-'))
.map(e => {
const cfg = safeReadJson(path.join(devDir, e.name, 'config.json')) || {};
const sts = safeReadJson(path.join(devDir, e.name, 'status.json')) || {};
return {
dev_id: e.name,
name: cfg.name || '?',
emoji: cfg.emoji || '',
modules: cfg.modules || [],
last_push: sts.last_push || null,
pending_broadcasts: sts.pending_broadcasts || 0
};
})
.sort((a, b) => a.dev_id.localeCompare(b.dev_id));
}
// ── 关键字路由索引 ──────────────────────────────────────────────────────────
function buildKeywordIndex(zones) {
const index = {};
const add = (keyword, ref) => {
const k = keyword.toLowerCase();
if (!index[k]) index[k] = [];
if (!index[k].includes(ref)) index[k].push(ref);
};
for (const zone of zones) {
for (const kw of (zone.keywords || [])) {
add(kw, zone.zone_id);
}
for (const shelf of (zone.shelves || [])) {
const basename = (shelf.file || shelf.dir || '').replace(/\.[^.]+$/, '');
add(basename, `${zone.zone_id}::${shelf.file || shelf.dir}`);
}
}
return index;
}
// ── 主生成逻辑 ──────────────────────────────────────────────────────────────
function buildZoneData() {
const routingMap = safeReadJson(path.join(BRAIN_DIR, 'routing-map.json'));
const memory = safeReadJson(path.join(BRAIN_DIR, 'memory.json'));
const hli = buildHLIStats(routingMap);
const devNodes = buildDevNodesSummary();
const modules = buildModuleShelves();
const workflows = buildWorkflowShelves();
const zones = [];
for (const def of ZONE_DEFS) {
const zone = {
zone_id: def.zone_id,
name: def.name,
emoji: def.emoji,
path: def.path,
description: def.description,
keywords: def.keywords,
shelves: []
};
if (def.zone_id === 'MODULES') {
zone.shelves = modules;
zone.item_count = modules.length;
} else if (def.zone_id === 'WORKFLOWS') {
zone.shelves = workflows;
zone.item_count = workflows.length;
} else if (def.zone_id === 'DEV_NODES') {
zone.shelves = devNodes.map(d => ({
file: `${d.dev_id}/`,
...d
}));
zone.item_count = devNodes.length;
} else if (def.path) {
const absPath = path.join(ROOT, def.path);
if (existsDir(absPath)) {
const files = listDir(absPath).filter(f => f !== '.gitkeep');
zone.shelves = files.map(f => ({ file: f }));
zone.item_count = files.length;
} else {
zone.item_count = 0;
}
}
zones.push(zone);
}
return { zones, hli, devNodes, memory, routingMap };
}
// ── 生成 repo-map.json ──────────────────────────────────────────────────────
function generateRepoMap(data) {
const { zones, hli, devNodes, memory } = data;
const map = {
description: '铸渊图书馆目录 · Library Catalog for 铸渊 (Zhùyuān)',
version: '2.0',
generated_at: nowISO,
generated_by: 'scripts/generate-repo-map.js',
repo: 'qinfendebingshuo/guanghulab',
stats: {
zones: zones.length,
total_modules: zones.find(z => z.zone_id === 'MODULES')?.item_count || 0,
total_workflows: zones.find(z => z.zone_id === 'WORKFLOWS')?.item_count || 0,
total_scripts: zones.find(z => z.zone_id === 'SCRIPTS')?.item_count || 0,
total_dev_nodes: devNodes.length,
hli_interfaces: hli.total,
hli_implemented: hli.implemented,
hli_coverage_pct: hli.total > 0
? Math.round((hli.implemented / hli.total) * 100) + '%'
: '0%',
last_ci_run: memory?.events?.find(e => e.type === 'ci_run')?.timestamp || null,
memory_last_updated: memory?.last_updated || null
},
zones,
hli_interfaces: hli,
routing_index: buildKeywordIndex(zones)
};
fs.writeFileSync(MAP_PATH, JSON.stringify(map, null, 2));
console.log(`✅ repo-map.json 已生成 · ${zones.length} 个区域 · ${map.stats.total_workflows} 个工作流`);
return map;
}
// ── 生成 repo-snapshot.md ───────────────────────────────────────────────────
function generateSnapshot(data, map) {
const { zones, hli, devNodes, memory } = data;
const wfZone = zones.find(z => z.zone_id === 'WORKFLOWS');
const scriptZone = zones.find(z => z.zone_id === 'SCRIPTS');
const modZone = zones.find(z => z.zone_id === 'MODULES');
const brainZone = zones.find(z => z.zone_id === 'BRAIN');
const hliBadge = `${hli.implemented}/${hli.total} (${map.stats.hli_coverage_pct})`;
let md = `# 铸渊图书馆快照 · Repo Snapshot
> 生成于 ${nowBJ} · 每次 push 自动更新 · 铸渊唤醒时优先读取此文件
---
## 📊 仓库总览一眼全局
| 指标 | 数值 |
|------|------|
| 区域总数 | ${zones.length} 个区域 |
| 功能模块 | ${map.stats.total_modules} (m01~m18) |
| 工作流 | ${map.stats.total_workflows} GitHub Actions |
| 脚本 | ${map.stats.total_scripts} 个执行脚本 |
| 开发者节点 | ${map.stats.total_dev_nodes} |
| HLI 接口覆盖率 | ${hliBadge} |
| 快照生成时间 | ${nowBJ} |
---
## 🗺 图书馆全区地图
`;
// Zone overview table
for (const z of zones) {
const count = z.item_count !== undefined ? z.item_count : z.shelves?.length || 0;
md += `### ${z.emoji} ${z.name}${z.zone_id}\n`;
md += `**路径**: \`${z.path || '多个目录'}\` · **数量**: ${count}\n`;
md += `**描述**: ${z.description}\n`;
md += `**关键词**: ${(z.keywords || []).slice(0, 6).join(' · ')}\n\n`;
}
// ── 自动化工作流详情
if (wfZone?.shelves?.length) {
md += `---\n\n## ⚡ 工作流详情(铸渊的自动执行手脚)\n\n| 文件 | 名称 | 触发方式 |\n|------|------|----------|\n`;
for (const wf of wfZone.shelves) {
md += `| \`${wf.file}\` | ${wf.name} | ${wf.triggers.join(', ')} |\n`;
}
md += '\n';
}
// ── 脚本详情
if (scriptZone?.shelves?.length) {
md += `---\n\n## 🔧 执行脚本库(铸渊的工作人员)\n\n`;
const scripts = scriptZone.shelves.map(s => s.file).sort();
scripts.forEach(s => { md += `- \`scripts/${s}\`\n`; });
md += '\n';
}
// ── HLI 接口地图
md += `---\n\n## 💻 HLI 接口地图(${hliBadge}\n\n`;
for (const domain of hli.domains) {
const icon = domain.implemented === domain.total ? '✅' : domain.implemented > 0 ? '🔶' : '⬜';
md += `${icon} **${domain.name}** (${domain.module}) \`${domain.prefix}\`${domain.implemented}/${domain.total}\n`;
for (const iface of domain.interfaces) {
const si = iface.status === 'implemented' ? ' ✓' : ' ○';
md += `${si} \`${iface.id}\`\`${iface.path}\`\n`;
}
}
md += '\n';
// ── 开发者节点
if (devNodes.length > 0) {
md += `---\n\n## 👥 开发者节点dev-nodes/\n\n| DEV ID | 姓名 | 模块 | 待广播 |\n|--------|------|------|--------|\n`;
for (const d of devNodes) {
md += `| ${d.dev_id} | ${d.emoji}${d.name} | ${d.modules.join(', ') || '—'} | ${d.pending_broadcasts} |\n`;
}
md += '\n';
}
// ── 功能模块列表
if (modZone?.shelves?.length) {
md += `---\n\n## 📦 功能模块区(各开发者工作目录)\n\n`;
for (const m of modZone.shelves) {
md += `- \`${m.dir}/\`${m.files} 个文件${m.has_readme ? ' (有README)' : ''}\n`;
}
md += '\n';
}
// ── 大脑文件速查
if (brainZone?.shelves?.length) {
md += `---\n\n## 🧠 铸渊大脑文件速查(.github/brain/\n\n`;
for (const s of brainZone.shelves) {
md += `- \`.github/brain/${s.file}\`\n`;
}
md += '\n';
}
// ── 最近记忆
if (memory?.events?.length) {
md += `---\n\n## 🕐 最近动态memory.json 最新3条\n\n`;
const recent = memory.events.slice(-3).reverse();
for (const e of recent) {
const ts = e.timestamp || e.date || '?';
md += `- \`${ts}\` · ${e.type || '事件'}${e.description || e.result || e.title || ''}\n`;
}
md += '\n';
}
md += `---\n\n*本文件由 \`scripts/generate-repo-map.js\` 自动生成,勿手动编辑*\n`;
fs.writeFileSync(SNAPSHOT_PATH, md);
console.log(`✅ repo-snapshot.md 已生成 · ${hli.domains.length} 个HLI域 · ${devNodes.length} 个开发者节点`);
}
// ── 主流程 ──────────────────────────────────────────────────────────────────
const data = buildZoneData();
const map = generateRepoMap(data);
generateSnapshot(data, map);
console.log(`\n📚 铸渊图书馆目录更新完成 · ${nowBJ}`);
console.log(` repo-map.json → .github/brain/repo-map.json`);
console.log(` repo-snapshot.md → .github/brain/repo-snapshot.md`);