/** * ━━━ HLDP Notion → HLDP 转换器 ━━━ * TCS 通感语言核系统编程语言 · 第一个落地协议层 * HLDP = TCS 在 Notion ↔ GitHub 通道上的落地实现 * 版权:国作登字-2026-A-00037559 · 冰朔(ICE-GL∞) * 指令来源:SY-CMD-BRG-005 * * 转换规则: * 1. Notion 页面标题 → metadata.name * 2. Notion 页面属性 → payload 中对应字段 * 3. Notion @提及 → relations 数组 * 4. Notion 关系属性 → relations 数组 * 5. Notion 选择/多选属性 → payload 中的字符串/数组 * 6. Notion 日期属性 → ISO-8601 字符串 * 7. Notion 页面内容(blocks)→ payload.content(Markdown 格式) * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const fs = require('fs'); const path = require('path'); const HLDP_VERSION = '1.0'; /** * Extract plain text from Notion rich_text array */ function extractPlainText(richTextArr) { if (!Array.isArray(richTextArr)) return ''; return richTextArr.map(t => t.plain_text || '').join(''); } /** * Extract title from Notion page properties */ function extractTitle(properties) { for (const [, prop] of Object.entries(properties || {})) { if (prop.type === 'title') { return extractPlainText(prop.title); } } return ''; } /** * Convert a Notion property to a plain value */ function convertProperty(prop) { if (!prop) return null; switch (prop.type) { case 'title': return extractPlainText(prop.title); case 'rich_text': return extractPlainText(prop.rich_text); case 'number': return prop.number; case 'select': return prop.select ? prop.select.name : null; case 'multi_select': return (prop.multi_select || []).map(s => s.name); case 'date': return prop.date ? prop.date.start : null; case 'checkbox': return prop.checkbox; case 'url': return prop.url; case 'email': return prop.email; case 'phone_number': return prop.phone_number; case 'formula': return prop.formula ? convertFormulaResult(prop.formula) : null; case 'relation': return (prop.relation || []).map(r => r.id); case 'rollup': return prop.rollup ? prop.rollup.array : null; case 'people': return (prop.people || []).map(p => p.name || p.id); case 'status': return prop.status ? prop.status.name : null; default: return null; } } function convertFormulaResult(formula) { switch (formula.type) { case 'string': return formula.string; case 'number': return formula.number; case 'boolean': return formula.boolean; case 'date': return formula.date ? formula.date.start : null; default: return null; } } /** * Convert Notion blocks to Markdown */ function blocksToMarkdown(blocks) { if (!Array.isArray(blocks)) return ''; return blocks.map(block => { const text = extractPlainText(block[block.type]?.rich_text || []); switch (block.type) { case 'paragraph': return text; case 'heading_1': return `# ${text}`; case 'heading_2': return `## ${text}`; case 'heading_3': return `### ${text}`; case 'bulleted_list_item': return `- ${text}`; case 'numbered_list_item': return `1. ${text}`; case 'to_do': { const checked = block.to_do?.checked ? 'x' : ' '; return `- [${checked}] ${text}`; } case 'toggle': return `
${text}
`; case 'code': { const lang = block.code?.language || ''; return `\`\`\`${lang}\n${text}\n\`\`\``; } case 'quote': return `> ${text}`; case 'callout': { const icon = block.callout?.icon?.emoji || '💡'; return `> ${icon} ${text}`; } case 'divider': return '---'; default: return text; } }).filter(Boolean).join('\n\n'); } /** * Extract relations from Notion properties */ function extractRelations(properties) { const relations = []; for (const [key, prop] of Object.entries(properties || {})) { if (prop.type === 'relation' && Array.isArray(prop.relation)) { for (const rel of prop.relation) { relations.push({ target_id: rel.id, relation_type: 'reference', description: `Notion relation: ${key}` }); } } } return relations; } /** * Detect data_type from Notion page properties and tags */ function detectDataType(properties, tags) { const allTags = (tags || []).map(t => t.toLowerCase()); const title = extractTitle(properties).toLowerCase(); if (allTags.includes('persona') || allTags.includes('人格体') || title.includes('人格体')) return 'persona'; if (allTags.includes('registry') || allTags.includes('注册表') || title.includes('注册表')) return 'registry'; if (allTags.includes('instruction') || allTags.includes('指令') || title.includes('指令')) return 'instruction'; if (allTags.includes('broadcast') || allTags.includes('广播') || title.includes('广播')) return 'broadcast'; if (allTags.includes('id_system') || allTags.includes('编号') || title.includes('编号')) return 'id_system'; return 'registry'; // default fallback } /** * Convert a Notion page to HLDP JSON format * @param {Object} page - Notion API page object * @param {Array} blocks - Notion page blocks (optional) * @param {string} dataType - Override data_type detection * @returns {Object} HLDP JSON */ function notionPageToHLDP(page, blocks, dataType) { const properties = page.properties || {}; const title = extractTitle(properties); // Extract tags from multi_select property named 'Tags' or '标签' const tagsProp = properties.Tags || properties['标签']; const tags = tagsProp?.type === 'multi_select' ? (tagsProp.multi_select || []).map(s => s.name) : []; const type = dataType || detectDataType(properties, tags); // Build payload from all non-system properties const payload = {}; for (const [key, prop] of Object.entries(properties)) { const val = convertProperty(prop); if (val !== null && val !== '' && key !== 'Tags' && key !== '标签') { payload[key] = val; } } // Add content from blocks if provided if (blocks && blocks.length > 0) { payload.content = blocksToMarkdown(blocks); } // Extract ID from properties or generate from title const idProp = properties['ID'] || properties['编号'] || properties['id']; const id = idProp ? convertProperty(idProp) : `HLDP-${Date.now()}`; return { hldp_version: HLDP_VERSION, data_type: type, source: { platform: 'notion', page_url: page.url || '', page_id: page.id || '', last_edited: page.last_edited_time || new Date().toISOString(), edited_by: page.last_edited_by?.name || 'unknown' }, metadata: { id: String(id), name: title, name_en: '', created: page.created_time || new Date().toISOString(), tags }, payload, relations: extractRelations(properties) }; } /** * Convert a Notion database query result to an array of HLDP entries * @param {Object} queryResult - Notion API database query result * @param {string} dataType - Data type for all entries * @returns {Array} Array of HLDP JSON objects */ function notionDatabaseToHLDP(queryResult, dataType) { const results = queryResult.results || []; return results.map(page => notionPageToHLDP(page, null, dataType)); } /** * CLI: Convert raw Notion JSON files to HLDP format */ function runCLI() { const args = process.argv.slice(2); let inputDir = ''; let outputDir = ''; for (let i = 0; i < args.length; i++) { if (args[i] === '--input' && args[i + 1]) { inputDir = args[i + 1]; i++; } if (args[i] === '--output' && args[i + 1]) { outputDir = args[i + 1]; i++; } } if (!inputDir || !outputDir) { console.log('Usage: node notion-to-hldp.js --input --output '); process.exit(1); } const root = path.resolve(__dirname, '../..'); inputDir = path.resolve(root, inputDir); outputDir = path.resolve(root, outputDir); if (!fs.existsSync(inputDir)) { console.log(`⚠️ 输入目录不存在: ${inputDir}`); process.exit(0); } let converted = 0; let failed = 0; const files = fs.readdirSync(inputDir).filter(f => f.endsWith('.json')); for (const file of files) { try { const raw = JSON.parse(fs.readFileSync(path.join(inputDir, file), 'utf8')); // Handle both single page and database query results let hldpEntries; if (raw.results) { hldpEntries = notionDatabaseToHLDP(raw); } else if (raw.id) { hldpEntries = [notionPageToHLDP(raw)]; } else { console.warn(` ⚠️ 无法识别格式: ${file}`); failed++; continue; } // Determine output subdirectory based on data_type for (const entry of hldpEntries) { const typeDir = { persona: 'personas', registry: 'registries', instruction: 'instructions', broadcast: 'broadcasts', id_system: 'id-system' }[entry.data_type] || 'registries'; const outDir = path.join(outputDir, typeDir); fs.mkdirSync(outDir, { recursive: true }); const safeName = entry.metadata.id.replace(/[^a-zA-Z0-9_-]/g, '_'); const outFile = path.join(outDir, `${safeName}.json`); fs.writeFileSync(outFile, JSON.stringify(entry, null, 2), 'utf8'); converted++; } } catch (e) { console.error(` ❌ 转换失败 ${file}: ${e.message}`); failed++; } } console.log(`🔄 Notion → HLDP 转换完成: ${converted} 条成功, ${failed} 条失败`); } // Export for programmatic use module.exports = { notionPageToHLDP, notionDatabaseToHLDP, extractPlainText, blocksToMarkdown, convertProperty, HLDP_VERSION }; // CLI entry point if (require.main === module) { runCLI(); }