guanghulab/scripts/bridge/process-syslog-batch.js

253 lines
9.3 KiB
JavaScript
Raw Normal View History

// scripts/bridge/process-syslog-batch.js
// 🌉 桥接·SYSLOG 批量处理
//
// 读取 syslog-inbox/ 中的所有新 SYSLOG 文件,
// 通过 Notion API 写入回执+画像+主控台,
// 并在调度队列创建 SYSLOG_RECEIVED 任务,
// 然后移动已处理文件到 syslog-processed/
//
// 环境变量:
// NOTION_TOKEN Notion API token
// BRIDGE_QUEUE_DB_ID 桥接调度队列数据库 ID
// SYSLOG_DB_ID SYSLOG 收件箱数据库 ID可选有默认值
// COMMIT_SHA 当前 Git commit SHA
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
const NOTION_TITLE_MAX = 120;
const DEFAULT_SYSLOG_DB_ID = '330ab17507d542c9bbb96d0749b41197';
const INBOX_DIR = 'syslog-inbox';
const PROCESSED_DIR = 'syslog-processed';
// ══════════════════════════════════════════════════════════
// Notion API
// ══════════════════════════════════════════════════════════
function notionPost(endpoint, body, token) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const opts = {
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
'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 API ${res.statusCode}: ${parsed.message || data}`));
}
} catch (e) {
reject(new Error(`Notion API parse error: ${data}`));
}
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// 属性构建辅助
// ══════════════════════════════════════════════════════════
function richText(content) {
const str = String(content || '');
const chunks = [];
for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
}
return chunks.length ? chunks : [{ type: 'text', text: { content: '' } }];
}
function titleProp(content) {
return [{ type: 'text', text: { content: String(content || '').slice(0, NOTION_TITLE_MAX) } }];
}
// ══════════════════════════════════════════════════════════
// 文件扫描
// ══════════════════════════════════════════════════════════
function scanDir(dir, extensions) {
const results = [];
if (!fs.existsSync(dir)) return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === '.gitkeep' || entry.name === 'README.md') continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...scanDir(fullPath, extensions));
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
results.push(fullPath);
}
}
return results;
}
// ══════════════════════════════════════════════════════════
// 核心逻辑
// ══════════════════════════════════════════════════════════
/**
* SYSLOG 写入 Notion SYSLOG 收件箱数据库
*/
async function writeSyslogToNotion(dbId, fileContent, filePath, entry, commitSha, token) {
const filename = path.basename(filePath);
const title = entry.title || filename;
const devId = entry.from || entry.dev_id || entry.header?.dev_id || '';
const ts = entry.timestamp || new Date().toISOString();
const properties = {
'标题': { title: titleProp(title) },
'文件内容': { rich_text: richText(fileContent) },
'来源路径': { rich_text: richText(filePath) },
'接收时间': { date: { start: ts } },
'处理状态': { status: { name: '待处理' } },
'commit_sha': { rich_text: richText(commitSha || '') },
'推送方': { rich_text: richText('铸渊') },
};
if (devId) {
properties['DEV编号'] = { select: { name: devId } };
}
return notionPost('/v1/pages', { parent: { database_id: dbId }, properties }, token);
}
/**
* 在调度队列创建 SYSLOG_RECEIVED 任务
*/
async function createQueueTask(queueDbId, entry, filePath, source, token) {
const devId = entry.from || entry.dev_id || entry.header?.dev_id || '';
const broadcastId = entry.broadcast_id || entry.header?.broadcast_id || '';
const taskName = `SYSLOG-${devId}-${broadcastId || path.basename(filePath, '.json')}`;
const properties = {
'任务名称': { title: titleProp(taskName) },
'类型': { select: { name: 'SYSLOG_RECEIVED' } },
'处理状态': { status: { name: '待处理' } },
'来源渠道': { rich_text: richText(source || '飞书') },
'Payload': { rich_text: richText(JSON.stringify(entry).slice(0, NOTION_RICH_TEXT_MAX)) },
};
if (devId) properties['DEV编号'] = { select: { name: devId } };
if (broadcastId) properties['广播编号'] = { rich_text: richText(broadcastId) };
return notionPost('/v1/pages', { parent: { database_id: queueDbId }, properties }, token);
}
/**
* 移动已处理文件到 syslog-processed/
*/
function moveToProcessed(filePath) {
const dateDir = new Date().toISOString().slice(0, 7); // YYYY-MM
const destDir = path.join(PROCESSED_DIR, dateDir);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
const destPath = path.join(destDir, path.basename(filePath));
fs.renameSync(filePath, destPath);
return destPath;
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const token = process.env.NOTION_TOKEN;
const syslogDb = process.env.SYSLOG_DB_ID || DEFAULT_SYSLOG_DB_ID;
const queueDb = process.env.BRIDGE_QUEUE_DB_ID;
const commitSha = process.env.COMMIT_SHA || '';
if (!token) {
console.log('⚠️ 缺少 NOTION_TOKEN跳过 SYSLOG 批处理');
process.exit(0);
}
const files = scanDir(INBOX_DIR, ['.json', '.md', '.txt']);
if (files.length === 0) {
console.log('📭 syslog-inbox/ 无待处理条目,跳过');
process.exit(0);
}
console.log(`📥 发现 ${files.length} 条 SYSLOG开始批量处理…`);
let ok = 0, failed = 0;
for (const fullPath of files) {
const relPath = fullPath.replace(/\\/g, '/');
let raw, entry;
try {
raw = fs.readFileSync(fullPath, 'utf8');
entry = fullPath.endsWith('.json') ? JSON.parse(raw) : {};
} catch (e) {
console.error(`❌ 读取 ${relPath} 失败: ${e.message}`);
failed++;
continue;
}
try {
// 写入 SYSLOG 收件箱
const page = await writeSyslogToNotion(syslogDb, raw, relPath, entry, commitSha, token);
console.log(`${relPath} → Notion SYSLOG: ${page.id}`);
// 在调度队列创建任务(如配置了队列数据库)
if (queueDb) {
const source = entry.source || entry.header?.source || '飞书';
const task = await createQueueTask(queueDb, entry, relPath, source, token);
console.log(` 📋 调度队列任务已创建: ${task.id}`);
}
// 移动到已处理目录
const destPath = moveToProcessed(fullPath);
console.log(` 📦 已归档: ${destPath}`);
ok++;
} catch (e) {
console.error(`${relPath} 处理失败: ${e.message}`);
failed++;
}
}
console.log(`\n✅ SYSLOG 批处理完成 · 成功 ${ok} 条 · 失败 ${failed}`);
// 输出到 GITHUB_OUTPUT
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`processed_count=${ok}\nfailed_count=${failed}\n`
);
}
if (failed > 0) process.exit(1);
}
main().catch(e => { console.error(e); process.exit(1); });