! 本篇文章將會介紹 備援設計:斷更即淘汰的保命機制,期望大家都能讓自動化系統在沒有你的時候依然撐得住 :D
鐵人賽的殘酷規則大家都懂:斷更即淘汰。我這 30 天的賭注,是押在一套「我睡著時它也要照常出貨」的自動化系統上。所以對這套系統來說,最可怕的從來不是文章寫得不好——是 20:00 的發文 job 醒來時,發現今天根本沒有文章。
昨天(D22)把 19:00 生成 → 20:00 發文接成一條龍之後,這條管線最後一塊拼圖就是備援。今天來攤開三層保命機制:重試鏈、通用備稿、告警升級。這三層不是理論,是 generate.sh 和 publish.sh 裡真實在跑的程式碼。
讀完這篇你會學到:
延續 D22 的管線,你需要:
scripts/generate.sh(19:00 生成)與 scripts/publish.sh(20:00 發文)scripts/install-launchd.sh 會裝好三個 launchd job:generate、publish,還有一個今天的主角 republishfallback/ 目錄目前我的 fallback 目錄長這樣:
$ ls fallback/
ff1-when-not-to-automate.md # 先別急著自動化(主題獨立、不過時)
ff2-cli-coworker.md # CLI 當同事的心法
ff3-failure-modes.md # 自動化的失敗模式(正好今天用得上 XD)
最直覺的備援是「失敗就重試」,但重試有兩個坑:重試了垃圾和硬碰硬。
generate.sh 的重試迴圈,每次跑完 opencode run 都會過一組品質判準,沒過就重來:
# 判準:檔案存在 >1500 bytes、中文字數 ≥1500、標題與 outline 一致、無機敏內容
MAX_ATTEMPTS=5
for (( attempt=1; attempt<=MAX_ATTEMPTS; attempt++ )); do
(cd "$ROOT" && opencode run --auto --title "ironman-d${DD}-gen" "$PROMPT") >>"$LOG" 2>&1
R=""
[[ -s "$ARTICLE" ]] || R="檔案不存在"
# ...bytes / CJK 字數 / 標題一致 逐項檢查,哪關沒過記在哪個變數
if [[ -z "$R" ]]; then
if secret_scan "$ARTICLE"; then
# 機敏掃描命中:隔離到 state/,絕不外流,然後重試
mv "$ARTICLE" "$ROOT/state/quarantine-d${DD}-$(date +%H%M%S).md"
continue
fi
log "成功:${ARTICLE}"; exit 0
fi
# backoff:暫時性失敗(網路逾時)別用立刻重打硬碰硬
(( attempt < MAX_ATTEMPTS )) && sleep 30
done
三個設計重點:
CJK(823字)不足、標題不符[...]。publish.sh 也有自己的重試鏈。D1 的血淚教訓:iThelp 尖峰時段按「發佈」會靜默無反應——按鈕點了、頁面不動、沒有報錯。所以重試的判準不是「點擊成功」,而是輪詢 URL:
# 發佈成功特徵:URL 變成 /articles/10xxxx 且不是 /draft
for attempt in 1 2 3; do
# 用 JS 直接触發發佈鈕,輪詢 6 次、每次 5 秒看 URL 有沒有變
"${AB[@]}" eval 'document.querySelector(".save-group__dropdown-btn--publish").click()'
for i in $(seq 1 6); do
sleep 5
AFTER=$("${AB[@]}" get url 2>/dev/null)
[[ "$AFTER" =~ /articles/10[0-9]+$ ]] && { PUBLISHED=1; break; }
done
[[ -n "$PUBLISHED" ]] && break
sleep 20 # 沒成功就等 20 秒再點一次
done
以「系統的實際狀態」當驗收標準,而不是「指令有沒有跑完」——這是重試鏈的靈魂。
重試 5 次全軍覆沒怎麼辦?這時輪到 fallback/ 目錄上場。這幾篇備稿是提前寫好的「通用型」文章:主題獨立於賽程進度、不引用特定天數的內容、什麼時候發都不違和。
generate.sh 的備稿救援邏輯:
# 只准在「根本沒有文章」時啟用;已存在但品質不足的文章保留現狀交人工
if [[ -s "$ARTICLE" ]] && (( $(wc -c <"$ARTICLE" | tr -d ' ') > 1500 )); then
log "文章存在但未過判準——保留現狀不覆蓋,請人工檢查"
exit 1
fi
for fb in "$ROOT"/fallback/*.md; do
flag="$ROOT/state/$(basename "$fb").used"
[[ -e "$flag" ]] && continue # 這篇備稿用過了,換下一篇
cp "$fb" "$ARTICLE"
sed -i '' "s/^date:.*/date: ${TODAY}/" "$ARTICLE" # 修正 frontmatter 日期
touch "$flag" # 記帳:這篇已消耗
exit 0
done
兩個容易被忽略的細節:
.used flag 記帳。備稿是消耗品,同一篇不能發兩次(發兩次一樣的內容在鐵人賽也算斷更)。三篇備稿 = 三天的保命額度,用完就真的要靠人工了。小小小測驗:你知道為什麼備稿不准在「文章品質不足」時啟用嗎?答案在踩坑記錄——我曾經為此差點翻車。
前兩層都是機器自救,第三層承認一個事實:總有機器救不了的情形。這時目標從「自動修好」降級成「讓我知道,而且盡快」。
告警走 Discord webhook,分三級:
# NOTIFY=$HOME/ai/automations/discord-notify.sh,認 -s 參數:ok(綠)/ warn(黃)/ error(紅)
"$NOTIFY" -t "鐵人賽生成" -s ok "d23〈備援設計〉已生成,20:00 自動發佈" # 一切正常
"$NOTIFY" -t "鐵人賽生成" -s warn "生成失敗 x5,已改用備稿,請 20:00 前人工檢查" # 自救成功但要複查
"$NOTIFY" -t "鐵人賽生成" -s error "生成與備稿全數失敗,斷更危險!請立即手動處理" # 最後一級:叫人
升級策略的重點:warn 和 error 的差別是「要不要放下手邊的事」。備稿啟用是 warn——系統還在正軌上,但我得在 20:00 前瞄一眼備稿內容;全數失敗是 error——手動發文倒數計時開始。
最後一道保險是排程層的冗餘。install-launchd.sh 裝了第三個 job:20:15 的 republish,它只是重跑 publish.sh:
# 補發保險:重跑 publish.sh 本身;冪等由 state/published-*.flag 保證
make_plist republish publish.sh "$REPUB_H" "$REPUB_M" > .../com.benben.ironman.republish.plist
20:00 的 publish 如果因為任何原因(筆電睡眠、網路斷線、iThelp 抽風)沒成功,20:15 會再來一次。防重複發文的關鍵是 publish.sh 開頭的冪等守衛:發佈成功就會 touch state/published-YYYYMMDD.flag,flag 存在就直接跳過。所以重跑是安全的——這就是 D12 講過的冪等性在實戰的樣子。
Q:備稿為什麼不准拿來覆蓋「品質不足」的文章?
A:2026-09-12(d02)的教訓。當天生成沒過判準,舊版邏輯直接用備稿覆蓋——結果備稿的標題和「明日預告」都對不上當日大綱,差點把一篇內容錯位的文章發出去。現在的規則是:備稿只在根本沒有文章時啟用;品質不足的文章保留現狀,發 warn 告警交人工判斷。寧可慢一步,不要發錯內容。
Q:重試之間為什麼要 sleep 30?一口氣連打 5 次不是更快?
A:如果失敗是暫時性的(API 逾時、網路抖動),連打只是把 5 次機會在 1 分鐘內燒光,而且每次都撞同一堵牆。backoff 30 秒給服務端恢復時間,5 次重試拉開到約 25 分鐘,正好涵蓋大多數暫時性故障的窗口。
Q:發佈重試為什麼用輪詢 URL,不直接看點擊結果?
A:因為 iThelp 尖峰時段的失敗是「靜默的」——點擊指令成功回傳,但頁面根本沒跳轉。唯一可靠的真相是 URL:變成 /articles/10xxxx 才是真的發出去了。教訓:驗收要看系統狀態,不是看指令退出碼。
.used flag 記帳、只准在「完全沒文章」時啟用——品質問題交人工,不要讓備稿掩蓋問題三層備援的心法其實一句話:先讓機器自救,自救不了就讓人類盡快知道。最怕的是中間那種「看起來有在跑、實際上什麼都沒發生」的沉默失敗。
下一篇我們要介紹 進度追蹤:30 天作戰地圖,敬請期待!系統會自己跑之後,下一個問題是「它到底跑得怎麼樣」——用狀態檔與簡單腳本把完賽進度視覺化。
參考資料:
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D