zhizhi/scripts/sync-snapshot-to-notion.js

172 lines
5.0 KiB
JavaScript
Raw Normal View History

/**
* 🔄 铸渊系统快照 → Notion 同步
* sync-snapshot-to-notion.js
*
* 将 signal-log/system-snapshot.json 推送到 Notion 数据库,
* 让 Notion 侧(认知层)始终有仓库最新结构和系统状况的认知。
*
* 用法: NOTION_TOKEN=xxx SNAPSHOT_DB_ID=xxx node scripts/sync-snapshot-to-notion.js
*
* 环境变量:
* NOTION_TOKEN — Notion API Token
* SNAPSHOT_DB_ID — 快照数据库 ID(Notion 侧)
* SIGNAL_LOG_DB_ID — 信号日志数据库 ID(备用)
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const ROOT = path.resolve(__dirname, '..');
const SNAPSHOT_PATH = path.join(ROOT, 'signal-log/system-snapshot.json');
function readSnapshot() {
try {
return JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf-8'));
} catch (err) {
console.error('❌ 无法读取快照:', err.message);
process.exit(1);
}
}
function notionRequest(method, endpoint, body) {
const token = process.env.NOTION_TOKEN;
if (!token) {
console.error('❌ NOTION_TOKEN 未设置');
process.exit(1);
}
return new Promise((resolve, reject) => {
const data = body ? JSON.stringify(body) : null;
const options = {
hostname: 'api.notion.com',
port: 443,
path: `/v1/${endpoint}`,
method,
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {})
}
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(body) });
} catch {
resolve({ status: res.statusCode, data: body });
}
});
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
function buildSnapshotPage(snapshot, dbId) {
const counts = snapshot.system_counts || {};
const health = snapshot.health || {};
const aliveList = (snapshot.alive_workflows || [])
.map(w => `${w.id} (${w.runs} runs, ${w.status})`).join('\n');
return {
parent: { database_id: dbId },
properties: {
'Name': {
title: [{ text: { content: `系统快照 · ${snapshot.generated_at?.split('T')[0] || 'unknown'}` } }]
},
'Status': {
select: { name: snapshot.consciousness_status || 'unknown' }
},
'Directive': {
rich_text: [{ text: { content: snapshot.last_directive || '' } }]
},
'Workflows Active': {
number: counts.workflows_total_active || 0
},
'Core Alive': {
number: counts.workflows_alive_core || 0
},
'Archived': {
number: counts.workflows_archived || 0
},
'Total Runs': {
number: counts.total_runs || 0
},
'Health': {
select: { name: health.overall || 'unknown' }
}
},
children: [
{
object: 'block',
type: 'heading_2',
heading_2: { rich_text: [{ text: { content: '🌊 铸渊系统快照' } }] }
},
{
object: 'block',
type: 'code',
code: {
rich_text: [{ text: { content: JSON.stringify(snapshot, null, 2).substring(0, 2000) } }],
language: 'json'
}
},
{
object: 'block',
type: 'heading_3',
heading_3: { rich_text: [{ text: { content: '存活 Workflow' } }] }
},
{
object: 'block',
type: 'paragraph',
paragraph: { rich_text: [{ text: { content: aliveList || '(none)' } }] }
}
]
};
}
async function syncToNotion() {
const snapshot = readSnapshot();
const dbId = process.env.SNAPSHOT_DB_ID || process.env.SIGNAL_LOG_DB_ID;
if (!dbId) {
console.log('⚠️ SNAPSHOT_DB_ID 未设置,快照同步跳过');
console.log('📋 快照内容预览:');
console.log(` 状态: ${snapshot.consciousness_status}`);
console.log(` 指令: ${snapshot.last_directive}`);
console.log(` Workflow: ${snapshot.system_counts?.workflows_total_active} active`);
console.log(` 健康: ${snapshot.health?.overall}`);
console.log('');
console.log('💡 要启用 Notion 同步,请设置:');
console.log(' SNAPSHOT_DB_ID=<notion-database-id>');
return;
}
console.log('🔄 正在同步快照到 Notion...');
const page = buildSnapshotPage(snapshot, dbId);
try {
const result = await notionRequest('POST', 'pages', page);
if (result.status === 200) {
console.log('✅ 快照已同步到 Notion');
console.log(` Page ID: ${result.data?.id}`);
} else {
console.error('❌ Notion 同步失败:', result.status, JSON.stringify(result.data).substring(0, 500));
}
} catch (err) {
console.error('❌ Notion 请求失败:', err.message);
}
}
if (require.main === module) {
syncToNotion();
}
module.exports = { syncToNotion, readSnapshot };