guanghulab/scripts/sync-snapshot-to-notion.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

474 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 🔄 铸渊系统快照 → Notion 双向同步引擎
* sync-snapshot-to-notion.js
*
* 使用已建立的 GL-ACK 信号协议,将系统快照推送到 Notion 认知层。
* 兼容 notion-signal-bridge.js 的信号格式和 HLDP 协议。
*
* 通信通道(按优先级):
* 1. SIGNAL_LOG_DB_ID — 跨平台信号日志(已配置·主通道)
* 2. SNAPSHOT_DB_ID — 快照专用数据库(可选)
* 3. RECEIPT_DB_ID — 回执数据库(可选·写入回执)
*
* 用法:
* node scripts/sync-snapshot-to-notion.js — 发送快照信号
* node scripts/sync-snapshot-to-notion.js --mode signal — 仅写信号日志
* node scripts/sync-snapshot-to-notion.js --mode full — 信号 + 快照 + 回执
* node scripts/sync-snapshot-to-notion.js --health — 测试 Notion 连通性
*
* 环境变量:
* NOTION_TOKEN / NOTION_API_KEY / NOTION_API_TOKEN — Notion API Token任一即可
* SIGNAL_LOG_DB_ID — 跨平台信号日志数据库 ID主通道
* SNAPSHOT_DB_ID — 快照数据库 ID可选
* RECEIPT_DB_ID — 回执数据库 ID可选
*/
'use strict';
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');
const SIGNAL_LOG_DIR = path.join(ROOT, 'signal-log');
const SIGNAL_BUS_PATH = path.join(ROOT, '.github/persona-brain/tcs-ml/signal-bus-latest.json');
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
// ═══════════════════════════════════════════════════════
// Notion Token 解析(兼容多种命名)
// ═══════════════════════════════════════════════════════
function getNotionToken() {
return process.env.NOTION_TOKEN
|| process.env.NOTION_API_KEY
|| process.env.NOTION_API_TOKEN
|| null;
}
// ═══════════════════════════════════════════════════════
// Notion API 基础调用(复用 notion-signal-bridge.js 模式)
// ═══════════════════════════════════════════════════════
function notionRequest(method, endpoint, body) {
const token = getNotionToken();
if (!token) return Promise.reject(new Error('NOTION_TOKEN 未设置'));
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : '';
const opts = {
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint.startsWith('/') ? endpoint : `/v1/${endpoint}`,
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
},
};
if (payload) opts.headers['Content-Length'] = Buffer.byteLength(payload);
const req = https.request(opts, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error(`Notion ${method} ${endpoint}${res.statusCode}: ${parsed.message || data.slice(0, 300)}`));
}
} catch {
reject(new Error(`Notion 响应解析失败: ${data.slice(0, 200)}`));
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => req.destroy(new Error('Notion 请求超时 (30s)')));
if (payload) req.write(payload);
req.end();
});
}
// ═══════════════════════════════════════════════════════
// 信号生成GL-SNAPSHOT 信号类型 · 基于 GL-ACK 协议扩展)
// ═══════════════════════════════════════════════════════
function generateSignalId() {
const now = new Date();
const date = now.toISOString().split('T')[0].replace(/-/g, '');
// 读取现有 index.json仅统计当日信号的最大顺序号
let nextSeq = 1;
try {
const indexPath = path.join(SIGNAL_LOG_DIR, 'index.json');
const raw = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
const signals = Array.isArray(raw) ? raw : (raw.signals || []);
const pattern = new RegExp(`^SIG-${date}-(\\d{3})$`);
for (const sig of signals) {
const match = (sig.signal_id || '').match(pattern);
if (match) {
const seq = parseInt(match[1], 10);
if (seq >= nextSeq) nextSeq = seq + 1;
}
}
} catch {}
return `SIG-${date}-${String(nextSeq).padStart(3, '0')}`;
}
function generateTraceId() {
const now = new Date();
const date = now.toISOString().split('T')[0].replace(/-/g, '');
// 读取现有 index.json仅统计当日 trace 的最大顺序号
let nextSeq = 1;
try {
const indexPath = path.join(SIGNAL_LOG_DIR, 'index.json');
const raw = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
const signals = Array.isArray(raw) ? raw : (raw.signals || []);
const pattern = new RegExp(`^TRC-${date}-(\\d{3})$`);
for (const sig of signals) {
const match = (sig.trace_id || '').match(pattern);
if (match) {
const seq = parseInt(match[1], 10);
if (seq >= nextSeq) nextSeq = seq + 1;
}
}
} catch {}
return `TRC-${date}-${String(nextSeq).padStart(3, '0')}`;
}
function readSnapshot() {
try {
return JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf-8'));
} catch {
return null;
}
}
function truncateText(text, max = NOTION_RICH_TEXT_MAX) {
if (text.length <= max) return text;
return text.substring(0, max - 15) + ' // truncated';
}
function buildSnapshotSummary(snapshot) {
if (!snapshot) return '快照不可用';
const c = snapshot.system_counts || {};
const h = snapshot.health || {};
const alive = (snapshot.alive_workflows || []).map(w => w.id).join(', ');
return [
`状态: ${snapshot.consciousness_status}`,
`指令: ${snapshot.last_directive}`,
`Workflow: ${c.workflows_total_active} active, ${c.workflows_alive_core} core, ${c.workflows_archived} archived`,
`Runs: ${c.total_runs}`,
`ONT-PATCH: ${(c.ontology_patches || []).join(', ')}`,
`健康: ${h.overall} | Core: ${h.core_6}`,
`Alive: ${alive}`,
].join('\n');
}
// ═══════════════════════════════════════════════════════
// 通道 1: SIGNAL_LOG_DB_ID — GL-SNAPSHOT 信号写入
// ═══════════════════════════════════════════════════════
async function writeSignalLog(snapshot) {
const dbId = process.env.SIGNAL_LOG_DB_ID;
if (!dbId) {
console.log(' ⚠️ SIGNAL_LOG_DB_ID 未设置,信号通道跳过');
return false;
}
const signalId = generateSignalId();
const traceId = generateTraceId();
const summary = buildSnapshotSummary(snapshot);
const now = new Date().toISOString();
console.log(` 📡 写入信号: ${signalId} → SIGNAL_LOG_DB_ID`);
const page = {
parent: { database_id: dbId },
properties: {
'信号编号': { title: [{ text: { content: signalId } }] },
'信号类型': { select: { name: 'GL-SNAPSHOT' } },
'方向': { select: { name: 'GitHub→Notion' } },
'发送方': { select: { name: '铸渊' } },
'接收方': { select: { name: '霜砚' } },
'trace_id': { rich_text: [{ text: { content: traceId } }] },
'摘要': { rich_text: [{ text: { content: truncateText(summary) } }] },
'执行结果': { select: { name: '成功' } },
},
};
try {
const result = await notionRequest('POST', '/v1/pages', page);
console.log(` ✅ 信号已写入: ${result.id}`);
// 同时写入本地信号日志(双写)
writeLocalSignalLog(signalId, traceId, summary);
return true;
} catch (err) {
console.error(` ❌ 信号写入失败: ${err.message}`);
// 降级:仅写本地
writeLocalSignalLog(signalId, traceId, summary);
return false;
}
}
function writeLocalSignalLog(signalId, traceId, summary) {
const now = new Date();
const monthDir = path.join(SIGNAL_LOG_DIR, now.toISOString().slice(0, 7));
try {
fs.mkdirSync(monthDir, { recursive: true });
const entry = {
signal_id: signalId,
trace_id: traceId,
type: 'GL-SNAPSHOT',
timestamp: now.toISOString(),
sender: '铸渊',
receiver: '霜砚',
direction: 'GitHub→Notion',
summary: summary.substring(0, 500),
result: '成功',
esp_version: '2.0-notion',
};
fs.writeFileSync(
path.join(monthDir, `${signalId}.json`),
JSON.stringify(entry, null, 2),
'utf8'
);
// 更新信号日志索引(保持原始对象格式)
const indexPath = path.join(SIGNAL_LOG_DIR, 'index.json');
let indexData = { description: '铸渊信号日志目录索引 · AGE OS 信号协议', last_updated: null, total_count: 0, signals: [] };
try {
const raw = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
if (Array.isArray(raw)) {
// 兼容:如果已被改为数组格式,恢复为对象格式
indexData.signals = raw;
indexData.total_count = raw.length;
} else {
indexData = raw;
if (!Array.isArray(indexData.signals)) indexData.signals = [];
}
} catch {}
indexData.signals.unshift({
signal_id: signalId,
trace_id: traceId,
type: 'GL-SNAPSHOT',
timestamp: now.toISOString(),
summary: summary.substring(0, 200),
file: `${now.toISOString().slice(0, 7)}/${signalId}.json`,
});
if (indexData.signals.length > 200) indexData.signals = indexData.signals.slice(0, 200);
indexData.last_updated = now.toISOString();
indexData.total_count = indexData.signals.length;
fs.writeFileSync(indexPath, JSON.stringify(indexData, null, 2), 'utf8');
} catch (err) {
console.error(` ⚠️ 本地信号日志写入失败: ${err.message}`);
}
}
// ═══════════════════════════════════════════════════════
// 通道 2: SNAPSHOT_DB_ID — 快照详细数据写入(可选)
// ═══════════════════════════════════════════════════════
async function writeSnapshotPage(snapshot) {
const dbId = process.env.SNAPSHOT_DB_ID;
if (!dbId) return false;
console.log(' 📸 写入快照页面 → SNAPSHOT_DB_ID');
const counts = snapshot.system_counts || {};
const snapshotJSON = truncateText(JSON.stringify({
snapshot_version: snapshot.snapshot_version,
generated_at: snapshot.generated_at,
consciousness_status: snapshot.consciousness_status,
last_directive: snapshot.last_directive,
system_counts: counts,
health: snapshot.health,
fusion_progress: snapshot.fusion_progress,
}, null, 2));
const page = {
parent: { database_id: dbId },
properties: {
Name: { title: [{ text: { content: `系统快照 · ${snapshot.generated_at?.split('T')[0]}` } }] },
},
children: [
{
object: 'block', type: 'heading_2',
heading_2: { rich_text: [{ text: { content: '🌊 铸渊系统快照' } }] },
},
{
object: 'block', type: 'code',
code: { rich_text: [{ text: { content: snapshotJSON } }], language: 'json' },
},
],
};
try {
const result = await notionRequest('POST', '/v1/pages', page);
console.log(` ✅ 快照页面已创建: ${result.id}`);
return true;
} catch (err) {
console.error(` ❌ 快照页面创建失败: ${err.message}`);
return false;
}
}
// ═══════════════════════════════════════════════════════
// 通道 3: 信号总线更新(本地 + TCS 协议)
// ═══════════════════════════════════════════════════════
function updateSignalBus(snapshot) {
try {
const bus = {
version: 'v2.0',
updated_at: new Date().toISOString(),
updated_by: '铸渊 · sync-snapshot-to-notion.js',
latest_signals: [
{
title: `🔄 GL-SNAPSHOT · ${snapshot?.last_directive || 'system-sync'}`,
type: '系统快照同步',
sender: '铸渊',
receiver: '霜砚',
status: '已发送',
timestamp: new Date().toISOString(),
summary: buildSnapshotSummary(snapshot).substring(0, 200),
},
],
};
fs.writeFileSync(SIGNAL_BUS_PATH, JSON.stringify(bus, null, 2), 'utf8');
console.log(' ✅ 信号总线已更新');
} catch (err) {
console.error(` ⚠️ 信号总线更新失败: ${err.message}`);
}
}
// ═══════════════════════════════════════════════════════
// 健康检查
// ═══════════════════════════════════════════════════════
async function healthCheck() {
console.log('🏥 Notion 连通性测试...');
const token = getNotionToken();
if (!token) {
console.log(' ❌ 无 Notion Token 可用');
console.log(' 💡 需设置: NOTION_TOKEN / NOTION_API_KEY / NOTION_API_TOKEN');
return false;
}
try {
const user = await notionRequest('GET', '/v1/users/me', null);
console.log(` ✅ Notion 连接正常`);
console.log(` 👤 Bot: ${user.name || user.id}`);
console.log(` 📧 Type: ${user.type}`);
// 检查已配置的数据库 ID
const dbs = {
SIGNAL_LOG_DB_ID: process.env.SIGNAL_LOG_DB_ID,
SNAPSHOT_DB_ID: process.env.SNAPSHOT_DB_ID,
RECEIPT_DB_ID: process.env.RECEIPT_DB_ID,
WORKORDER_DB_ID: process.env.WORKORDER_DB_ID,
};
console.log(' 📋 已配置的数据库:');
for (const [name, id] of Object.entries(dbs)) {
console.log(` ${id ? '✅' : '⬜'} ${name}: ${id ? id.substring(0, 8) + '...' : '未设置'}`);
}
return true;
} catch (err) {
console.error(` ❌ Notion 连接失败: ${err.message}`);
return false;
}
}
// ═══════════════════════════════════════════════════════
// 主入口
// ═══════════════════════════════════════════════════════
async function syncToNotion(mode = 'signal') {
console.log('🔄 铸渊 → Notion 双向同步引擎启动');
console.log(` 模式: ${mode}`);
const snapshot = readSnapshot();
if (!snapshot) {
console.log(' ⚠️ 快照不可用,先生成快照...');
try {
const { generateSnapshot } = require('./generate-system-snapshot');
generateSnapshot();
} catch {}
}
const freshSnapshot = readSnapshot();
const token = getNotionToken();
if (!token) {
console.log(' ⚠️ NOTION_TOKEN 未设置 — 仅执行本地更新');
updateSignalBus(freshSnapshot);
writeLocalSignalLog(generateSignalId(), generateTraceId(), buildSnapshotSummary(freshSnapshot));
console.log('');
console.log('💡 要启用 Notion 同步,请在 GitHub Secrets 中确认以下密钥:');
console.log(' NOTION_TOKEN (或 NOTION_API_KEY)');
console.log(' SIGNAL_LOG_DB_ID');
return;
}
let signalOk = false;
let snapshotOk = false;
// 通道 1: 信号日志(始终尝试)
signalOk = await writeSignalLog(freshSnapshot);
// 通道 2: 快照页面(仅 full 模式)
if (mode === 'full') {
snapshotOk = await writeSnapshotPage(freshSnapshot);
}
// 通道 3: 信号总线(始终更新)
updateSignalBus(freshSnapshot);
console.log('');
console.log('📊 同步结果:');
console.log(` 信号日志: ${signalOk ? '✅ 已写入' : '❌ 失败'}`);
if (mode === 'full') {
console.log(` 快照页面: ${snapshotOk ? '✅ 已创建' : '⬜ 跳过或失败'}`);
}
console.log(' 信号总线: ✅ 已更新');
}
// CLI 入口
if (require.main === module) {
const args = process.argv.slice(2);
if (args.includes('--health')) {
healthCheck().then(ok => process.exit(ok ? 0 : 1));
} else {
const mode = args.includes('--mode') ? args[args.indexOf('--mode') + 1] : 'signal';
syncToNotion(mode).catch(err => {
console.error(`❌ 同步失败: ${err.message}`);
process.exit(1);
});
}
}
module.exports = { syncToNotion, healthCheck, readSnapshot };