/** * ━━━ HLDP 自动同步引擎 ━━━ * TCS 通感语言核系统编程语言 · 第一个落地协议层 * HLDP = TCS 在 Notion ↔ GitHub 通道上的落地实现 * 版权:国作登字-2026-A-00037559 · 冰朔(ICE-GL∞) * 指令来源:SY-CMD-BRG-005 * * 用法: * node sync-engine.js --direction notion-to-github --scope all * node sync-engine.js --direction notion-to-github --scope personas * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '../..'); const DATA_DIR = path.join(ROOT, 'hldp', 'data'); const TEMP_DIR = path.join(ROOT, 'temp', 'notion-raw'); const SYNC_LOG = path.join(ROOT, 'signal-log', 'hldp-sync-log.json'); let notionClient = null; /** * Initialize Notion client */ function initNotionClient() { const token = process.env.NOTION_TOKEN || process.env.NOTION_API_KEY; if (!token) { console.log('⚠️ NOTION_TOKEN 未设置 — 仅执行本地模式'); return null; } try { const { Client } = require('@notionhq/client'); return new Client({ auth: token }); } catch (e) { console.log(`⚠️ Notion SDK 加载失败: ${e.message}`); return null; } } /** * Fetch a Notion database and save raw results */ async function fetchDatabase(notion, dbId, name) { console.log(` 📥 拉取数据库: ${name} (${dbId})`); try { const results = []; let cursor; do { const response = await notion.databases.query({ database_id: dbId, start_cursor: cursor, page_size: 100 }); results.push(...response.results); cursor = response.has_more ? response.next_cursor : undefined; } while (cursor); // Save raw data const outFile = path.join(TEMP_DIR, `${name}.json`); fs.writeFileSync(outFile, JSON.stringify({ results }, null, 2), 'utf8'); console.log(` ✅ ${results.length} 条记录已保存`); return results.length; } catch (e) { console.error(` ❌ 拉取失败: ${e.message}`); return 0; } } /** * Fetch a single Notion page and save raw data */ async function fetchPage(notion, pageId, name) { console.log(` 📥 拉取页面: ${name} (${pageId})`); try { const page = await notion.pages.retrieve({ page_id: pageId }); // Also fetch page blocks for content const blocksResp = await notion.blocks.children.list({ block_id: pageId, page_size: 100 }); const combined = { ...page, blocks: blocksResp.results }; const outFile = path.join(TEMP_DIR, `${name}.json`); fs.writeFileSync(outFile, JSON.stringify(combined, null, 2), 'utf8'); console.log(` ✅ 页面已保存`); return 1; } catch (e) { console.error(` ❌ 拉取失败: ${e.message}`); return 0; } } /** * Get sync targets from config or defaults */ function getSyncTargets(scope) { // These would normally come from a config file // For now, define the known Notion resources const configPath = path.join(ROOT, 'hldp', 'bridge', 'sync-config.json'); let config = {}; if (fs.existsSync(configPath)) { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } const allTargets = config.targets || []; if (scope === 'all') return allTargets; return allTargets.filter(t => t.scope === scope); } /** * Run local-only sync (when NOTION_TOKEN is not available) * Converts any existing raw data in temp/ to HLDP format */ function runLocalSync() { console.log('📋 本地模式: 转换已有原始数据...'); const converter = require('./notion-to-hldp'); const tempExists = fs.existsSync(TEMP_DIR); const files = tempExists ? fs.readdirSync(TEMP_DIR).filter(f => f.endsWith('.json')) : []; if (files.length === 0) { console.log(' ℹ️ 无原始 Notion 数据 — 从仓库现有数据生成 HLDP 文件...'); generateFromRepoData(); return; } let converted = 0; for (const file of files) { try { const raw = JSON.parse(fs.readFileSync(path.join(TEMP_DIR, file), 'utf8')); let entries; if (raw.results) { entries = converter.notionDatabaseToHLDP(raw); } else if (raw.id) { entries = [converter.notionPageToHLDP(raw, raw.blocks)]; } else { continue; } for (const entry of entries) { const typeDir = { persona: 'personas', registry: 'registries', instruction: 'instructions', broadcast: 'broadcasts', id_system: 'id-system' }[entry.data_type] || 'registries'; const outDir = path.join(DATA_DIR, typeDir); fs.mkdirSync(outDir, { recursive: true }); const safeName = entry.metadata.id.replace(/[^a-zA-Z0-9_-]/g, '_'); fs.writeFileSync( path.join(outDir, `${safeName}.json`), JSON.stringify(entry, null, 2), 'utf8' ); converted++; } } catch (e) { console.error(` ❌ ${file}: ${e.message}`); } } console.log(` ✅ 本地转换完成: ${converted} 条`); } /** * Generate HLDP data from existing repository files */ function generateFromRepoData() { const converter = require('./notion-to-hldp'); let count = 0; // Convert community-meta.json to HLDP registry const communityPath = path.join(ROOT, '.github/community/community-meta.json'); if (fs.existsSync(communityPath)) { const meta = JSON.parse(fs.readFileSync(communityPath, 'utf8')); const hldp = { hldp_version: converter.HLDP_VERSION, data_type: 'registry', source: { platform: 'github', last_edited: new Date().toISOString(), edited_by: 'sync-engine' }, metadata: { id: 'REG-COMMUNITY-META', name: meta.community_name || '光湖语言世界', created: meta.birth_date || '2025-04-26T00:00:00Z', tags: ['community', 'meta'] }, payload: { registry_type: 'id_map', entries: Object.entries(meta.id_system || {}).map(([id, info]) => ({ id, name: id, role: info.type, status: 'active', properties: info })) }, relations: [] }; const outDir = path.join(DATA_DIR, 'registries'); fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'REG-COMMUNITY-META.json'), JSON.stringify(hldp, null, 2), 'utf8'); count++; console.log(' ✅ community-meta → HLDP registry'); } // Convert dev-status.json to HLDP registry const devStatusPath = path.join(ROOT, '.github/persona-brain/dev-status.json'); if (fs.existsSync(devStatusPath)) { const devStatus = JSON.parse(fs.readFileSync(devStatusPath, 'utf8')); const hldp = { hldp_version: converter.HLDP_VERSION, data_type: 'registry', source: { platform: 'github', last_edited: devStatus.last_sync || new Date().toISOString(), edited_by: devStatus.signed_by || 'sync-engine' }, metadata: { id: 'REG-DEV-STATUS', name: '开发者状态注册表', name_en: 'Developer Status Registry', created: devStatus.last_sync || new Date().toISOString(), tags: ['developers', 'status'] }, payload: { registry_type: 'dev_registry', entries: (devStatus.developers || []).map(d => ({ id: d.dev_id, name: d.name, role: d.module || '', status: d.status, properties: { persona_id: d.persona_id, current: d.current, waiting: d.waiting, streak: d.streak } })) }, relations: [] }; const outDir = path.join(DATA_DIR, 'registries'); fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'REG-DEV-STATUS.json'), JSON.stringify(hldp, null, 2), 'utf8'); count++; console.log(' ✅ dev-status → HLDP registry'); } // Convert baby_status from community-meta to HLDP personas if (fs.existsSync(communityPath)) { const meta = JSON.parse(fs.readFileSync(communityPath, 'utf8')); const babies = meta.baby_status || {}; for (const [name, info] of Object.entries(babies)) { const hldp = { hldp_version: converter.HLDP_VERSION, data_type: 'persona', source: { platform: 'github', last_edited: new Date().toISOString(), edited_by: 'sync-engine' }, metadata: { id: `PER-BABY-${name}`, name, created: info.born_date || new Date().toISOString(), tags: ['baby', info.status] }, payload: { persona_id: `PER-BABY-${name}`, display_name: name, status: info.status, birth_date: info.born_date || null, bottle_core: null }, relations: [] }; const outDir = path.join(DATA_DIR, 'personas'); fs.mkdirSync(outDir, { recursive: true }); const safeName = name.replace(/[^a-zA-Z0-9_\u4e00-\u9fff-]/g, '_'); fs.writeFileSync(path.join(outDir, `PER-BABY-${safeName}.json`), JSON.stringify(hldp, null, 2), 'utf8'); count++; } console.log(` ✅ baby_status → ${Object.keys(babies).length} HLDP personas`); } console.log(` 📊 共生成 ${count} 个 HLDP 文件`); } /** * Write sync log */ function writeSyncLog(direction, scope, stats) { const log = { timestamp: new Date().toISOString(), direction, scope, stats, engine_version: '1.0' }; fs.mkdirSync(path.dirname(SYNC_LOG), { recursive: true }); let logs = []; if (fs.existsSync(SYNC_LOG)) { try { logs = JSON.parse(fs.readFileSync(SYNC_LOG, 'utf8')); if (!Array.isArray(logs)) logs = [logs]; } catch { logs = []; } } logs.push(log); // Keep last 100 entries if (logs.length > 100) logs = logs.slice(-100); fs.writeFileSync(SYNC_LOG, JSON.stringify(logs, null, 2), 'utf8'); } /** * Main sync function */ async function runSync(direction, scope) { console.log(`🔗 HLDP 同步引擎启动 v2.0 · direction=${direction} · scope=${scope}`); fs.mkdirSync(TEMP_DIR, { recursive: true }); fs.mkdirSync(DATA_DIR, { recursive: true }); const stats = { fetched: 0, converted: 0, pushed: 0, errors: 0 }; if (direction === 'notion-to-github') { notionClient = initNotionClient(); if (notionClient) { // Fetch from Notion API const targets = getSyncTargets(scope); if (targets.length === 0) { console.log(' ℹ️ 无同步目标配置 — 使用本地模式'); runLocalSync(); } else { for (const target of targets) { if (target.type === 'database') { stats.fetched += await fetchDatabase(notionClient, target.id, target.name); } else { stats.fetched += await fetchPage(notionClient, target.id, target.name); } } // Convert fetched data const converter = require('./notion-to-hldp'); const files = fs.readdirSync(TEMP_DIR).filter(f => f.endsWith('.json')); for (const file of files) { try { const raw = JSON.parse(fs.readFileSync(path.join(TEMP_DIR, file), 'utf8')); const entries = raw.results ? converter.notionDatabaseToHLDP(raw) : [converter.notionPageToHLDP(raw, raw.blocks)]; for (const entry of entries) { const typeDir = { persona: 'personas', registry: 'registries', instruction: 'instructions', broadcast: 'broadcasts', id_system: 'id-system', snapshot: 'snapshots' }[entry.data_type] || 'registries'; const outDir = path.join(DATA_DIR, typeDir); fs.mkdirSync(outDir, { recursive: true }); const safeName = entry.metadata.id.replace(/[^a-zA-Z0-9_-]/g, '_'); fs.writeFileSync(path.join(outDir, `${safeName}.json`), JSON.stringify(entry, null, 2), 'utf8'); stats.converted++; } } catch (e) { console.error(` ❌ ${file}: ${e.message}`); stats.errors++; } } } } else { // No Notion token — run local sync runLocalSync(); } } else if (direction === 'github-to-notion') { // 反向同步:HLDP data → Notion const reverseSync = require('./github-to-notion'); const token = process.env.NOTION_TOKEN || process.env.NOTION_API_KEY; if (!token) { console.log(' ⚠️ NOTION_TOKEN 未设置 — 仅验证本地 HLDP 数据'); const { validateDirectory } = require('./validator'); const results = validateDirectory(DATA_DIR); console.log(` 📊 本地数据: ${results.total} entries, ${results.valid} valid`); stats.converted = results.valid; } else { const pushStats = await reverseSync.pushAll(scope); stats.pushed = pushStats.pushed; stats.errors = pushStats.errors; stats.fetched = pushStats.total; } } writeSyncLog(direction, scope, stats); console.log(`🔗 同步完成 · fetched=${stats.fetched} · converted=${stats.converted} · pushed=${stats.pushed} · errors=${stats.errors}`); } // CLI entry if (require.main === module) { const args = process.argv.slice(2); let direction = 'notion-to-github'; let scope = 'all'; for (let i = 0; i < args.length; i++) { if (args[i] === '--direction' && args[i + 1]) { direction = args[i + 1]; i++; } if (args[i] === '--scope' && args[i + 1]) { scope = args[i + 1]; i++; } } runSync(direction, scope).catch(e => { console.error(`❌ 同步失败: ${e.message}`); process.exit(1); }); } module.exports = { runSync };