210 lines
5.4 KiB
JavaScript
210 lines
5.4 KiB
JavaScript
/**
|
||
* 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 };
|