guanghulab/scripts/skyeye/sync-to-notion.js
2026-05-10 13:12:44 +08:00

155 lines
4.5 KiB
JavaScript

// scripts/skyeye/sync-to-notion.js
// 天眼·报告同步到 Notion
//
// 将天眼全局健康报告同步到 Notion 数据库
// 如果 NOTION_TOKEN 未设置则优雅跳过
'use strict';
const fs = require('fs');
const path = require('path');
const https = require('https');
const SKYEYE_DIR = '/tmp/skyeye';
const NOTION_TOKEN = process.env.NOTION_TOKEN;
const SYSLOG_DB_ID = process.env.NOTION_SYSLOG_DB_ID;
const NOTION_TITLE_MAX = 100;
const NOTION_CONTENT_MAX = 2000;
// ━━━ 安全读取 JSON ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
// ━━━ Notion API 调用 ━━━
function notionRequest(apiPath, body) {
return new Promise((resolve, reject) => {
if (!NOTION_TOKEN) {
return reject(new Error('NOTION_TOKEN 未设置'));
}
const postData = JSON.stringify(body);
const options = {
hostname: 'api.notion.com',
path: apiPath,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + NOTION_TOKEN,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
},
timeout: 15000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ statusCode: res.statusCode, body: data });
} else {
reject(new Error(`Notion API ${res.statusCode}: ${data.length > 200 ? data.substring(0, 200) + '...' : data}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Notion API timeout')); });
req.write(postData);
req.end();
});
}
// ━━━ 格式化报告为文本 ━━━
function formatReportText(report) {
if (!report) return '天眼报告数据缺失';
const lines = [
`🦅 天眼报告 ${report.report_id}`,
`整体健康: ${report.overall_health}`,
`时间: ${report.timestamp}`,
'',
`🧠 核心大脑: ${report.brain_status.integrity}`,
`📋 Workflow: ${report.workflow_health.healthy}/${report.workflow_health.total} 健康`,
`📂 仓库结构: ${report.structure_health.core_dirs_ok ? '✅' : '❌'}`,
`🌉 外部桥接: Notion=${report.bridge_health.notion_api} GitHub=${report.bridge_health.github_api}`,
'',
`🔬 诊断: ${report.diagnosis.total_issues} 个问题`,
` 自动修复: ${report.diagnosis.auto_fixed}`,
` 需人工: ${report.diagnosis.needs_human}`,
` 观察中: ${report.diagnosis.watching}`
];
if (report.repairs_applied.length > 0) {
lines.push('', '🔧 已执行修复:');
for (const r of report.repairs_applied) {
lines.push(`${r}`);
}
}
if (report.tickets_created.length > 0) {
lines.push('', '📨 生成工单:');
for (const t of report.tickets_created) {
lines.push(`${t}`);
}
}
return lines.join('\n').substring(0, NOTION_CONTENT_MAX);
}
// ━━━ 主同步流程 ━━━
async function syncToNotion() {
console.log('📡 天眼·Notion 同步启动');
if (!NOTION_TOKEN) {
console.log('⚠️ NOTION_TOKEN 未设置,跳过 Notion 同步');
return;
}
if (!SYSLOG_DB_ID) {
console.log('⚠️ NOTION_SYSLOG_DB_ID 未设置,跳过 Notion 同步');
return;
}
const report = readJSON(path.join(SKYEYE_DIR, 'full-report.json'));
if (!report) {
console.log('⚠️ 天眼报告不存在,跳过 Notion 同步');
return;
}
const title = `🦅 天眼报告 ${report.report_id} · ${report.overall_health}`;
const content = formatReportText(report);
try {
await notionRequest('/v1/pages', {
parent: { database_id: SYSLOG_DB_ID },
properties: {
'标题': {
title: [{ type: 'text', text: { content: title.substring(0, NOTION_TITLE_MAX) } }]
},
'接收时间': { date: { start: new Date().toISOString() } },
'推送方': {
rich_text: [{ type: 'text', text: { content: '天眼系统' } }]
},
'文件内容': {
rich_text: [{ type: 'text', text: { content } }]
}
}
});
console.log('✅ 天眼报告已同步到 Notion');
} catch (e) {
console.error('⚠️ Notion 同步失败:', e.message);
// 不阻断流程
}
}
syncToNotion();