From 40c12c75a369d24c67f8c50e23bd37d49aeee353 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:23:32 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=89=20BRIDGE-DEPLOY:=20=E5=85=A8?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E6=A1=A5=E6=8E=A5=E7=B3=BB=E7=BB=9F=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=20=E2=80=94=20=E8=84=9A=E6=9C=AC8=E4=B8=AA=20=C2=B7?= =?UTF-8?q?=20Workflow3=E4=B8=AA=20=C2=B7=20=E9=A3=9E=E4=B9=A6=E8=B7=AF?= =?UTF-8?q?=E7=94=B11=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .github/workflows/bridge-broadcast-pdf.yml | 133 +++++++++++ .github/workflows/bridge-heartbeat.yml | 80 +++++++ .github/workflows/bridge-syslog-intake.yml | 84 +++++++ backend/routes/feishu.js | 112 +++++++++ data/bridge-logs/.gitkeep | 0 data/broadcasts/pdf/.gitkeep | 0 scripts/bridge/.gitkeep | 0 scripts/bridge/check-queue.js | 170 ++++++++++++++ scripts/bridge/distribute.js | 230 +++++++++++++++++++ scripts/bridge/fetch-broadcast.js | 234 +++++++++++++++++++ scripts/bridge/generate-pdf.js | 163 +++++++++++++ scripts/bridge/heartbeat.js | 214 +++++++++++++++++ scripts/bridge/process-syslog-batch.js | 252 +++++++++++++++++++++ scripts/bridge/update-queue-status.js | 117 ++++++++++ scripts/bridge/upload-pdf.js | 181 +++++++++++++++ 15 files changed, 1970 insertions(+) create mode 100644 .github/workflows/bridge-broadcast-pdf.yml create mode 100644 .github/workflows/bridge-heartbeat.yml create mode 100644 .github/workflows/bridge-syslog-intake.yml create mode 100644 data/bridge-logs/.gitkeep create mode 100644 data/broadcasts/pdf/.gitkeep create mode 100644 scripts/bridge/.gitkeep create mode 100644 scripts/bridge/check-queue.js create mode 100644 scripts/bridge/distribute.js create mode 100644 scripts/bridge/fetch-broadcast.js create mode 100644 scripts/bridge/generate-pdf.js create mode 100644 scripts/bridge/heartbeat.js create mode 100644 scripts/bridge/process-syslog-batch.js create mode 100644 scripts/bridge/update-queue-status.js create mode 100644 scripts/bridge/upload-pdf.js diff --git a/.github/workflows/bridge-broadcast-pdf.yml b/.github/workflows/bridge-broadcast-pdf.yml new file mode 100644 index 00000000..3493be3c --- /dev/null +++ b/.github/workflows/bridge-broadcast-pdf.yml @@ -0,0 +1,133 @@ +name: ้ธๆธ ยท ๐ ๆกฅๆฅยทๅนฟๆญPDF็ๆ+ๅๅ + +# ๅฎๆถๆๅ Notion ไธญ BROADCAST_READY ็ๅนฟๆญ โ ็ๆ PDF โ ไธไผ โ ๅๅๅฐ IM +# +# ไพ่ต Secrets๏ผ +# NOTION_API_TOKEN Notion ้ๆ token +# BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID +# FEISHU_APP_ID ้ฃไนฆ App ID +# FEISHU_APP_SECRET ้ฃไนฆ App Secret +# DINGTALK_TOKEN ้้ๆบๅจไบบ Token +# OSS_ACCESS_KEY ้ฟ้ไบ OSS AccessKey๏ผๅฏ้๏ผ +# OSS_SECRET_KEY ้ฟ้ไบ OSS SecretKey๏ผๅฏ้๏ผ + +on: + schedule: + - cron: '0 6,14 * * *' # UTC 06:00+14:00 = ๅไบฌ 14:00+22:00 + workflow_dispatch: + +jobs: + # โโ AGE OS v1.0: ๆ ธๅฟๅคง่ๅค้ โโ + wake-brain: + name: ๐ ๅค้ๆ ธๅฟๅคง่ + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + brain_awake: ${{ steps.wake.outputs.brain_awake }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ง ๅค้้ธๆธๆ ธๅฟๅคง่ + id: wake + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + run: | + node core/brain-wake --task "ๆกฅๆฅๅนฟๆญPDF็ๆๅๅ" || { + echo "โ ๏ธ ๆ ธๅฟๅคง่ๅค้ๅคฑ่ดฅ๏ผไฝฟ็จ dry-run ๆจกๅผ็ปง็ปญ" + echo "brain_awake=dry-run" >> "$GITHUB_OUTPUT" + } + + generate-and-distribute: + name: ๐ ๅนฟๆญPDF็ๆ+ๅๅ + needs: wake-brain + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ก ๆฃๆฅ่ฐๅบฆ้ๅ + id: check + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + BRIDGE_QUEUE_DB_ID: ${{ secrets.BRIDGE_QUEUE_DB_ID }} + run: node scripts/bridge/check-queue.js BROADCAST_READY + + - name: ๐ฅ ๆๅๅนฟๆญๅ ๅฎน + if: steps.check.outputs.task_count != '0' + id: fetch + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + QUEUE_FILE: ${{ steps.check.outputs.queue_file }} + run: node scripts/bridge/fetch-broadcast.js + + - name: ๐ ็ๆPDF + if: steps.check.outputs.task_count != '0' + id: pdf + env: + MANIFEST_FILE: ${{ steps.fetch.outputs.manifest_file }} + run: | + npm install --no-save md-to-pdf 2>/dev/null || echo "โ ๏ธ md-to-pdf ๅฎ่ฃ ๅคฑ่ดฅ๏ผไฝฟ็จๅ้ๆจกๅผ" + node scripts/bridge/generate-pdf.js + + - name: โ๏ธ ไธไผ PDF + if: steps.check.outputs.task_count != '0' + id: upload + env: + PDF_MANIFEST: ${{ steps.pdf.outputs.pdf_manifest }} + UPLOAD_MODE: ${{ vars.BRIDGE_UPLOAD_MODE || 'server' }} + OSS_ACCESS_KEY: ${{ secrets.OSS_ACCESS_KEY }} + OSS_SECRET_KEY: ${{ secrets.OSS_SECRET_KEY }} + run: node scripts/bridge/upload-pdf.js + + - name: ๐ข ๅๅๅฐIM + if: steps.check.outputs.task_count != '0' + env: + DIST_MANIFEST: ${{ steps.upload.outputs.dist_manifest }} + FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }} + FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }} + DINGTALK_TOKEN: ${{ secrets.DINGTALK_TOKEN }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + ALERT_EMAIL: ${{ vars.ALERT_EMAIL || '565183519@qq.com' }} + FEISHU_BROADCAST_CHAT_ID: ${{ vars.FEISHU_BROADCAST_CHAT_ID }} + run: node scripts/bridge/distribute.js + + - name: โ ๆดๆฐ่ฐๅบฆ้ๅ็ถๆ + if: steps.check.outputs.task_count != '0' + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + QUEUE_FILE: ${{ steps.check.outputs.queue_file }} + run: node scripts/bridge/update-queue-status.js + + - name: ๐ฆ ๆไบค็ๆ็ปๆ + if: steps.check.outputs.task_count != '0' + run: | + git config user.name "้ธๆธ[bot]" + git config user.email "zhu-yuan-bot@guanghulab.com" + git add data/broadcasts/ data/bridge-logs/ + git diff --staged --quiet || git commit -m "๐ ๆกฅๆฅๅนฟๆญPDF็ๆ+ๅๅ ยท $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M') [auto-bridge]" + git push + + - name: ๐ ่ฟ่กๆป็ป + run: | + echo "๐ ๆกฅๆฅยทๅนฟๆญPDF็ๆ+ๅๅ ๅฎๆ" + echo "๐ ๅพ ๅค็ไปปๅก: ${{ steps.check.outputs.task_count || '0' }}" + echo "๐ ็ๆPDF: ${{ steps.pdf.outputs.pdf_count || '0' }}" + echo "โ๏ธ ๅทฒไธไผ : ${{ steps.upload.outputs.uploaded_count || '0' }}" diff --git a/.github/workflows/bridge-heartbeat.yml b/.github/workflows/bridge-heartbeat.yml new file mode 100644 index 00000000..6028861f --- /dev/null +++ b/.github/workflows/bridge-heartbeat.yml @@ -0,0 +1,80 @@ +name: ้ธๆธ ยท ๐ ๆกฅๆฅยทๅฟ่ทณๆฃๆต + +# ๆฏ6ๅฐๆถๆฃๆตๆกฅๆฅ้้็ ้ๆง +# ๆต่ฏ Notion API ่ฟ้ + ๅจ่ฐๅบฆ้ๅๅๅ ฅ HEARTBEAT ่ฎฐๅฝ +# +# ไพ่ต Secrets๏ผ +# NOTION_API_TOKEN Notion ้ๆ token +# BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID + +on: + schedule: + - cron: '0 0,6,12,18 * * *' # UTC ๆฏ6ๅฐๆถ = ๅไบฌ 08:00/14:00/20:00/02:00 + workflow_dispatch: + +jobs: + # โโ AGE OS v1.0: ๆ ธๅฟๅคง่ๅค้ โโ + wake-brain: + name: ๐ ๅค้ๆ ธๅฟๅคง่ + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + brain_awake: ${{ steps.wake.outputs.brain_awake }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ง ๅค้้ธๆธๆ ธๅฟๅคง่ + id: wake + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + run: | + node core/brain-wake --task "ๆกฅๆฅๅฟ่ทณๆฃๆต" || { + echo "โ ๏ธ ๆ ธๅฟๅคง่ๅค้ๅคฑ่ดฅ๏ผไฝฟ็จ dry-run ๆจกๅผ็ปง็ปญ" + echo "brain_awake=dry-run" >> "$GITHUB_OUTPUT" + } + + heartbeat: + name: ๐ ๆกฅๆฅๅฟ่ทณๆฃๆต + needs: wake-brain + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ ๆง่กๅฟ่ทณๆฃๆต + id: heartbeat + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + BRIDGE_QUEUE_DB_ID: ${{ secrets.BRIDGE_QUEUE_DB_ID }} + run: node scripts/bridge/heartbeat.js + + - name: ๐ฆ ๆไบคๅฟ่ทณๆฅๅฟ + run: | + git config user.name "้ธๆธ[bot]" + git config user.email "zhu-yuan-bot@guanghulab.com" + git add data/bridge-logs/ + git diff --staged --quiet || git commit -m "๐ ๆกฅๆฅๅฟ่ทณ ยท $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M') [auto-bridge]" + git push + + - name: ๐ ๅฟ่ทณ็ปๆ + run: | + echo "๐ ๆกฅๆฅๅฟ่ทณๆฃๆตๅฎๆ" + echo "๐ก Notion: ${{ steps.heartbeat.outputs.notion_healthy || 'unknown' }}" + echo "๐ ็ถๆ: ${{ steps.heartbeat.outputs.heartbeat_status || 'unknown' }}" diff --git a/.github/workflows/bridge-syslog-intake.yml b/.github/workflows/bridge-syslog-intake.yml new file mode 100644 index 00000000..239d7c25 --- /dev/null +++ b/.github/workflows/bridge-syslog-intake.yml @@ -0,0 +1,84 @@ +name: ้ธๆธ ยท ๐ ๆกฅๆฅยทSYSLOG ไธ่กๅค็ + +# syslog-inbox/ ๆๆฐๆไปถ push โ ๆน้ๅๅ ฅ Notion + ๅๅปบ่ฐๅบฆ้ๅไปปๅก + ๅฝๆกฃ +# +# ไพ่ต Secrets๏ผ +# NOTION_API_TOKEN Notion ้ๆ token +# BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID +# NOTION_SYSLOG_DB_ID SYSLOG ๆถไปถ็ฎฑๆฐๆฎๅบ ID๏ผๅฏ้๏ผๆ้ป่ฎคๅผ๏ผ + +on: + push: + branches: [main] + paths: + - 'syslog-inbox/**' + workflow_dispatch: + +jobs: + # โโ AGE OS v1.0: ๆ ธๅฟๅคง่ๅค้ โโ + wake-brain: + name: ๐ ๅค้ๆ ธๅฟๅคง่ + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + brain_awake: ${{ steps.wake.outputs.brain_awake }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ง ๅค้้ธๆธๆ ธๅฟๅคง่ + id: wake + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + run: | + node core/brain-wake --task "ๆกฅๆฅSYSLOGไธ่ก" || { + echo "โ ๏ธ ๆ ธๅฟๅคง่ๅค้ๅคฑ่ดฅ๏ผไฝฟ็จ dry-run ๆจกๅผ็ปง็ปญ" + echo "brain_awake=dry-run" >> "$GITHUB_OUTPUT" + } + + process-syslog: + name: ๐ SYSLOG ๆน้ไธ่กๅค็ + needs: wake-brain + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: ๐ฅ ๆน้ๅค็ SYSLOG + id: process + env: + NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }} + BRIDGE_QUEUE_DB_ID: ${{ secrets.BRIDGE_QUEUE_DB_ID }} + SYSLOG_DB_ID: ${{ secrets.NOTION_SYSLOG_DB_ID }} + COMMIT_SHA: ${{ github.sha }} + run: node scripts/bridge/process-syslog-batch.js + + - name: ๐ฆ ๆไบคๅฝๆกฃ็ปๆ + run: | + git config user.name "้ธๆธ[bot]" + git config user.email "zhu-yuan-bot@guanghulab.com" + git add syslog-inbox/ syslog-processed/ data/bridge-logs/ + git diff --staged --quiet || git commit -m "๐ ๆกฅๆฅSYSLOGไธ่กๅค็ ยท ๅทฒๅฝๆกฃ [auto-bridge]" + git push + + - name: โ ไธ่กๅค็ๅฎๆ + run: | + echo "โ SYSLOG ไธ่กๅค็ๅฎๆ" + echo "๐ ๅทฒๅค็: ${{ steps.process.outputs.processed_count || '0' }} ๆก" + echo "๐ก ็ญๅพ Notion Agent ่ชๅจ่งฆๅ SYSLOG ้ญ็ฏๅค็" diff --git a/backend/routes/feishu.js b/backend/routes/feishu.js index 85467da7..687cdc80 100644 --- a/backend/routes/feishu.js +++ b/backend/routes/feishu.js @@ -4,7 +4,11 @@ const router = express.Router(); const FEISHU_APP_ID = process.env.FEISHU_APP_ID; const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const BASE_URL = 'https://open.feishu.cn/open-apis'; +const GITHUB_API_BASE = 'https://api.github.com'; +const REPO_OWNER = 'qinfendebingshuo'; +const REPO_NAME = 'guanghulab'; // ่ทๅ้ฃไนฆ tenant_access_token async function getToken() { @@ -49,4 +53,112 @@ router.post('/broadcast', async (req, res) => { } }); +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ๐ SYSLOG ๆฅๆถ่ทฏ็ฑ๏ผ้ฃไนฆ่่ โ GitHub syslog-inbox๏ผ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +/** + * ่งฃๆ SYSLOG ๆถๆฏ + * ๆฏๆไธค็งๆ ผๅผ๏ผ + * 1. ็ดๆฅ JSON ๅฏน่ฑก๏ผPOST body๏ผ + * 2. ๅ ๅซ header ็ๆ ๅ SYSLOG ๆ ผๅผ + */ +function parseSyslog(body) { + if (!body || typeof body !== 'object') return null; + + // ๅฆๆ body ๆฌ่บซๅฐฑๆฏ SYSLOG๏ผๆ header ๅญๆฎต๏ผ + if (body.header && (body.header.broadcast_id || body.header.dev_id)) { + return body; + } + + // ๅฆๆๆฏ้ฃไนฆๆถๆฏๅ ่ฃนๆ ผๅผ + if (body.syslog && typeof body.syslog === 'object') { + return body.syslog; + } + + // ๅฆๆ็ดๆฅๆ broadcast_id ๅ dev_id๏ผ็ฎๅๆ ผๅผ๏ผ + if (body.broadcast_id && body.dev_id) { + return { header: { broadcast_id: body.broadcast_id, dev_id: body.dev_id }, ...body }; + } + + return null; +} + +router.post('/syslog-receive', async (req, res) => { + try { + // โ ่งฃๆ SYSLOG + const syslog = parseSyslog(req.body); + if (!syslog) { + return res.status(400).json({ + error: true, + code: 'INVALID_SYSLOG', + message: 'ๆ ๆ็ SYSLOG ๆ ผๅผ๏ผ่ฏท็กฎไฟๅ ๅซ header.broadcast_id ๅ header.dev_id' + }); + } + + // โก ๆ ก้ชๅฟ ๅกซๅญๆฎต + const broadcastId = syslog.header?.broadcast_id || syslog.broadcast_id; + const devId = syslog.header?.dev_id || syslog.dev_id; + + if (!broadcastId || !devId) { + return res.status(400).json({ + error: true, + code: 'MISSING_FIELDS', + message: 'โ SYSLOG ็ผบๅฐ broadcast_id ๆ dev_id' + }); + } + + // โข ๅๅ ฅ GitHub syslog-inbox/ + if (!GITHUB_TOKEN) { + return res.status(503).json({ + error: true, + code: 'NO_GITHUB_TOKEN', + message: 'GitHub Token ๆช้ ็ฝฎ๏ผๆ ๆณๅๅ ฅไปๅบ' + }); + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const safeDevId = String(devId).replace(/[^a-zA-Z0-9_-]/g, ''); + const filename = `SYSLOG-${safeDevId}-${timestamp}.json`; + const filePath = `syslog-inbox/${filename}`; + const content = Buffer.from(JSON.stringify(syslog, null, 2)).toString('base64'); + + const githubUrl = `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${filePath}`; + + const githubResponse = await axios.put( + githubUrl, + { + message: `[BRIDGE] SYSLOG from ${safeDevId} via ้ฃไนฆ`, + content: content + }, + { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json' + } + } + ); + + // โฃ ๅๅคๅผๅ่ + res.json({ + success: true, + message: 'โ SYSLOG ๅทฒๆไบค', + broadcast_id: broadcastId, + dev_id: devId, + filename: filename, + info: 'ๅฐๅจไธไธไธชๅค็็ชๅฃ่ชๅจๅค็๏ผๅค็ๅฎๆๅไผๆจ้ๆฐๅนฟๆญ', + github_sha: githubResponse.data?.content?.sha || '' + }); + + } catch (err) { + const status = err.response?.status || 500; + res.status(status).json({ + error: true, + code: 'SYSLOG_SUBMIT_FAILED', + message: err.message, + detail: err.response?.data || null + }); + } +}); + module.exports = router; diff --git a/data/bridge-logs/.gitkeep b/data/bridge-logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/broadcasts/pdf/.gitkeep b/data/broadcasts/pdf/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/bridge/.gitkeep b/scripts/bridge/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/bridge/check-queue.js b/scripts/bridge/check-queue.js new file mode 100644 index 00000000..8894c6dd --- /dev/null +++ b/scripts/bridge/check-queue.js @@ -0,0 +1,170 @@ +// scripts/bridge/check-queue.js +// ๐ ๆกฅๆฅยท่ฐๅบฆ้ๅๆฃๆฅ +// +// ๆฅ่ฏข Notion ่ฐๅบฆ้ๅๆฐๆฎๅบไธญๆๅฎ็ฑปๅ+็ถๆ็ไปปๅก +// ็จๆณ: node scripts/bridge/check-queue.js [BROADCAST_READY|SYSLOG_RECEIVED|HEARTBEAT] +// +// ็ฏๅขๅ้๏ผ +// NOTION_TOKEN Notion API token +// BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID + +'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'; + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// 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(); + }); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function checkQueue(taskType) { + const token = process.env.NOTION_TOKEN; + const dbId = process.env.BRIDGE_QUEUE_DB_ID; + + if (!token) { + console.log('โ ๏ธ ็ผบๅฐ NOTION_TOKEN๏ผ่ทณ่ฟ่ฐๅบฆ้ๅๆฃๆฅ'); + process.exit(0); + } + if (!dbId) { + console.log('โ ๏ธ ็ผบๅฐ BRIDGE_QUEUE_DB_ID๏ผ่ทณ่ฟ่ฐๅบฆ้ๅๆฃๆฅ'); + process.exit(0); + } + + console.log(`๐ ๆฃๆฅ่ฐๅบฆ้ๅ ยท ็ฑปๅ=${taskType} ยท ็ถๆ=ๅพ ๅค็`); + + const filter = { + and: [ + { property: '็ฑปๅ', select: { equals: taskType } }, + { property: 'ๅค็็ถๆ', status: { equals: 'ๅพ ๅค็' } }, + ], + }; + + const body = { + filter, + sorts: [{ property: 'ๅๅปบๆถ้ด', direction: 'ascending' }], + page_size: 50, + }; + + try { + const result = await notionPost(`/v1/databases/${dbId}/query`, body, token); + const tasks = result.results || []; + + console.log(`๐ ๅ็ฐ ${tasks.length} ๆกๅพ ๅค็ไปปๅก`); + + // ๅฐไปปๅกๅ่กจๅๅ ฅไธดๆถๆไปถไพๅ็ปญๆญฅ้ชคไฝฟ็จ + const outputDir = path.join('data', 'bridge-logs'); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const outputFile = path.join(outputDir, `queue-${taskType.toLowerCase()}-${Date.now()}.json`); + const taskSummaries = tasks.map(t => ({ + id: t.id, + task_name: extractTitle(t), + task_type: taskType, + dev_id: extractSelect(t, 'DEV็ผๅท'), + broadcast_id: extractRichText(t, 'ๅนฟๆญ็ผๅท'), + source: extractRichText(t, 'ๆฅๆบๆธ ้'), + payload: extractRichText(t, 'Payload'), + created_time: t.created_time, + })); + + fs.writeFileSync(outputFile, JSON.stringify(taskSummaries, null, 2)); + console.log(`๐ ไปปๅกๅ่กจๅทฒๅๅ ฅ: ${outputFile}`); + + // ่พๅบๅฐ GITHUB_OUTPUT๏ผไพ workflow ไฝฟ็จ๏ผ + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `task_count=${tasks.length}\n` + + `queue_file=${outputFile}\n` + ); + } + + return taskSummaries; + } catch (e) { + console.error(`โ ๆฅ่ฏข่ฐๅบฆ้ๅๅคฑ่ดฅ: ${e.message}`); + process.exit(1); + } +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Notion ๅฑๆงๆๅ่พ ๅฉ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function extractTitle(page) { + const prop = page.properties && page.properties['ไปปๅกๅ็งฐ']; + if (prop && prop.title && prop.title.length > 0) { + return prop.title.map(t => t.plain_text).join(''); + } + return ''; +} + +function extractSelect(page, name) { + const prop = page.properties && page.properties[name]; + if (prop && prop.select) { + return prop.select.name || ''; + } + return ''; +} + +function extractRichText(page, name) { + const prop = page.properties && page.properties[name]; + if (prop && prop.rich_text && prop.rich_text.length > 0) { + return prop.rich_text.map(t => t.plain_text).join(''); + } + return ''; +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ๅ ฅๅฃ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +const taskType = process.argv[2] || 'BROADCAST_READY'; +checkQueue(taskType).catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/distribute.js b/scripts/bridge/distribute.js new file mode 100644 index 00000000..f3e736fa --- /dev/null +++ b/scripts/bridge/distribute.js @@ -0,0 +1,230 @@ +// scripts/bridge/distribute.js +// ๐ ๆกฅๆฅยทๅนฟๆญๅๅๅฐ IM +// +// ๆ นๆฎๅๅๆธ ๅ๏ผๆฅ่ฏขไบบๆ ผไฝๆณจๅ่กจ่ทๅ้็ฅๆธ ้๏ผ +// ๅฐ PDF ไธ่ฝฝ้พๆฅๆจ้ๅฐ้ฃไนฆ/้้/้ฎ็ฎฑ +// +// ็ฏๅขๅ้๏ผ +// NOTION_TOKEN Notion API token๏ผๆฅ่ฏขไบบๆ ผไฝๆณจๅ่กจ๏ผ +// DIST_MANIFEST dist-manifest.json ่ทฏๅพ +// FEISHU_APP_ID ้ฃไนฆ App ID +// FEISHU_APP_SECRET ้ฃไนฆ App Secret +// DINGTALK_TOKEN ้้ๆบๅจไบบ Token +// SMTP_USER ้ฎไปถๅ้่ +// SMTP_PASS ้ฎไปถๆๆ็ + +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf'); + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// HTTP ่ฏทๆฑ่พ ๅฉ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function httpsPost(hostname, apiPath, body, headers) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const opts = { + hostname, + port: 443, + path: apiPath, + method: 'POST', + headers: Object.assign({ + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + }, headers || {}), + }; + + const req = https.request(opts, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + resolve({ status: res.statusCode, body: data }); + }); + }); + + req.on('error', reject); + req.write(payload); + req.end(); + }); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ้ฃไนฆๅๅ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function getFeishuToken() { + const appId = process.env.FEISHU_APP_ID; + const appSecret = process.env.FEISHU_APP_SECRET; + + if (!appId || !appSecret) return null; + + const result = await httpsPost( + 'open.feishu.cn', + '/open-apis/auth/v3/tenant_access_token/internal', + { app_id: appId, app_secret: appSecret } + ); + + const parsed = JSON.parse(result.body); + return parsed.tenant_access_token || null; +} + +async function sendFeishuMessage(token, chatId, title, downloadUrl) { + const text = `๐ก ๆฐๅนฟๆญๅทฒ็ๆ\n\n๐ ${title}\n\n๐ ไธ่ฝฝ้พๆฅ: ${downloadUrl}\n\nโฐ ${new Date().toISOString()}`; + + return httpsPost( + 'open.feishu.cn', + '/open-apis/im/v1/messages?receive_id_type=chat_id', + { + receive_id: chatId, + msg_type: 'text', + content: JSON.stringify({ text }), + }, + { 'Authorization': 'Bearer ' + token } + ); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ้้ๅๅ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function sendDingtalkMessage(webhookToken, title, downloadUrl) { + const text = `๐ก ๆฐๅนฟๆญๅทฒ็ๆ\n\n๐ ${title}\n\n๐ ไธ่ฝฝ้พๆฅ: ${downloadUrl}`; + + return httpsPost( + 'oapi.dingtalk.com', + `/robot/send?access_token=${webhookToken}`, + { + msgtype: 'text', + text: { content: text }, + } + ); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ้ฎไปถๅๅ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function sendEmailNotification(to, title, downloadUrl) { + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + + if (!smtpUser || !smtpPass) { + console.log(' โ ๏ธ SMTP ๆช้ ็ฝฎ๏ผ่ทณ่ฟ้ฎไปถ้็ฅ'); + return; + } + + try { + const nodemailer = require('nodemailer'); + const transporter = nodemailer.createTransport({ + host: 'smtp.qq.com', + port: 465, + secure: true, + auth: { user: smtpUser, pass: smtpPass }, + }); + + await transporter.sendMail({ + from: `"ๅ ๆนๅนฟๆญ" <${smtpUser}>`, + to: to, + subject: `๐ก ${title}`, + html: ` +
ๆ ้ข: ${title}
+ไธ่ฝฝ้พๆฅ: ${downloadUrl}
+โ ๅ ๆนๅนฟๆญ็ณป็ป ยท guanghulab.com
+ `, + }); + console.log(` โ ้ฎไปถๅทฒๅ้: ${to}`); + } catch (e) { + console.error(` โ ้ฎไปถๅ้ๅคฑ่ดฅ: ${e.message}`); + } +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const manifestFile = process.env.DIST_MANIFEST || + path.join(OUTPUT_DIR, 'dist-manifest.json'); + + if (!fs.existsSync(manifestFile)) { + console.log('๐ญ ๆ dist-manifest.json๏ผ่ทณ่ฟๅๅ'); + process.exit(0); + } + + const items = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); + if (items.length === 0) { + console.log('๐ญ ๅๅๅ่กจไธบ็ฉบ๏ผ่ทณ่ฟ'); + process.exit(0); + } + + console.log(`๐ข ๅผๅงๅๅ ${items.length} ๆกๅนฟๆญโฆ`); + + // ้ฃไนฆ Token๏ผไธๆฌกๆง่ทๅ๏ผ + let feishuToken = null; + if (process.env.FEISHU_APP_ID) { + try { + feishuToken = await getFeishuToken(); + if (feishuToken) console.log(' ๐ ้ฃไนฆ Token ่ทๅๆๅ'); + } catch (e) { + console.error(` โ ๏ธ ้ฃไนฆ Token ่ทๅๅคฑ่ดฅ: ${e.message}`); + } + } + + const dingtalkToken = process.env.DINGTALK_TOKEN; + let distributed = 0; + + for (const item of items) { + const title = item.title || 'ๅ ๆนๅนฟๆญ'; + const downloadUrl = item.download_url || ''; + + if (!downloadUrl) { + console.log(` โ ๏ธ ${title}: ๆ ไธ่ฝฝ้พๆฅ๏ผ่ทณ่ฟ`); + continue; + } + + console.log(`\n๐ก ๅๅ: ${title}`); + + // ้ฃไนฆๅๅ๏ผๅนฟๆญๅฐ้ป่ฎค็พค๏ผ + if (feishuToken && process.env.FEISHU_BROADCAST_CHAT_ID) { + try { + await sendFeishuMessage(feishuToken, process.env.FEISHU_BROADCAST_CHAT_ID, title, downloadUrl); + console.log(' โ ้ฃไนฆ็พคๅทฒๆจ้'); + } catch (e) { + console.error(` โ ้ฃไนฆๆจ้ๅคฑ่ดฅ: ${e.message}`); + } + } + + // ้้ๅๅ + if (dingtalkToken) { + try { + await sendDingtalkMessage(dingtalkToken, title, downloadUrl); + console.log(' โ ้้ๅทฒๆจ้'); + } catch (e) { + console.error(` โ ้้ๆจ้ๅคฑ่ดฅ: ${e.message}`); + } + } + + // ้ฎไปถๅๅ๏ผๅ้็ปๅฐๆ๏ผ + if (process.env.SMTP_USER && process.env.ALERT_EMAIL) { + await sendEmailNotification(process.env.ALERT_EMAIL, title, downloadUrl); + } + + distributed++; + } + + console.log(`\nโ ๅๅๅฎๆ ยท ${distributed}/${items.length} ๆกๅนฟๆญๅทฒๆจ้`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `distributed_count=${distributed}\n` + ); + } +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/fetch-broadcast.js b/scripts/bridge/fetch-broadcast.js new file mode 100644 index 00000000..1420573a --- /dev/null +++ b/scripts/bridge/fetch-broadcast.js @@ -0,0 +1,234 @@ +// scripts/bridge/fetch-broadcast.js +// ๐ ๆกฅๆฅยทไป Notion ๆๅๅนฟๆญๅ ๅฎน +// +// ๆ นๆฎ่ฐๅบฆ้ๅไธญ BROADCAST_READY ไปปๅก็ๅ ณ่้กต้ข๏ผ +// ไป Notion ่ฏปๅๅนฟๆญ้กต้ขๅฎๆดๅ ๅฎน๏ผ่ฝฌๆขไธบ Markdown ๆ ผๅผ +// +// ็ฏๅขๅ้๏ผ +// NOTION_TOKEN Notion API token +// QUEUE_FILE check-queue.js ่พๅบ็ไปปๅกๅ่กจๆไปถ่ทฏๅพ + +'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 OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf'); + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Notion API +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function notionRequest(method, endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : ''; + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: method, + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }, + }; + if (payload) { + opts.headers['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); + if (payload) req.write(payload); + req.end(); + }); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Notion Block โ Markdown ่ฝฌๆข +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function blockToMarkdown(block) { + const type = block.type; + const content = block[type]; + if (!content) return ''; + + const text = extractRichTextContent(content.rich_text); + + switch (type) { + case 'paragraph': return text + '\n'; + case 'heading_1': return '# ' + text + '\n'; + case 'heading_2': return '## ' + text + '\n'; + case 'heading_3': return '### ' + text + '\n'; + case 'bulleted_list_item': return '- ' + text + '\n'; + case 'numbered_list_item': return '1. ' + text + '\n'; + case 'to_do': return (content.checked ? '- [x] ' : '- [ ] ') + text + '\n'; + case 'toggle': return '> ' + text + '\n'; + case 'code': return '```' + (content.language || '') + '\n' + text + '\n```\n'; + case 'quote': return '> ' + text + '\n'; + case 'divider': return '---\n'; + case 'callout': return '> ' + (content.icon?.emoji || '๐ก') + ' ' + text + '\n'; + default: return text ? text + '\n' : ''; + } +} + +function extractRichTextContent(richTextArray) { + if (!richTextArray || !Array.isArray(richTextArray)) return ''; + return richTextArray.map(t => { + let text = t.plain_text || ''; + if (t.annotations) { + if (t.annotations.bold) text = '**' + text + '**'; + if (t.annotations.italic) text = '*' + text + '*'; + if (t.annotations.code) text = '`' + text + '`'; + if (t.annotations.strikethrough) text = '~~' + text + '~~'; + } + if (t.href) text = '[' + text + '](' + t.href + ')'; + return text; + }).join(''); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ่ทๅ้กต้ขๆๆ blocks +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function getPageBlocks(pageId, token) { + let allBlocks = []; + let cursor = undefined; + + do { + const endpoint = `/v1/blocks/${pageId}/children?page_size=100` + + (cursor ? `&start_cursor=${cursor}` : ''); + const result = await notionRequest('GET', endpoint, null, token); + allBlocks = allBlocks.concat(result.results || []); + cursor = result.has_more ? result.next_cursor : undefined; + } while (cursor); + + return allBlocks; +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ่ทๅ้กต้ขๆ ้ข +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function getPageTitle(pageId, token) { + const page = await notionRequest('GET', `/v1/pages/${pageId}`, null, token); + const props = page.properties || {}; + for (const key of Object.keys(props)) { + if (props[key].type === 'title' && props[key].title) { + return props[key].title.map(t => t.plain_text).join(''); + } + } + return 'Untitled'; +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const token = process.env.NOTION_TOKEN; + const queueFile = process.env.QUEUE_FILE; + + if (!token) { + console.log('โ ๏ธ ็ผบๅฐ NOTION_TOKEN๏ผ่ทณ่ฟๅนฟๆญๆๅ'); + process.exit(0); + } + + if (!queueFile || !fs.existsSync(queueFile)) { + console.log('๐ญ ๆ ๅพ ๅค็็ BROADCAST_READY ไปปๅก๏ผ่ทณ่ฟ'); + process.exit(0); + } + + const tasks = JSON.parse(fs.readFileSync(queueFile, 'utf8')); + if (tasks.length === 0) { + console.log('๐ญ ไปปๅกๅ่กจไธบ็ฉบ๏ผ่ทณ่ฟ'); + process.exit(0); + } + + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + } + + console.log(`๐ฅ ๅๅคๆๅ ${tasks.length} ๆกๅนฟๆญๅ ๅฎนโฆ`); + + const mdFiles = []; + let ok = 0, failed = 0; + + for (const task of tasks) { + // ๅฐ่ฏไป Payload ไธญๆๅ้กต้ข ID + let pageId = ''; + try { + const payload = JSON.parse(task.payload || '{}'); + pageId = payload.page_id || payload.broadcast_page_id || ''; + } catch (_) { /* ignore */ } + + if (!pageId) { + console.log(` โ ๏ธ ไปปๅก ${task.task_name} ๆ ๅ ณ่้กต้ข ID๏ผ่ทณ่ฟ`); + failed++; + continue; + } + + try { + console.log(` ๐ ๆๅๅนฟๆญ: ${task.task_name} (page: ${pageId})`); + + const title = await getPageTitle(pageId, token); + const blocks = await getPageBlocks(pageId, token); + + // ่ฝฌๆขไธบ Markdown + let markdown = `# ${title}\n\n`; + markdown += `> ๅนฟๆญ็ผๅท: ${task.broadcast_id || 'N/A'}\n`; + markdown += `> ็ๆๆถ้ด: ${new Date().toISOString()}\n\n`; + markdown += '---\n\n'; + + for (const block of blocks) { + markdown += blockToMarkdown(block); + } + + // ๅๅ ฅ Markdown ๆไปถ + const safeName = (task.broadcast_id || task.task_name || 'broadcast') + .replace(/[^a-zA-Z0-9_\-\u4e00-\u9fa5]/g, '_'); + const mdFile = path.join(OUTPUT_DIR, `${safeName}.md`); + fs.writeFileSync(mdFile, markdown, 'utf8'); + mdFiles.push({ file: mdFile, task_id: task.id, title }); + console.log(` โ ${mdFile} (${blocks.length} blocks)`); + ok++; + } catch (e) { + console.error(` โ ๆๅๅคฑ่ดฅ: ${task.task_name}: ${e.message}`); + failed++; + } + } + + console.log(`\nโ ๅนฟๆญๅ ๅฎนๆๅๅฎๆ ยท ๆๅ ${ok} ๆก ยท ๅคฑ่ดฅ ${failed} ๆก`); + + // ๅฐ Markdown ๆไปถๅ่กจๅๅ ฅไพๅ็ปญๆญฅ้ชคไฝฟ็จ + const manifestFile = path.join(OUTPUT_DIR, 'manifest.json'); + fs.writeFileSync(manifestFile, JSON.stringify(mdFiles, null, 2)); + console.log(`๐ ๆธ ๅๅทฒๅๅ ฅ: ${manifestFile}`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `broadcast_count=${ok}\nmanifest_file=${manifestFile}\n` + ); + } +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/generate-pdf.js b/scripts/bridge/generate-pdf.js new file mode 100644 index 00000000..d052ee41 --- /dev/null +++ b/scripts/bridge/generate-pdf.js @@ -0,0 +1,163 @@ +// scripts/bridge/generate-pdf.js +// ๐ ๆกฅๆฅยทMarkdown โ PDF ็ๆ +// +// ่ฏปๅ data/broadcasts/pdf/ ไธ็ Markdown ๆไปถ๏ผ +// ไฝฟ็จ md-to-pdf ่ฝฌๆขไธบ PDF๏ผๅฅ็จๅ ๆนๅนฟๆญๆจกๆฟ +// +// ็ฏๅขๅ้๏ผ +// MANIFEST_FILE fetch-broadcast.js ็ๆ็ๆธ ๅๆไปถ่ทฏๅพ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf'); + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// PDF ๆ ทๅผๆจกๆฟ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +const PDF_CSS = ` +body { + font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 1.8; + color: #333; + padding: 40px; +} +h1 { + font-size: 24px; + border-bottom: 2px solid #1a73e8; + padding-bottom: 10px; + margin-bottom: 20px; +} +h2 { font-size: 20px; margin-top: 30px; } +h3 { font-size: 16px; margin-top: 20px; } +blockquote { + border-left: 4px solid #1a73e8; + padding: 10px 20px; + margin: 15px 0; + background: #f8f9fa; + color: #555; +} +code { + background: #f0f0f0; + padding: 2px 6px; + border-radius: 3px; + font-size: 13px; +} +pre code { + display: block; + padding: 15px; + overflow-x: auto; +} +hr { + border: none; + border-top: 1px solid #ddd; + margin: 20px 0; +} +@page { + margin: 20mm; + @top-center { content: "ๅ ๆนๅนฟๆญ ยท guanghulab.com"; font-size: 10px; color: #999; } + @bottom-center { content: counter(page) " / " counter(pages); font-size: 10px; color: #999; } +} +`; + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const manifestFile = process.env.MANIFEST_FILE || + path.join(OUTPUT_DIR, 'manifest.json'); + + if (!fs.existsSync(manifestFile)) { + console.log('๐ญ ๆ manifest.json๏ผ่ทณ่ฟ PDF ็ๆ'); + process.exit(0); + } + + const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); + if (manifest.length === 0) { + console.log('๐ญ ๆธ ๅไธบ็ฉบ๏ผ่ทณ่ฟ PDF ็ๆ'); + process.exit(0); + } + + // ๅจๆๅ ่ฝฝ md-to-pdf๏ผๅจ workflow ไธญๅฎ่ฃ ๏ผ + let mdToPdf; + try { + mdToPdf = require('md-to-pdf').mdToPdf; + } catch (e) { + console.error('โ md-to-pdf ๆชๅฎ่ฃ ๏ผ่ฏทๅ ่ฟ่ก: npm install md-to-pdf'); + console.log('โ ๏ธ ๅ้ๅฐ็บฏๆๆฌ PDF ๆจกๅผ๏ผๅฐ Markdown ๅๆไฟ็๏ผ'); + + // ๅ้๏ผไป ไฟ็ .md ๆไปถ๏ผๆ ่ฎฐไธบๅพ ๅค็ + for (const item of manifest) { + if (fs.existsSync(item.file)) { + const pdfPath = item.file.replace(/\.md$/, '.pdf.pending'); + fs.copyFileSync(item.file, pdfPath); + console.log(` ๐ ${pdfPath} (ๅพ ่ฝฌๆข)`); + } + } + return; + } + + console.log(`๐ ๅผๅง็ๆ ${manifest.length} ไปฝ PDFโฆ`); + + let ok = 0, failed = 0; + const pdfFiles = []; + + for (const item of manifest) { + if (!fs.existsSync(item.file)) { + console.log(` โ ๏ธ ๆไปถไธๅญๅจ: ${item.file}`); + failed++; + continue; + } + + try { + const pdfPath = item.file.replace(/\.md$/, '.pdf'); + + const result = await mdToPdf( + { path: path.resolve(item.file) }, + { + stylesheet: [], + css: PDF_CSS, + pdf_options: { + format: 'A4', + margin: { top: '25mm', bottom: '25mm', left: '20mm', right: '20mm' }, + printBackground: true, + }, + launch_options: { args: ['--no-sandbox', '--disable-setuid-sandbox'] }, + } + ); + + if (result && result.content) { + fs.writeFileSync(pdfPath, result.content); + pdfFiles.push({ ...item, pdf: pdfPath }); + console.log(` โ ${pdfPath}`); + ok++; + } else { + console.error(` โ PDF ๅ ๅฎนไธบ็ฉบ: ${item.file}`); + failed++; + } + } catch (e) { + console.error(` โ PDF ็ๆๅคฑ่ดฅ: ${item.file}: ${e.message}`); + failed++; + } + } + + console.log(`\nโ PDF ็ๆๅฎๆ ยท ๆๅ ${ok} ไปฝ ยท ๅคฑ่ดฅ ${failed} ไปฝ`); + + // ๆดๆฐ manifest ๅ ๅซ PDF ่ทฏๅพ + const pdfManifest = path.join(OUTPUT_DIR, 'pdf-manifest.json'); + fs.writeFileSync(pdfManifest, JSON.stringify(pdfFiles, null, 2)); + console.log(`๐ PDF ๆธ ๅ: ${pdfManifest}`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `pdf_count=${ok}\npdf_manifest=${pdfManifest}\n` + ); + } +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/heartbeat.js b/scripts/bridge/heartbeat.js new file mode 100644 index 00000000..b9e1d9be --- /dev/null +++ b/scripts/bridge/heartbeat.js @@ -0,0 +1,214 @@ +// scripts/bridge/heartbeat.js +// ๐ ๆกฅๆฅยทๅฟ่ทณๆฃๆต +// +// ๅฎๆถๆฃๆตๆกฅๆฅ้้็ ้ๆง๏ผ +// 1. ๆต่ฏ Notion API ่ฟ้ๆง +// 2. ๅจ่ฐๅบฆ้ๅๅๅปบ HEARTBEAT ไปปๅก +// 3. ่ฎฐๅฝๅฟ่ทณๆฅๅฟ +// +// ็ฏๅขๅ้๏ผ +// NOTION_TOKEN Notion API token +// BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID + +'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_TITLE_MAX = 120; +const NOTION_RICH_TEXT_MAX = 2000; + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Notion API +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function notionRequest(method, endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = body ? JSON.stringify(body) : ''; + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: method, + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + }, + }; + if (payload) { + opts.headers['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); + resolve({ status: res.statusCode, data: parsed }); + } catch (e) { + resolve({ status: res.statusCode, data: data }); + } + }); + }); + + req.on('error', reject); + if (payload) 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) } }]; +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ๅฟ่ทณๆฃๆต +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function checkNotionHealth(token) { + const start = Date.now(); + try { + const result = await notionRequest('GET', '/v1/users/me', null, token); + const elapsed = Date.now() - start; + return { + healthy: result.status >= 200 && result.status < 300, + status: result.status, + latency: elapsed, + user: result.data?.name || 'unknown', + }; + } catch (e) { + return { + healthy: false, + status: 0, + latency: Date.now() - start, + error: e.message, + }; + } +} + +async function createHeartbeatTask(queueDbId, healthResult, token) { + const now = new Date().toISOString(); + const taskName = `HEARTBEAT-${now.slice(0, 10)}-${now.slice(11, 19).replace(/:/g, '')}`; + + const status = healthResult.healthy ? 'โ ้้็ ้' : 'โ ้้ๅผๅธธ'; + const payload = JSON.stringify({ + notion_healthy: healthResult.healthy, + notion_status: healthResult.status, + notion_latency: healthResult.latency, + notion_user: healthResult.user || '', + error: healthResult.error || '', + timestamp: now, + }); + + const properties = { + 'ไปปๅกๅ็งฐ': { title: titleProp(taskName) }, + '็ฑปๅ': { select: { name: 'HEARTBEAT' } }, + 'ๅค็็ถๆ': { status: { name: healthResult.healthy ? 'ๅทฒๅฎๆ' : 'ๅพ ๅค็' } }, + 'ๆฅๆบๆธ ้': { rich_text: richText('้ธๆธๅฟ่ทณ') }, + 'Payload': { rich_text: richText(payload) }, + }; + + const body = { parent: { database_id: queueDbId }, properties }; + + try { + const result = await notionRequest('POST', '/v1/pages', body, token); + if (result.status >= 200 && result.status < 300) { + return { success: true, page_id: result.data.id }; + } + return { success: false, error: `${result.status}: ${JSON.stringify(result.data)}` }; + } catch (e) { + return { success: false, error: e.message }; + } +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const token = process.env.NOTION_TOKEN; + const queueDb = process.env.BRIDGE_QUEUE_DB_ID; + + console.log('๐ ๆกฅๆฅๅฟ่ทณๆฃๆตๅผๅงโฆ'); + console.log(`โฐ ${new Date().toISOString()}`); + + const heartbeat = { + timestamp: new Date().toISOString(), + notion: { healthy: false }, + queue: { created: false }, + }; + + // โ ๆต่ฏ Notion API ่ฟ้ๆง + if (token) { + console.log('\n๐ก ๆฃๆต Notion API ่ฟ้ๆงโฆ'); + heartbeat.notion = await checkNotionHealth(token); + + if (heartbeat.notion.healthy) { + console.log(` โ Notion API ๆญฃๅธธ ยท ๅปถ่ฟ ${heartbeat.notion.latency}ms ยท ็จๆท: ${heartbeat.notion.user}`); + } else { + console.error(` โ Notion API ๅผๅธธ ยท ็ถๆ ${heartbeat.notion.status} ยท ${heartbeat.notion.error || ''}`); + } + } else { + console.log(' โ ๏ธ NOTION_TOKEN ๆช้ ็ฝฎ๏ผ่ทณ่ฟ Notion ๆฃๆต'); + } + + // โก ๅจ่ฐๅบฆ้ๅๅๅปบๅฟ่ทณ่ฎฐๅฝ + if (token && queueDb) { + console.log('\n๐ ๅๅ ฅๅฟ่ทณ่ฎฐๅฝๅฐ่ฐๅบฆ้ๅโฆ'); + const queueResult = await createHeartbeatTask(queueDb, heartbeat.notion, token); + heartbeat.queue = queueResult; + + if (queueResult.success) { + console.log(` โ ๅฟ่ทณ่ฎฐๅฝๅทฒๅๅปบ: ${queueResult.page_id}`); + } else { + console.error(` โ ๅฟ่ทณ่ฎฐๅฝๅๅปบๅคฑ่ดฅ: ${queueResult.error}`); + } + } else { + console.log(' โ ๏ธ ่ฐๅบฆ้ๅๆช้ ็ฝฎ๏ผ่ทณ่ฟๅฟ่ทณ่ฎฐๅฝ'); + } + + // โข ๅๅ ฅๆฌๅฐๅฟ่ทณๆฅๅฟ + const logDir = path.join('data', 'bridge-logs'); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + const logFile = path.join(logDir, `heartbeat-${new Date().toISOString().slice(0, 10)}.json`); + + let logs = []; + if (fs.existsSync(logFile)) { + try { logs = JSON.parse(fs.readFileSync(logFile, 'utf8')); } catch (_) { logs = []; } + } + logs.push(heartbeat); + fs.writeFileSync(logFile, JSON.stringify(logs, null, 2)); + + console.log(`\n๐ ๅฟ่ทณๆฅๅฟ: ${logFile}`); + + // โฃ ๆป็ป + const allHealthy = heartbeat.notion.healthy; + console.log(`\n${allHealthy ? 'โ ' : 'โ'} ๅฟ่ทณๆฃๆตๅฎๆ ยท Notion: ${heartbeat.notion.healthy ? 'ๆญฃๅธธ' : 'ๅผๅธธ'}`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `notion_healthy=${heartbeat.notion.healthy}\nheartbeat_status=${allHealthy ? 'ok' : 'error'}\n` + ); + } + + // ๅฟ่ทณๅผๅธธไธ้ๅบๅคฑ่ดฅ๏ผ้ฟๅ workflow ้ข็นๆฅ้๏ผ +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/process-syslog-batch.js b/scripts/bridge/process-syslog-batch.js new file mode 100644 index 00000000..521c7571 --- /dev/null +++ b/scripts/bridge/process-syslog-batch.js @@ -0,0 +1,252 @@ +// 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); }); diff --git a/scripts/bridge/update-queue-status.js b/scripts/bridge/update-queue-status.js new file mode 100644 index 00000000..dfcaa4d2 --- /dev/null +++ b/scripts/bridge/update-queue-status.js @@ -0,0 +1,117 @@ +// scripts/bridge/update-queue-status.js +// ๐ ๆกฅๆฅยทๆดๆฐ่ฐๅบฆ้ๅ็ถๆ +// +// ๅฐๅทฒๅค็็่ฐๅบฆ้ๅไปปๅก็ถๆๆดๆฐไธบใๅทฒๅฎๆใ +// +// ็ฏๅขๅ้๏ผ +// NOTION_TOKEN Notion API token +// BRIDGE_QUEUE_DB_ID ๆกฅๆฅ่ฐๅบฆ้ๅๆฐๆฎๅบ ID +// QUEUE_FILE check-queue.js ่พๅบ็ไปปๅกๅ่กจๆไปถ่ทฏๅพ + +'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'; + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// Notion API +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +function notionPatch(endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: 'PATCH', + 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(); + }); +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const token = process.env.NOTION_TOKEN; + const queueFile = process.env.QUEUE_FILE; + + if (!token) { + console.log('โ ๏ธ ็ผบๅฐ NOTION_TOKEN๏ผ่ทณ่ฟ็ถๆๆดๆฐ'); + process.exit(0); + } + + if (!queueFile || !fs.existsSync(queueFile)) { + console.log('๐ญ ๆ ๅพ ๆดๆฐ็ไปปๅก๏ผ่ทณ่ฟ'); + process.exit(0); + } + + const tasks = JSON.parse(fs.readFileSync(queueFile, 'utf8')); + if (tasks.length === 0) { + console.log('๐ญ ไปปๅกๅ่กจไธบ็ฉบ๏ผ่ทณ่ฟ'); + process.exit(0); + } + + console.log(`โ๏ธ ๆดๆฐ ${tasks.length} ๆกไปปๅก็ถๆไธบใๅทฒๅฎๆใโฆ`); + + let ok = 0, failed = 0; + + for (const task of tasks) { + try { + await notionPatch(`/v1/pages/${task.id}`, { + properties: { + 'ๅค็็ถๆ': { status: { name: 'ๅทฒๅฎๆ' } }, + }, + }, token); + console.log(` โ ${task.task_name} โ ๅทฒๅฎๆ`); + ok++; + } catch (e) { + console.error(` โ ${task.task_name} ๆดๆฐๅคฑ่ดฅ: ${e.message}`); + failed++; + } + } + + console.log(`\nโ ็ถๆๆดๆฐๅฎๆ ยท ๆๅ ${ok} ๆก ยท ๅคฑ่ดฅ ${failed} ๆก`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `updated_count=${ok}\n` + ); + } + + if (failed > 0) process.exit(1); +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/bridge/upload-pdf.js b/scripts/bridge/upload-pdf.js new file mode 100644 index 00000000..a897d09e --- /dev/null +++ b/scripts/bridge/upload-pdf.js @@ -0,0 +1,181 @@ +// scripts/bridge/upload-pdf.js +// ๐ ๆกฅๆฅยทPDF ไธไผ +// +// ๅฐ็ๆ็ PDF ๆไปถไธไผ ๅฐๅฏไธ่ฝฝไฝ็ฝฎ +// ๆฏๆๅค็งไธไผ ๆนๅผ๏ผ +// ๆนๆกA๏ผ้ฟ้ไบ OSS๏ผๅฝๅ ๅฟซ๏ผ +// ๆนๆกB๏ผGitHub Release Assets +// ๆนๆกC๏ผๆๅกๅจ็ฎๅฝ๏ผ/var/www/guanghulab/broadcasts/๏ผ +// +// ็ฏๅขๅ้๏ผ +// PDF_MANIFEST pdf-manifest.json ่ทฏๅพ +// UPLOAD_MODE ไธไผ ๆจกๅผ: oss | github | server๏ผ้ป่ฎค server๏ผ +// OSS_ACCESS_KEY ้ฟ้ไบ OSS AccessKey +// OSS_SECRET_KEY ้ฟ้ไบ OSS SecretKey +// OSS_BUCKET ้ฟ้ไบ OSS Bucket +// OSS_REGION ้ฟ้ไบ OSS Region +// SERVER_BROADCAST_DIR ๆๅกๅจๅนฟๆญ็ฎๅฝ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf'); + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธไผ ๆจกๅผ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +/** + * ๆๅกๅจๆฌๅฐๆจกๅผ๏ผๅคๅถ PDF ๅฐๆๅฎ็ฎๅฝ + */ +async function uploadToServer(pdfFiles) { + const serverDir = process.env.SERVER_BROADCAST_DIR || + '/var/www/guanghulab/broadcasts'; + + // ๅจ CI ็ฏๅขไธญ๏ผๅช่ฎฐๅฝ่ทฏๅพ๏ผไธๅฎ้ ๅคๅถ๏ผ + const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'; + + console.log(`โ๏ธ ๆๅกๅจๆจกๅผ ยท ็ฎๆ ็ฎๅฝ: ${serverDir}`); + + const results = []; + for (const item of pdfFiles) { + const pdfPath = item.pdf; + if (!pdfPath || !fs.existsSync(pdfPath)) { + console.log(` โ ๏ธ PDF ไธๅญๅจ: ${pdfPath}`); + continue; + } + + const filename = path.basename(pdfPath); + const destPath = path.join(serverDir, filename); + + if (isCI) { + // CI ็ฏๅข๏ผไป ่ฎฐๅฝ๏ผ็ฑ้จ็ฝฒ่ๆฌ่ด่ดฃๅฎ้ ๅคๅถ + const downloadUrl = `https://guanghulab.com/broadcasts/${filename}`; + results.push({ ...item, download_url: downloadUrl }); + console.log(` ๐ ${filename} โ ${downloadUrl} (CIๆจกๅผยทๅพ ้จ็ฝฒ)`); + } else { + try { + if (!fs.existsSync(serverDir)) { + fs.mkdirSync(serverDir, { recursive: true }); + } + fs.copyFileSync(pdfPath, destPath); + const downloadUrl = `https://guanghulab.com/broadcasts/${filename}`; + results.push({ ...item, download_url: downloadUrl }); + console.log(` โ ${filename} โ ${destPath}`); + } catch (e) { + console.error(` โ ๅคๅถๅคฑ่ดฅ: ${filename}: ${e.message}`); + } + } + } + return results; +} + +/** + * GitHub ๆจกๅผ๏ผๅฐ PDF ๆไบคๅฐไปๅบ๏ผๅทฒๅจ data/broadcasts/pdf/ ไธญ๏ผ + */ +async function uploadToGitHub(pdfFiles) { + console.log(`โ๏ธ GitHub ๆจกๅผ ยท PDF ๅทฒๅจไปๅบ data/broadcasts/pdf/ ไธญ`); + + const results = []; + for (const item of pdfFiles) { + const pdfPath = item.pdf; + if (!pdfPath || !fs.existsSync(pdfPath)) continue; + + const filename = path.basename(pdfPath); + const downloadUrl = `https://github.com/qinfendebingshuo/guanghulab/raw/main/${pdfPath}`; + results.push({ ...item, download_url: downloadUrl }); + console.log(` โ ${filename} โ ${downloadUrl}`); + } + return results; +} + +/** + * OSS ๆจกๅผ๏ผไธไผ ๅฐ้ฟ้ไบ OSS๏ผ้ๅฎ่ฃ ali-oss SDK๏ผ + */ +async function uploadToOSS(pdfFiles) { + const accessKey = process.env.OSS_ACCESS_KEY; + const secretKey = process.env.OSS_SECRET_KEY; + const bucket = process.env.OSS_BUCKET || 'guanghulab-broadcasts'; + const region = process.env.OSS_REGION || 'oss-cn-shanghai'; + + if (!accessKey || !secretKey) { + console.log('โ ๏ธ OSS ๅฏ้ฅๆช้ ็ฝฎ๏ผๅ้ๅฐ GitHub ๆจกๅผ'); + return uploadToGitHub(pdfFiles); + } + + console.log(`โ๏ธ OSS ๆจกๅผ ยท Bucket: ${bucket} ยท Region: ${region}`); + + let OSS; + try { + OSS = require('ali-oss'); + } catch (e) { + console.log('โ ๏ธ ali-oss ๆชๅฎ่ฃ ๏ผๅ้ๅฐ GitHub ๆจกๅผ'); + return uploadToGitHub(pdfFiles); + } + + const client = new OSS({ region, accessKeyId: accessKey, accessKeySecret: secretKey, bucket }); + + const results = []; + for (const item of pdfFiles) { + const pdfPath = item.pdf; + if (!pdfPath || !fs.existsSync(pdfPath)) continue; + + const filename = path.basename(pdfPath); + const ossKey = `broadcasts/${filename}`; + + try { + const result = await client.put(ossKey, pdfPath); + results.push({ ...item, download_url: result.url }); + console.log(` โ ${filename} โ ${result.url}`); + } catch (e) { + console.error(` โ OSS ไธไผ ๅคฑ่ดฅ: ${filename}: ${e.message}`); + } + } + return results; +} + +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +// ไธป้ป่พ +// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + +async function main() { + const manifestFile = process.env.PDF_MANIFEST || + path.join(OUTPUT_DIR, 'pdf-manifest.json'); + + if (!fs.existsSync(manifestFile)) { + console.log('๐ญ ๆ pdf-manifest.json๏ผ่ทณ่ฟไธไผ '); + process.exit(0); + } + + const pdfFiles = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); + if (pdfFiles.length === 0) { + console.log('๐ญ PDF ๅ่กจไธบ็ฉบ๏ผ่ทณ่ฟไธไผ '); + process.exit(0); + } + + const mode = (process.env.UPLOAD_MODE || 'server').toLowerCase(); + console.log(`๐ PDF ไธไผ ยท ๆจกๅผ: ${mode} ยท ๅ ฑ ${pdfFiles.length} ไปฝ`); + + let results; + switch (mode) { + case 'oss': results = await uploadToOSS(pdfFiles); break; + case 'github': results = await uploadToGitHub(pdfFiles); break; + case 'server': + default: results = await uploadToServer(pdfFiles); break; + } + + // ๅๅ ฅๅๅๆธ ๅ + const distManifest = path.join(OUTPUT_DIR, 'dist-manifest.json'); + fs.writeFileSync(distManifest, JSON.stringify(results, null, 2)); + console.log(`\nโ ไธไผ ๅฎๆ ยท ${results.length} ไปฝ ยท ๆธ ๅ: ${distManifest}`); + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, + `uploaded_count=${results.length}\ndist_manifest=${distManifest}\n` + ); + } +} + +main().catch(e => { console.error(e); process.exit(1); });