zhizhi/connectors/notion-sync/index.js

210 lines
5.4 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.

/**
* connectors/notion-sync — Notion 双向同步模块
*
* 功能:
* - 读取 Notion 广播
* - 写回执行日志
* - 同步任务状态
*
* 同步结构:
* Notion → 仓库(下行:读取广播/工单)
* 仓库 → Notion上行写回日志/状态)
*
* 环境变量:
* NOTION_TOKEN — Notion API Token
* BROADCAST_DB_ID — 广播数据库 ID
* EXECUTION_LOG_DB_ID — 执行日志数据库 ID
*
* 调用方式:
* node connectors/notion-sync [pull|push|status]
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const NOTION_VERSION = '2022-06-28';
/**
* 发送 Notion API 请求
*/
function notionRequest(method, endpoint, body = null) {
const token = process.env.NOTION_TOKEN;
if (!token) {
return Promise.reject(new Error('NOTION_TOKEN 环境变量未设置'));
}
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.notion.com',
path: `/v1/${endpoint}`,
method,
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': NOTION_VERSION,
'Content-Type': 'application/json'
}
};
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => { data += chunk; });
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) {
reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`));
} else {
resolve(parsed);
}
} catch {
reject(new Error(`Notion 响应解析失败: ${data}`));
}
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
/**
* 从 Notion 拉取广播
*/
async function pullBroadcasts() {
const dbId = process.env.BROADCAST_DB_ID;
if (!dbId) {
console.log('⚠️ BROADCAST_DB_ID 未设置,跳过广播拉取');
return [];
}
console.log('📡 拉取 Notion 广播...');
try {
const result = await notionRequest('POST', `databases/${dbId}/query`, {
filter: {
property: 'status',
select: { equals: '待执行' }
},
sorts: [{ property: 'created_time', direction: 'descending' }]
});
const broadcasts = (result.results || []).map(page => ({
id: page.id,
title: page.properties?.Name?.title?.[0]?.plain_text || '未命名',
status: page.properties?.status?.select?.name || 'unknown',
created: page.created_time
}));
console.log(`✅ 拉取到 ${broadcasts.length} 条广播`);
return broadcasts;
} catch (err) {
console.error(`❌ 广播拉取失败: ${err.message}`);
return [];
}
}
/**
* 写回执行日志到 Notion
*/
async function pushExecutionLog(logEntry) {
const dbId = process.env.EXECUTION_LOG_DB_ID;
if (!dbId) {
console.log('⚠️ EXECUTION_LOG_DB_ID 未设置,执行日志仅写入本地');
return writeLocalLog(logEntry);
}
console.log(`📝 写回执行日志: ${logEntry.task_id || 'unknown'}`);
try {
await notionRequest('POST', 'pages', {
parent: { database_id: dbId },
properties: {
Name: { title: [{ text: { content: logEntry.task_id || 'execution-log' } }] },
Status: { select: { name: logEntry.status || 'completed' } },
Executor: { rich_text: [{ text: { content: 'zhuyuan' } }] },
Timestamp: { rich_text: [{ text: { content: new Date().toISOString() } }] }
}
});
console.log('✅ 日志已同步到 Notion');
} catch (err) {
console.error(`⚠️ Notion 写入失败,回退本地: ${err.message}`);
writeLocalLog(logEntry);
}
}
/**
* 本地日志写入(回退方案)
*/
function writeLocalLog(logEntry) {
const logDir = path.join(ROOT, 'core/task-queue');
const logFile = path.join(logDir, 'execution-log.json');
let logs = [];
if (fs.existsSync(logFile)) {
try {
logs = JSON.parse(fs.readFileSync(logFile, 'utf-8'));
} catch {
logs = [];
}
}
logs.push({
...logEntry,
logged_at: new Date().toISOString()
});
// 保留最近 100 条
if (logs.length > 100) {
logs = logs.slice(-100);
}
fs.writeFileSync(logFile, JSON.stringify(logs, null, 2), 'utf-8');
console.log('📝 日志已写入本地');
}
/**
* 检查 Notion 连接状态
*/
async function checkStatus() {
console.log('🔍 检查 Notion 连接状态...');
try {
await notionRequest('GET', 'users/me');
console.log('✅ Notion API 连接正常');
return { connected: true };
} catch (err) {
console.error(`❌ Notion 连接失败: ${err.message}`);
return { connected: false, error: err.message };
}
}
// CLI 入口
if (require.main === module) {
const cmd = process.argv[2] || 'status';
(async () => {
switch (cmd) {
case 'pull':
await pullBroadcasts();
break;
case 'push':
await pushExecutionLog({
task_id: 'manual-test',
status: 'completed',
message: 'Manual push test'
});
break;
case 'status':
await checkStatus();
break;
default:
console.log('用法: node connectors/notion-sync [pull|push|status]');
}
})();
}
module.exports = { pullBroadcasts, pushExecutionLog, writeLocalLog, checkStatus, notionRequest };