iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

! 本篇文章將會介紹 備援設計:斷更即淘汰的保命機制,期望大家都能讓自動化系統在沒有你的時候依然撐得住 :D

鐵人賽的殘酷規則大家都懂:斷更即淘汰。我這 30 天的賭注,是押在一套「我睡著時它也要照常出貨」的自動化系統上。所以對這套系統來說,最可怕的從來不是文章寫得不好——是 20:00 的發文 job 醒來時,發現今天根本沒有文章。

昨天(D22)把 19:00 生成 → 20:00 發文接成一條龍之後,這條管線最後一塊拼圖就是備援。今天來攤開三層保命機制:重試鏈、通用備稿、告警升級。這三層不是理論,是 generate.sh 和 publish.sh 裡真實在跑的程式碼。

本篇目標

讀完這篇你會學到:

  • 怎麼設計「帶品質判準」的重試鏈,而不是無腦重打
  • 通用備稿(fallback 文章)的設計,以及它什麼時候不准啟用
  • 告警分級(ok / warn / error)與「20:15 補發」這道最後保險

環境準備

延續 D22 的管線,你需要:

  • scripts/generate.sh(19:00 生成)與 scripts/publish.sh(20:00 發文)
  • scripts/install-launchd.sh 會裝好三個 launchd job:generate、publish,還有一個今天的主角 republish
  • 一個放通用備稿的 fallback/ 目錄

目前我的 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

三個設計重點:

  1. 重試前先驗收。agent 回「我寫好了」不算數,bytes、中文字數、標題一致性、機敏掃描全過才算。這樣重試打掉的是「不合格的產出」,不是白白浪費五次機會。
  2. 逐項診斷進 log。早期版本只會說「失敗」,半夜除錯時完全不知道 agent 到底產出了什麼。現在每個判準沒過都會記下實際數值,例如 CJK(823字)不足、標題不符[...]。
  3. backoff 30 秒。失敗原因如果是 API 暫時性逾時,馬上重打只是再撞一次牆。5 次重試加 backoff 最壞約 25 分鐘,還來得及趕上 20:00 發文。

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 記帳。備稿是消耗品,同一篇不能發兩次(發兩次一樣的內容在鐵人賽也算斷更)。三篇備稿 = 三天的保命額度,用完就真的要靠人工了。
  • 日期用 sed 當場修。備稿的 frontmatter date 是假日期,發文腳本只看檔案不看內容,所以啟用時必須把 date 改成今天。

小小小測驗:你知道為什麼備稿不准在「文章品質不足」時啟用嗎?答案在踩坑記錄——我曾經為此差點翻車。

步驟三:第三層——告警升級與 20:15 補發

前兩層都是機器自救,第三層承認一個事實:總有機器救不了的情形。這時目標從「自動修好」降級成「讓我知道,而且盡快」。

告警走 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 才是真的發出去了。教訓:驗收要看系統狀態,不是看指令退出碼。

小結

  • 重試鏈:重試前先驗收(bytes / 字數 / 標題 / 機敏掃描),配 backoff,驗收看系統狀態而非退出碼
  • 通用備稿:主題獨立、.used flag 記帳、只准在「完全沒文章」時啟用——品質問題交人工,不要讓備稿掩蓋問題
  • 告警升級:ok / warn / error 三級對應三種反應強度,20:15 republish 是排程層的最後保險,冪等 flag 讓重跑安全

三層備援的心法其實一句話:先讓機器自救,自救不了就讓人類盡快知道。最怕的是中間那種「看起來有在跑、實際上什麼都沒發生」的沉默失敗。

明日預告

下一篇我們要介紹 進度追蹤:30 天作戰地圖,敬請期待!系統會自己跑之後,下一個問題是「它到底跑得怎麼樣」——用狀態檔與簡單腳本把完賽進度視覺化。

參考資料:

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


上一篇
22 全管線整合:19:00 生成 → 20:00 發文
系列文
自我耍廢組:全自動化の鐵人 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言