iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

! 本篇文章將會介紹 Discord 告警:讓機器人主動回報戰況,期望大家都能用一支 webhook 腳本,讓 agent 學會報平安與求救 :D

昨天聊錯誤處理,文中反覆出現的 "$NOTIFY" -s ok/warn/error 就是今天的主角本體——一支三十幾行的 bash,叫 discord-notify.sh。順便把 d08 立下的旗也拔了:「第 13 天會專文展開告警分級」。口號既然是「我耍廢、Agent 完賽」,系統狀態就必須主動送到我手機上,而不是我每天開終端機巡邏——需要巡邏的自動化,耍的是自己。

把告警分成兩種來想會很清楚:心跳(heartbeat)與警報(alert)。心跳是「我還活著」的定期回報,警報是「出事了」的即時求救。缺了心跳,你只能等出事才知道系統掛了,而且掛了多久無從得知。今天就用 Discord webhook 把兩種都建起來。

本篇目標

讀完這篇你會學到:

  • Discord webhook 的最小知識:三分鐘拿到 URL、一發 curl 就能讓機器人說話,以及為什麼 URL 本身就是密碼
  • 一支可複用的 discord-notify.sh:分級顏色、embed 版面、發進指定 thread、只信 HTTP 狀態碼
  • 事件 → 等級的告警設計:什麼事該喊、喊哪一級、心跳與警報怎麼分工

環境準備

  • macOS(內建 curl;jq 沒有的話用 Homebrew 裝)
  • 一個你有頻道管理權限的 Discord 伺服器(免費)
  • 手機裝 Discord app——耍廢組告警的終點是沙發上的手機
# jq 用來安全產生 JSON payload(手組字串遇到引號、換行就炸)
brew install jq

# 確認 curl 在(macOS 內建)
curl --version | head -1

主要內容

步驟一:三分鐘取得 webhook

到 Discord 伺服器 → 目標頻道右鍵「編輯頻道」→「整合」→「建立 Webhook」,複製 URL。它長這樣(本文一律用佔位符):

https://discord.com/api/webhooks/<ID>/<TOKEN>

webhook 就是一個「只准 POST 的發言入口」:不用建 bot、不用 OAuth,拿到 URL 就能讓一個機器人分身在頻道裡說話。先講最重要的安全觀念——這條 URL 就是密碼。任何人拿到它都能在你的頻道冒充機器人發言,所以它的待遇比照 API key:真值放 gitignored 的 secrets.env、程式只從環境變數拿、絕不寫進任何會進 git 的檔案。本系列 config.sh 的機敏掃描 SECRET_PAT,第一條 pattern 就是 discord\.com/api/webhooks——d04、d09 被隔離重寫的稿子,守衛抓的就是這種字串。

步驟二:一行 curl 到一支腳本

先用裸 curl 開光,感受一下「機器人說話」可以有多便宜:

# -w "%{http_code}" 只印 HTTP 狀態碼,回應內容本來就是空的
HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
  "https://discord.com/api/webhooks/<ID>/<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"content":"鐵人賽系統上線測試"}')
echo "HTTP $HTTP"
# ---- 預期輸出 ----
# HTTP 204

小小小測驗:你知道 Discord webhook 送出成功時,回傳的不是 200 而是 204 No Content 嗎?Discord 的習慣是「成功但沒有內容可回」,所以後面所有驗收判準都寫成「2xx 就算送達」,死等 200 會把成功誤判成失敗。

裸 curl 能用,但每天看你會想要:成功綠色、警告黃色、出事紅色;有標題有排版;還要發進指定的 thread 不洗亂頻道。把這些包成一支全系列共用的腳本(節選):

# ~/ai/automations/discord-notify.sh(節選)
# 用法:discord-notify.sh -t "標題" -s ok|warn|error "訊息"

WEBHOOK="${WEBHOOK:-https://discord.com/api/webhooks/<ID>/<TOKEN>}"
# 發進 thread:在 URL 加 ?thread_id=(該 thread 須在 webhook 所屬頻道下)
if [ -n "$THREAD_ID" ]; then WEBHOOK="${WEBHOOK}?thread_id=${THREAD_ID}"; fi

# -s 決定 embed 顏色:視覺分級,手機上一瞄顏色就知道事態大小
case "$SEVERITY" in
  ok)    COLOR=3066993 ;;   # 綠:心跳/一切順利
  warn)  COLOR=16098851 ;;  # 黃:有狀況,待會處理
  error) COLOR=15158332 ;;  # 紅:出事了,現在處理
  info|*) COLOR=9807270 ;;  # 灰:純資訊
esac

# payload 交給 jq 產生:--arg 自動處理跳脫,訊息帶引號、換行都不怕
PAYLOAD=$(jq -n --arg t "$TITLE" --arg d "$MSG" --arg f "$HOST" --argjson c "$COLOR" \
  '{embeds:[{title:$t, description:$d, color:$c, footer:{text:$f}, timestamp:(now|todateiso8601)}]}')

# 只信 HTTP 狀態碼:2xx 送達;告警本身失敗也要 exit 1,留下痕跡
HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$WEBHOOK" \
  -H "Content-Type: application/json" -d "$PAYLOAD")
if [ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ]; then exit 0; else exit 1; fi

幾個設計細節:

  • 顏色就是分級:embed 的 color 欄位吃一個整數色號,綠 3066993、黃 16098851、紅 15158332,在手機通知列表裡是一眼可辨的訊號。
  • footer 帶 hostname、timestamp 帶時間:多台機器跑自動化時,才知道這則戰報是誰發的、幾點發的。
  • 告警系統自己也不能啞巴:送不出去就 exit 1,讓 launchd job 的 log 留下痕跡——最尴尬的故障是「出事了但求救訊號本身壞掉」。

