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