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