發三種等級看看版面:

export WEBHOOK="https://discord.com/api/webhooks/<ID>/<TOKEN>" THREAD_ID="<THREAD_ID>"
~/ai/automations/discord-notify.sh -t "鐵人賽生成" -s ok    "d13 已生成,20:00 自動發佈"
~/ai/automations/discord-notify.sh -t "鐵人賽發文" -s warn  "d13 tag 不完整,缺:opencode"
~/ai/automations/discord-notify.sh -t "鐵人賽發文" -s error "發佈重試 x3 失敗,請人工確認"
# ---- Discord thread 裡 ----
# 🟩 鐵人賽生成:d13 已生成,20:00 自動發佈
# 🟨 鐵人賽發文:d13 tag 不完整,缺:opencode
# 🟥 鐵人賽發文:發佈重試 x3 失敗,請人工確認

步驟三:接進系統——什麼事件該喊、喊哪一級

工具就位,接下來是設計問題:哪些事件值得發訊息?我的分類法很樸素:綠色是心跳、黃色是注意、紅色是求救,一個事件只屬於一級。generate.sh 的接法(節選):

# scripts/generate.sh(節選):secrets.env 的真值由 config.sh 載入,轉成 notify 認得的變數
source "$ROOT/config.sh"
export WEBHOOK="$DISCORD_WEBHOOK" THREAD_ID="$DISCORD_THREAD_ID"
NOTIFY="$HOME/ai/automations/discord-notify.sh"

# ok:心跳。生成成功,報平安
"$NOTIFY" -t "鐵人賽生成" -s ok "d${DD}〈${TITLE}〉已生成,20:00 自動發佈"
# warn:有狀況但不致命,文案自帶下一步動作
"$NOTIFY" -t "鐵人賽生成" -s warn "d${DD} 5 次生成都未過品質判準,請 20:00 前人工確認"
# error:求救。動詞要急
"$NOTIFY" -t "鐵人賽生成" -s error "d${DD} 生成與備稿全數失敗,斷更危險!請立即手動處理"

publish.sh 更進一步,把「出事」的標準動作封裝成 die()——寫 log、發紅色告警、exit 1 三件事一次做完,全腳本十幾個失敗點都呼叫它:

# scripts/publish.sh(節選):所有致命失敗的統一出口
die() { log "FATAL: $1"; "$NOTIFY" -t "鐵人賽發文" -s error "$1"; exit 1; }
# 用起來像這樣:
[[ -n "$ARTICLE" ]] || die "找不到 d${DD} 的文章檔,今日無法發文"

心跳的價值,要到出事那天才看得出來:每天 19:00、20:00 各一則綠色,是系統的脈搏。哪天脈搏停了——綠色沒來、紅色也沒來——沉默本身就是警報,代表排程層可能整個沒醒(launchd 沒觸發、機器沒開機)。這種「連失敗訊息都沒有」的狀態,只有心跳抓得到。另外兩個實戰習慣:告警文案一定寫下下一步動作(「請 20:00 前人工確認」,而不是只喊「失敗了」);把鐵人賽 thread 的通知設成「所有訊息」,沙發上滑手機就能掌握戰況。順帶一提,d08 那個用 plugin 內 fetch 直發 webhook 的做法是 JS 這層的事,bash 腳本這層就交給今天的共用腳本,兩層各自把守。

常見問題 / 踩坑記錄

  • Q:webhook URL 一不小心貼進截圖、log 或文章怎麼辦?
    A:視同洩漏,立刻到頻道整合設定刪掉重生一條(一分鐘的事,別賭運氣)。預防端靠雙保險:真值只住 gitignored 的 secrets.env,加上 SECRET_PAT 機敏掃描——任何要對外發佈的文件先掃一輪,命中就隔離。

  • Q:訊息裡有引號或換行,就發不出去(HTTP 400)?
    A:別手組 JSON 字串。payload 一律用 jq -n --arg 產生,跳脫自動處理。那個 400 就是當年手組字串、被自己訊息裡的引號炸出來的教訓。

  • Q:排程跑的時候報 jq: command not found,手動跑卻好好的?
    A:D11 的老朋友——launchd 不載入 shell rc,Homebrew 的 /opt/homebrew/bin 不在預設 PATH。解法同款:腳本開頭手動掛 PATH,跟 nvm 的 node 一起處理。

  • Q:告警炸個不停,頻道變成訊息瀑布?
    A:分級本來就是為了克制。灰色 info 級能用 log 取代的就不發;黃色集中在專用 thread;紅色一天不該超過個位數——超過代表系統病了,該修的是病,不是關通知。

小結

  • webhook 是「只准 POST 的發言入口」,URL 本身就是密碼:住 secrets.env、走環境變數、機敏掃描當守衛
  • discord-notify.sh 做四件事:顏色分級、jq 產生 payload、thread 定向、只信 HTTP 2xx(Discord 成功回 204)
  • 事件 → 等級對應:綠色心跳報平安、黃色注意留緩衝、紅色求救帶動作;告警文案必寫下一步
  • 心跳抓的是「連失敗都沒有」的沉默故障:綠色沒來的那天,就是排程層出事的那天

明日預告

下一篇我們要介紹「第二週回顧:排程系統拼圖完成」,W2 從 hooks、launchd、PATH 地雷、錯誤處理一路走到今天的告警系統,拼圖終於成形——明天把整個自動化核心攤開來總盤點,附系統現況,敬請期待!

參考資料:

有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D


上一篇
12 錯誤處理:Agent 失敗了誰來擦屁股
下一篇
14 第二週回顧:排程系統拼圖完成
系列文
自我耍廢組:全自動化の鐵人 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言