265 lines
7.5 KiB
JavaScript
Raw Permalink Normal View History

/**
* connectors/notion-sync Notion 双向同步模块
*
* 功能
* - 读取 Notion 广播
* - 写回执行日志
* - 同步执行层状态
*
* 同步结构
* Notion 仓库下行读取广播/工单
* 仓库 Notion上行写回日志/状态
*
* 环境变量
* NOTION_TOKEN Notion API Token
* BROADCAST_DB_ID 广播数据库 ID需包含 status 属性: 待执行/已完成
* 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 syncExecutionStatus(statusData) {
const dbId = process.env.EXECUTION_LOG_DB_ID;
if (!dbId) {
console.log('⚠️ EXECUTION_LOG_DB_ID 未设置,执行状态仅写入本地');
writeLocalLog({
task_id: `execution-status-${new Date().toISOString().slice(0, 10)}`,
status: statusData.execution_layer_status || 'stable',
type: 'execution_status',
data: statusData
});
return;
}
console.log('📡 同步执行层状态到 Notion...');
const summary = [
`version: v${statusData.version || 'unknown'}`,
`modules: ${(statusData.core_modules || []).filter(m => m.status === 'enabled').length}/${(statusData.core_modules || []).length}`,
`connectors: ${(statusData.connectors || []).filter(c => c.status === 'enabled').length}/${(statusData.connectors || []).length}`,
`workflows: ${statusData.workflows?.count || 0}`,
`queue: ${statusData.task_queue?.total || 0} total / ${statusData.task_queue?.pending || 0} pending`
].join(' | ');
try {
await notionRequest('POST', 'pages', {
parent: { database_id: dbId },
properties: {
Name: { title: [{ text: { content: `Execution Status · ${new Date().toISOString().slice(0, 10)}` } }] },
Status: { select: { name: statusData.execution_layer_status || 'stable' } },
Executor: { rich_text: [{ text: { content: 'zhuyuan' } }] },
Timestamp: { rich_text: [{ text: { content: statusData.timestamp || new Date().toISOString() } }] }
},
children: [{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ text: { content: summary } }]
}
}]
});
console.log('✅ 执行层状态已同步到 Notion');
} catch (err) {
console.error(`⚠️ Notion 同步失败,回退本地: ${err.message}`);
writeLocalLog({
task_id: `execution-status-${new Date().toISOString().slice(0, 10)}`,
status: statusData.execution_layer_status || 'stable',
type: 'execution_status',
data: statusData
});
}
}
/**
* 检查 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, syncExecutionStatus, writeLocalLog, checkStatus, notionRequest };