guanghulab/scripts/generate-repo-map.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

530 lines
19 KiB
JavaScript
Raw Permalink 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/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`);