/** * 📡 Notion → GitHub 全量缓存同步脚本 * * 同步内容: * 1. 开发者画像 → .github/notion-cache/dev-profiles/ * 2. 数据库快照 → .github/notion-cache/databases/ * 3. 活跃广播 → .github/notion-cache/broadcasts/ * * 用法:node scripts/cache/sync-notion-cache.js * 环境变量: * NOTION_TOKEN — Notion API 密钥(必需) * NOTION_CONTROL_PANEL_DB_ID — 主控台数据库 ID(可选) * NOTION_SYSLOG_DB_ID — SYSLOG 收件箱 ID(可选) * NOTION_AGENT_REGISTRY_DB_ID — Agent 注册表 ID(可选) * NOTION_TICKET_DB_ID — 工单簿 ID(可选) * NOTION_MAINTENANCE_DB_ID — 维护日志 ID(可选) * * 签发:霜砚(AG-SY-01) * 审批:冰朔(TCS-0002∞) * 版权:国作登字-2026-A-00037559 */ 'use strict'; const https = require('https'); const fs = require('fs'); const path = require('path'); const NOTION_TOKEN = process.env.NOTION_TOKEN; const CACHE_DIR = path.join(__dirname, '../../.github/notion-cache'); // ====== Notion 数据库 ID 映射(通过环境变量传入)====== const DB_IDS = { controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '', syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '', agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '', ticketBook: process.env.NOTION_TICKET_DB_ID || '', maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '', receiptTracker: process.env.RECEIPT_DB_ID || '', }; // ====== 开发者编号 → 信息映射 ====== const DEVS = [ { id: 'DEV-001', name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] }, { id: 'DEV-002', name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] }, { id: 'DEV-003', name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] }, { id: 'DEV-004', name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] }, { id: 'DEV-005', name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] }, { id: 'DEV-009', name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] }, { id: 'DEV-010', name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] }, { id: 'DEV-011', name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] }, { id: 'DEV-012', name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] }, ]; // ====== Notion API 封装(纯 https,无外部依赖)====== function notionRequest(method, apiPath, body) { return new Promise(function(resolve, reject) { if (!NOTION_TOKEN) { console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用'); resolve({ error: 'NOTION_TOKEN not configured' }); return; } var bodyStr = body ? JSON.stringify(body) : ''; var options = { hostname: 'api.notion.com', port: 443, path: apiPath, method: method, headers: { 'Authorization': 'Bearer ' + NOTION_TOKEN, 'Notion-Version': '2022-06-28', 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) } }; var req = https.request(options, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); res.on('end', function() { try { resolve(JSON.parse(data)); } catch (e) { console.warn('⚠️ Notion API 响应解析失败:', e.message); resolve({ error: 'JSON parse failed', raw: data }); } }); }); req.on('error', reject); if (bodyStr) req.write(bodyStr); req.end(); }); } function queryDatabase(dbId, filter, sorts, pageSize) { pageSize = pageSize || 100; var body = { page_size: pageSize }; if (filter) body.filter = filter; if (sorts) body.sorts = sorts; return notionRequest('POST', '/v1/databases/' + dbId + '/query', body); } // ====== Notion 属性提取器 ====== function extractRichText(prop) { if (!prop) return ''; if (prop.type === 'rich_text' && prop.rich_text) { return prop.rich_text.map(function(t) { return t.plain_text; }).join(''); } if (prop.type === 'title' && prop.title) { return prop.title.map(function(t) { return t.plain_text; }).join(''); } return ''; } function extractTitle(props) { for (var key in props) { var val = props[key]; if (val && val.type === 'title' && val.title) { return val.title.map(function(t) { return t.plain_text; }).join(''); } } return ''; } function extractNumber(prop) { if (!prop || prop.type !== 'number') return null; return prop.number; } function extractSelect(prop) { if (!prop || prop.type !== 'select') return ''; return prop.select ? prop.select.name : ''; } function extractMultiSelect(prop) { if (!prop || prop.type !== 'multi_select') return []; return (prop.multi_select || []).map(function(s) { return s.name; }); } function extractDate(prop) { if (!prop || prop.type !== 'date') return ''; return prop.date ? prop.date.start : ''; } // ====== 同步开发者画像 ====== async function syncDevProfiles() { var dir = path.join(CACHE_DIR, 'dev-profiles'); fs.mkdirSync(dir, { recursive: true }); var syncCount = 0; var errorCount = 0; for (var i = 0; i < DEVS.length; i++) { var dev = DEVS[i]; console.log('📡 同步 ' + dev.id + '(' + dev.name + ')...'); var profile = { dev_id: dev.id, name: dev.name, persona: { name: dev.persona, id: dev.personaId }, github: { repo: dev.repo, org: 'qinfendebingshuo', repo_url: 'https://github.com/qinfendebingshuo/' + dev.repo }, modules_owned: dev.modules.map(function(m) { return { path: m }; }), current_work: {}, recent_syslogs: [], skyeye_health: {}, notion_page_urls: {}, last_synced: new Date().toISOString() }; // 从 Notion 读取该开发者的真实数据 if (NOTION_TOKEN) { try { // 查询主控台获取当前广播和进度 if (DB_IDS.controlPanel) { var controlPanel = await queryDatabase(DB_IDS.controlPanel, { property: 'DEV编号', rich_text: { equals: dev.id } }, null, 1); if (controlPanel.results && controlPanel.results.length > 0) { var page = controlPanel.results[0]; var props = page.properties || {}; profile.current_work = { broadcast_id: extractRichText(props['广播编号']) || '', broadcast_title: extractRichText(props['广播标题']) || extractTitle(props) || '', current_ring: extractNumber(props['当前环节']) || 0, total_rings: extractNumber(props['总环节']) || 0, status: extractRichText(props['状态']) || extractSelect(props['状态']) || '', module_path: extractRichText(props['模块路径']) || '', tech_stack: extractMultiSelect(props['技术栈']) || [] }; } } // 查询 SYSLOG 收件箱 if (DB_IDS.syslogInbox) { var syslogs = await queryDatabase(DB_IDS.syslogInbox, { property: 'DEV编号', rich_text: { equals: dev.id } }, [{ property: '日期', direction: 'descending' }], 5); if (syslogs.results) { profile.recent_syslogs = syslogs.results.map(function(p) { try { var sp = p.properties || {}; return { id: extractRichText(sp['SYSLOG编号']) || p.id, date: extractDate(sp['日期']) || '', ring: extractNumber(sp['环节']) || 0, status: extractSelect(sp['状态']) || '', summary: extractRichText(sp['摘要']) || extractTitle(sp) || '' }; } catch (_) { return null; } }).filter(Boolean); } } } catch (err) { console.warn(' ⚠️ Notion 查询失败(' + dev.id + '): ' + err.message); errorCount++; } } // 读取天眼健康状态(从本地仓库文件) try { var skyeyeReportPath = path.join(CACHE_DIR, 'skyeye/latest-report.json'); if (fs.existsSync(skyeyeReportPath)) { var report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8')); var subRepoData = report.sub_repos && report.sub_repos[dev.repo]; if (subRepoData) { profile.skyeye_health = { last_scan: report.timestamp, status: subRepoData.status || 'unknown', issues: subRepoData.issues || [], warnings: subRepoData.warnings || [] }; } else { profile.skyeye_health = { last_scan: report.timestamp || '', status: 'not_scanned', issues: [], warnings: [] }; } } } catch (_) { // 天眼报告读取失败,不阻断同步 } fs.writeFileSync( path.join(dir, dev.id + '.json'), JSON.stringify(profile, null, 2) ); console.log(' ✅ ' + dev.id + ' → ' + dev.id + '.json'); syncCount++; } // 写入开发者索引 var index = DEVS.map(function(d) { return { id: d.id, name: d.name, persona: d.persona, personaId: d.personaId }; }); fs.writeFileSync(path.join(dir, 'index.json'), JSON.stringify(index, null, 2)); console.log('✅ dev-profiles 同步完成 · 成功: ' + syncCount + ' · 失败: ' + errorCount); } // ====== 同步数据库快照 ====== async function syncDatabases() { var dir = path.join(CACHE_DIR, 'databases'); fs.mkdirSync(dir, { recursive: true }); if (!NOTION_TOKEN) { console.warn('⚠️ NOTION_TOKEN 未配置,跳过数据库同步'); return; } // Agent 注册表 if (DB_IDS.agentRegistry) { try { var result = await queryDatabase(DB_IDS.agentRegistry); if (result.results) { fs.writeFileSync( path.join(dir, 'agent-registry.json'), JSON.stringify({ synced: new Date().toISOString(), count: result.results.length, data: result.results }, null, 2) ); console.log('✅ agent-registry 同步完成 · ' + result.results.length + ' 条'); } } catch (e) { console.error('❌ agent-registry 同步失败:', e.message); } } else { console.log('ℹ️ NOTION_AGENT_REGISTRY_DB_ID 未配置,跳过 agent-registry'); } // SYSLOG 最近50条 if (DB_IDS.syslogInbox) { try { var result = await queryDatabase(DB_IDS.syslogInbox, null, [{ property: 'Created time', direction: 'descending' }], 50); if (result.results) { fs.writeFileSync( path.join(dir, 'syslog-recent.json'), JSON.stringify({ synced: new Date().toISOString(), count: result.results.length, data: result.results }, null, 2) ); console.log('✅ syslog-recent 同步完成 · ' + result.results.length + ' 条'); } } catch (e) { console.error('❌ syslog-recent 同步失败:', e.message); } } else { console.log('ℹ️ NOTION_SYSLOG_DB_ID 未配置,跳过 syslog-recent'); } // 活跃工单 if (DB_IDS.ticketBook) { try { var result = await queryDatabase(DB_IDS.ticketBook, { property: '状态', select: { does_not_equal: '已完成' } }, [{ property: 'Created time', direction: 'descending' }], 50); if (result.results) { fs.writeFileSync( path.join(dir, 'tickets-active.json'), JSON.stringify({ synced: new Date().toISOString(), count: result.results.length, data: result.results }, null, 2) ); console.log('✅ tickets-active 同步完成 · ' + result.results.length + ' 条'); } } catch (e) { console.error('❌ tickets-active 同步失败:', e.message); } } else { console.log('ℹ️ NOTION_TICKET_DB_ID 未配置,跳过 tickets-active'); } // 维护日志 if (DB_IDS.maintenanceLog) { try { var result = await queryDatabase(DB_IDS.maintenanceLog, null, [{ property: 'Created time', direction: 'descending' }], 20); if (result.results) { fs.writeFileSync( path.join(dir, 'maintenance-log.json'), JSON.stringify({ synced: new Date().toISOString(), count: result.results.length, data: result.results }, null, 2) ); console.log('✅ maintenance-log 同步完成 · ' + result.results.length + ' 条'); } } catch (e) { console.error('❌ maintenance-log 同步失败:', e.message); } } else { console.log('ℹ️ NOTION_MAINTENANCE_DB_ID 未配置,跳过 maintenance-log'); } } // ====== 同步活跃广播 ====== async function syncBroadcasts() { var dir = path.join(CACHE_DIR, 'broadcasts'); fs.mkdirSync(dir, { recursive: true }); // 从本地广播目录读取活跃广播 var broadcastsOutbox = path.join(__dirname, '../../.github/broadcasts'); var activeBroadcasts = []; try { if (fs.existsSync(broadcastsOutbox)) { var files = fs.readdirSync(broadcastsOutbox).filter(function(f) { return f.endsWith('.json') && !f.startsWith('.'); }); for (var i = 0; i < files.length; i++) { try { var content = JSON.parse(fs.readFileSync(path.join(broadcastsOutbox, files[i]), 'utf-8')); if (content.status !== 'completed' && content.status !== 'cancelled') { activeBroadcasts.push(content); } } catch (_) {} } } } catch (_) {} fs.writeFileSync( path.join(dir, 'active.json'), JSON.stringify({ broadcasts: activeBroadcasts, last_synced: new Date().toISOString() }, null, 2) ); console.log('✅ broadcasts 同步完成 · ' + activeBroadcasts.length + ' 条活跃广播'); } // ====== 主入口 ====== async function main() { console.log('═══════════════════════════════════════'); console.log('📡 Notion 全量缓存同步 · 开始'); console.log('═══════════════════════════════════════'); console.log('缓存目录: ' + CACHE_DIR); console.log('Notion Token: ' + (NOTION_TOKEN ? '✅ 已配置' : '❌ 未配置')); console.log(''); fs.mkdirSync(CACHE_DIR, { recursive: true }); await syncDevProfiles(); console.log(''); await syncDatabases(); console.log(''); await syncBroadcasts(); console.log(''); console.log('═══════════════════════════════════════'); console.log('📡 Notion 全量缓存同步 · 完成'); console.log('═══════════════════════════════════════'); } main().catch(function(e) { console.error('💥 同步失败:', e); process.exit(1); });