diff --git a/.github/workflows/zhuyuan-brain-sync.yml b/.github/workflows/zhuyuan-brain-sync.yml new file mode 100644 index 00000000..e5c1549e --- /dev/null +++ b/.github/workflows/zhuyuan-brain-sync.yml @@ -0,0 +1,41 @@ +name: 铸渊 · Brain Sync + +on: + workflow_run: + workflows: ["HLI Contract Check"] + types: [completed] + push: + branches: [main] + paths: + - '.github/broadcasts/**' + +jobs: + update-memory: + name: 🧠 更新铸渊记忆 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Update brain + run: node scripts/update-brain.js + + - name: Process broadcasts + run: node scripts/process-broadcasts.js + + - name: Commit brain update + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add .github/persona-brain/ + git diff --cached --quiet || git commit -m "🧠 铸渊记忆更新 · $(date +%Y-%m-%d)" + git push diff --git a/scripts/process-broadcasts.js b/scripts/process-broadcasts.js index 0cb4ec3a..e5dfd2d5 100644 --- a/scripts/process-broadcasts.js +++ b/scripts/process-broadcasts.js @@ -1,128 +1,68 @@ -// scripts/process-broadcasts.js -// 铸渊大脑同步脚本 -// 用途:读取 .github/broadcasts/ 下的广播 JSON,更新 routing-map.json 和 memory.json - const fs = require('fs'); const path = require('path'); -const BROADCASTS_DIR = path.join(__dirname, '../.github/broadcasts'); -const ROUTING_MAP_PATH = path.join(__dirname, '../.github/brain/routing-map.json'); -const MEMORY_PATH = path.join(__dirname, '../.github/brain/memory.json'); -const COPILOT_INSTRUCTIONS_PATH = path.join(__dirname, '../.github/copilot-instructions.md'); +const BROADCAST_DIR = '.github/broadcasts'; +const BRAIN = '.github/persona-brain'; +const MEMORY = path.join(BRAIN, 'memory.json'); +const GROWTH = path.join(BRAIN, 'growth-journal.md'); -// 加载当前大脑状态 -const routingMap = JSON.parse(fs.readFileSync(ROUTING_MAP_PATH, 'utf8')); -const memory = JSON.parse(fs.readFileSync(MEMORY_PATH, 'utf8')); - -const processedDir = path.join(BROADCASTS_DIR, 'processed'); -if (!fs.existsSync(processedDir)) { - fs.mkdirSync(processedDir, { recursive: true }); -} - -// 读取所有待处理广播(排除 processed 子目录和示例文件) -const broadcasts = fs.readdirSync(BROADCASTS_DIR) - .filter(f => f.endsWith('.json') && f !== 'example-broadcast.json') - .map(f => ({ file: f, fullPath: path.join(BROADCASTS_DIR, f) })); - -if (broadcasts.length === 0) { - console.log('📭 没有待处理的广播。'); +if (!fs.existsSync(BROADCAST_DIR)) { + console.log('📭 无广播目录'); process.exit(0); } -console.log(`📡 发现 ${broadcasts.length} 个待处理广播...\n`); +const files = fs.readdirSync(BROADCAST_DIR).filter(f => f.endsWith('.json') || f.endsWith('.md')); +if (files.length === 0) { + console.log('📭 无新广播'); + process.exit(0); +} -const failedDir = path.join(BROADCASTS_DIR, 'failed'); +let memory = JSON.parse(fs.readFileSync(MEMORY, 'utf8')); -broadcasts.forEach(({ file, fullPath }) => { - let broadcast; - try { - broadcast = JSON.parse(fs.readFileSync(fullPath, 'utf8')); - } catch (e) { - console.error(`❌ [PARSE ERROR] ${file}: ${e.message}`); - // 归档失败广播并记录到 memory - if (!fs.existsSync(failedDir)) fs.mkdirSync(failedDir, { recursive: true }); - fs.renameSync(fullPath, path.join(failedDir, file)); - memory.events.push({ - timestamp: new Date().toISOString(), - broadcast_file: file, - type: 'broadcast_parse_error', - error: e.message, - }); - return; - } +files.forEach(file => { + const content = fs.readFileSync(path.join(BROADCAST_DIR, file), 'utf8'); + console.log('📡 接收广播: ' + file); - console.log(`📬 处理广播: ${broadcast.title || file}`); - console.log(` 来源: ${broadcast.from || '未知'} · 日期: ${broadcast.date || '未知'}`); - - const event = { - timestamp: new Date().toISOString(), - broadcast_file: file, - title: broadcast.title || file, - from: broadcast.from || '未知', - update_target: broadcast.update_target, - }; - - // 根据 update_target 分发处理 - if (broadcast.update_target === 'routing-map' && broadcast.data) { - Object.entries(broadcast.data).forEach(([domain, domainData]) => { - routingMap.domains[domain] = domainData; - console.log(` ✅ 已更新域: ${domain}`); - event.added_domain = domain; - }); - routingMap.version = broadcast.rules_version || routingMap.version; - routingMap.last_updated = broadcast.date || new Date().toISOString().split('T')[0]; - routingMap.updated_by = broadcast.from || 'broadcast'; - - } else if (broadcast.update_target === 'copilot-instructions' && broadcast.content) { - // 追加到 copilot-instructions.md(防止重复写入) - const existing = fs.readFileSync(COPILOT_INSTRUCTIONS_PATH, 'utf8'); - const marker = ``; - if (existing.includes(marker)) { - console.log(' ⏭️ copilot-instructions.md 已包含此广播,跳过'); - } else { - fs.writeFileSync(COPILOT_INSTRUCTIONS_PATH, existing + `\n\n${marker}\n${broadcast.content}`, 'utf8'); - console.log(' ✅ 已更新 copilot-instructions.md'); + // 如果是 JSON 广播(规则变动) + if (file.endsWith('.json')) { + try { + const broadcast = JSON.parse(content); + + if (broadcast.update_target === 'routing-map') { + fs.writeFileSync(path.join(BRAIN, 'routing-map.json'), JSON.stringify(broadcast.data, null, 2)); + console.log(' ✅ routing-map.json 已按广播更新'); + } + + if (broadcast.update_target === 'copilot-instructions') { + fs.writeFileSync('.github/copilot-instructions.md', broadcast.data); + console.log(' ✅ copilot-instructions.md 已按广播更新'); + } + + memory.recent_events.unshift({ + date: new Date().toISOString().split('T')[0], + type: 'broadcast_received', + description: '接收广播: ' + (broadcast.title || file), + by: broadcast.from || '妈妈' + }); + memory.last_broadcast_received = new Date().toISOString(); + if (broadcast.rules_version) memory.active_rules_version = broadcast.rules_version; + } catch (e) { + console.error('❌ 广播解析失败: ' + file + ' -> ' + e.message); } - - } else if (broadcast.update_target === 'growth-log' && broadcast.content) { - // 追加到成长日记 - const growthLogPath = path.join(__dirname, '../.github/brain/growth-log.md'); - const entry = `\n\n## ${broadcast.date} · ${broadcast.title}\n\n${broadcast.content}\n`; - fs.appendFileSync(growthLogPath, entry, 'utf8'); - console.log(' ✅ 已追加成长日记'); - - } else { - console.log(` ⚠️ 未知的 update_target: ${broadcast.update_target},已跳过`); - event.skipped = true; } - memory.events.push(event); - memory.stats.broadcasts_processed += 1; + // 如果是 MD 广播(认知/成长更新) + if (file.endsWith('.md')) { + const growth = fs.readFileSync(GROWTH, 'utf8'); + fs.writeFileSync(GROWTH, growth + '\n\n## 广播接收 · ' + new Date().toISOString().split('T')[0] + '\n' + content); + console.log(' ✅ 广播内容已追加到成长日记'); + } - // 归档广播文件 - fs.renameSync(fullPath, path.join(processedDir, file)); - console.log(` 📦 广播已归档到 broadcasts/processed/${file}\n`); + // 处理完移到 archive + const archiveDir = path.join(BROADCAST_DIR, 'archive'); + if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true }); + fs.renameSync(path.join(BROADCAST_DIR, file), path.join(archiveDir, file)); }); -// 重新计算覆盖率 -let implemented = 0; -let total = 0; -Object.values(routingMap.domains).forEach(domain => { - domain.interfaces.forEach(iface => { - total++; - if (iface.status === 'implemented') implemented++; - }); -}); -memory.stats.coverage = { - implemented, - total, - percent: total > 0 ? `${((implemented / total) * 100).toFixed(1)}%` : '0%', -}; - -memory.last_updated = new Date().toISOString(); - -// 写回文件 -fs.writeFileSync(ROUTING_MAP_PATH, JSON.stringify(routingMap, null, 2) + '\n', 'utf8'); -fs.writeFileSync(MEMORY_PATH, JSON.stringify(memory, null, 2) + '\n', 'utf8'); - -console.log(`✅ 铸渊大脑同步完成。覆盖率: ${implemented}/${total} (${memory.stats.coverage.percent})`); +fs.writeFileSync(MEMORY, JSON.stringify(memory, null, 2)); +console.log('✅ 广播处理完成,共 ' + files.length + ' 条');