Files
JianMiauandClaude Fable 5 2e88a7d85a 建立建喵打羽球 Bot:移植 Node-RED 報名流程並新增群組記分板
根本原因:
原本的「今日羽球報名」bot 寫在 Node-RED 的 function 節點裡,不易測試與擴充;
羽球團又需要在 Telegram 群組內直接記分、看發球者,並把戰績寫進 web 版共用的 DB。

影響:
同一個 bot(@JianMiauBadmintonBot)改由本專案服務:報名指令、按鈕、暱稱、
週一 10:00 排程、白名單行為與訊息格式皆與 Node-RED 版相同,舊訊息按鈕仍可用;
另新增 /記分(/score、/new)記分板,並在結束/再一局時寫入共用的 history 表。

修法:
- signup.js:逐句移植 Node-RED bd_sm 狀態機(attendance / tg_poll / tg_members)
- scoreboard.js + match.js + render.js:羽球規則(發球區、換位、Deuce、30 分封頂)、
  上排 1 2/下排 4 3 版面、從今日出席選人開局、⏱ 結算、兩段式結束確認、
  開局者/管理員權限、scoreList(web 版格式)
- history.js:寫入 history 表,欄位與 web 版 ensureHistoryTable 相同
- scripts/nodered-switch.mjs:透過 admin API 暫停/恢復 Node-RED 舊流程
- 單元測試 23 項(規則、報名、選人、結算、權限、歷史寫入),Dockerfile / compose

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 11:10:27 +08:00

53 lines
2.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 切換 Node-RED 舊版「羽球報名」流程的開關(同一個 bot 只能有一個 polling 端)
//
// node scripts/nodered-switch.mjs pause # 停用 bd_tab、bot 設定改 send-only → 讓本專案接手
// node scripts/nodered-switch.mjs resume # 還原 Node-RED polling(本專案要先停)
// node scripts/nodered-switch.mjs status
//
// 用 Node-RED admin APIhttp://<host>:1880/flows),deploy type = nodes(只重啟有變動的節點)。
import 'dotenv/config'
const BASE = (process.env.NODERED_URL || 'http://192.168.5.45:1880').replace(/\/$/, '')
const TAB_ID = process.env.NODERED_BD_TAB || 'bd_tab'
const BOT_CONFIG_ID = process.env.NODERED_BD_BOT || 'tgbot_bd_config'
const mode = process.argv[2]
if (!['pause', 'resume', 'status'].includes(mode)) {
console.error('用法:node scripts/nodered-switch.mjs pause|resume|status')
process.exit(1)
}
const headers = { 'Node-RED-API-Version': 'v2', 'Content-Type': 'application/json' }
const res = await fetch(`${BASE}/flows`, { headers })
if (!res.ok) throw new Error(`GET /flows 失敗:${res.status}`)
const { rev, flows } = await res.json()
const tab = flows.find((n) => n.id === TAB_ID)
const bot = flows.find((n) => n.id === BOT_CONFIG_ID)
if (!tab || !bot) throw new Error(`找不到 ${TAB_ID}${BOT_CONFIG_ID}`)
const show = () =>
console.log(`Node-RED ${BASE}\n ${TAB_ID}: ${tab.disabled ? '停用' : '啟用'}\n ${BOT_CONFIG_ID}.updatemode: ${bot.updatemode}`)
if (mode === 'status') {
show()
process.exit(0)
}
if (mode === 'pause') {
tab.disabled = true
bot.updatemode = 'none'
} else {
tab.disabled = false
bot.updatemode = 'polling'
}
const post = await fetch(`${BASE}/flows`, {
method: 'POST',
headers: { ...headers, 'Node-RED-Deployment-Type': 'nodes' },
body: JSON.stringify({ rev, flows }),
})
if (!post.ok) throw new Error(`POST /flows 失敗:${post.status} ${await post.text()}`)
console.log(`✅ 已${mode === 'pause' ? '暫停' : '恢復'} Node-RED 羽球報名流程(rev ${(await post.json()).rev.slice(0, 8)}`)
show()