guanghulab/scripts/bridge/process-syslog-batch.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

253 lines
9.3 KiB
JavaScript
Raw Permalink 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.

// 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); });