iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

! 本篇文章將會介紹 iT 邦幫忙發文實戰(下):一鍵全自動發文,期望大家都能打造出一條從 markdown 檔案到正式上架、可以放心交給排程的發文管線 :D

昨天當了一整天偵察兵,把 iThelp 發文頁拆成零件攤在桌上:入口的 article_type 陷阱、SimpleMDE 底下的 CodeMirror 真身、select2 的遊戲規則。今天,組裝時刻——把這些零件連同 W2 練好的防禦工事(重試、截圖、告警)全部裝進一支 scripts/publish.sh。它每天 20:00 被 launchd 叫醒,接手 19:00 generate.sh 產出的文章,一鍵送上 iT 邦幫忙。而這篇你正在讀的文章,等一下就會被這支腳本親手發佈——內容與系統互相印證,正是這個系列的驗收時刻。

本篇目標

讀完這篇你會學到:

  • publish.sh 的一條龍骨架:冪等守衛、找稿、機敏掃描、填表、驗證、送出、存證、回報
  • MODE=dry-run 與 live 的切換設計:同一條流程,只差最後一顆按鈕
  • live 模式的三道關卡:Turnstile 複檢、發佈重試 x3、用 URL 輪詢判定成敗(兌現昨天埋的伏筆)

環境準備

  • d16 綁好的 iThelp 登入 profile、d18 的截圖存證資料夾
  • config.sh 裡一個決定生死的開關:MODE
# config.sh(節錄):發文模式切換
#   dry-run = 走完整發文流程但只按「儲存草稿」,不會真的發出
#   live    = 直接發佈到 iT 邦幫忙
MODE="live"

# 排程外手動重跑發文流程:用環境變數指定天數
IRONMAN_DAY=20 ./scripts/publish.sh
# 注意:今天的 published flag 若已存在,腳本會直接跳過(冪等守衛,見步驟一)

主要內容

步驟一:開跑前的守衛——冪等、找稿、機敏掃描

先看全程地圖,publish.sh 一共做八件事:

launchd 20:00 觸發 publish.sh
├── 1. 冪等守衛:今天已發佈過(flag 存在)→ 直接跳過
├── 2. 算天數 + 找稿:articles/*-d20.md(沒有就告警退出)
├── 3. 機敏掃描:標題與內文過 secret_scan,命中即中止
├── 4. 開發文頁:登入檢查 + article_type 驗明正身
├── 5. 填表:標題 → 內文 → tag,填完驗證
├── 6. MODE 分流:dry-run 存草稿 / live 發佈(重試 x3)
├── 7. 存證:截圖 + log
└── 8. 回報:Discord 通知 + 寫 flag

守衛一:冪等。排程裡除了 20:00 的主發文 job,還有 20:15 的補發保險——重跑同一支腳本。防重複發文靠的不是「小心一點」,是一面旗子:

# 冪等守衛:今天已成功發佈過就跳過(節錄自 publish.sh)
PUB_FLAG="$ROOT/state/published-$(date +%Y%m%d).flag"
if [[ -e "$PUB_FLAG" ]]; then
  log "今日已發佈過(flag 存在),跳過"
  exit 0
fi

發佈成功的當下才 touch 這面旗子——沒發成 flag 就不存在,20:15 的補發 job 自然會接手。守衛二:機敏掃描。生成端掃過一次、發文端送出前再掃一次,雙保險:標題與內文只要命中 webhook 或金鑰的特徵,直接 die 中止,寧可斷更一篇,不可外洩一次。

步驟二:填表主線——昨天的零件全部上場

接著回到昨天的戰場。帶著反偵測三件套開頁,確認沒被踢去登入頁、草稿驗明是 article_type=ironman 之後,依序餵表單:

# agent-browser 啟動參數(節錄自 publish.sh):
# --headed(headless 會被 iThelp 403)+ 真 Chrome + 反自動化偵測 + stealth init script
AB=(agent-browser --headed --executable-path "$CHROME_BIN" \
  --args "$AB_ARGS" --init-script "$STEALTH_INIT" --profile "$BROWSER_PROFILE")

# 填標題:普通 input,fill 就好
"${AB[@]}" fill 'input.post-header__title' "$TITLE"

# 填內文:CodeMirror API 直接 setValue,整篇 markdown 一次進場
BODY_JSON=$(jq -Rs . "$BODY_FILE")
"${AB[@]}" eval "(function(){var cm=document.querySelector('.CodeMirror');
  if(!cm||!cm.CodeMirror){return false}
  cm.CodeMirror.setValue(${BODY_JSON});return cm.CodeMirror.getValue().length})()"
# API 抓不到才 fallback:click 編輯區 + keyboard type(模擬真人打字)

# tag:select2 逐個「搜尋 + Enter」(細節見昨天)
for t in "${TAGARR[@]}"; do
  "${AB[@]}" fill '.select2-search__field' "$t"
  sleep 1                              # 等下拉選項浮出來
  "${AB[@]}" press Enter
done

填完不是拍拍手,是對帳:讀回實際掛上的 tag,缺了就發 Discord 警告但不擋發佈——tag 少一個不值得讓整篇卡關,「警告但放行」正是 d12 防禦工事的分級思想。Turnstile 在這裡也先條件式檢查一次:實測儲存草稿流程根本不渲染 widget,多數時候可以直接跳過等待;但 D1 那天它真的出現過、token 還等不到(screenshots/ 裡留著 turnstile-timeout 的存證),所以程式不能假設它不在,只能條件式應對。

小小小測驗:你知道腳本按完「發表文章」之後,用什麼判斷文章真的發出去了嗎?
答案:不是看按鈕給什麼回饋——昨天說過,送出走的是前端 XHR,表單不會給你任何成功訊息。腳本的判準是輪詢網址:發佈成功會被導向 /articles/10xxxx 的正式文章頁。URL 變了,才算數。

步驟三:MODE 切換——dry-run 與 live 只差最後一顆按鈕

走到這裡,兩個模式的流程完全相同:填表、驗證、截圖,一模一樣。分岔點只有最後那一顆按鈕:

# MODE 分流(節錄自 publish.sh)
if [[ "$MODE" == "live" ]]; then
  # live:展開「儲存草稿」旁的 ▲ 下拉,才看得到「發表文章」
  "${AB[@]}" click 'button.save-group__dropdown-toggle'
  sleep 2
  # ... 接 Turnstile 複檢 + 發佈重試(見下)
else
  # dry-run:只按「儲存草稿」,截圖存證後收工
  "${AB[@]}" click 'button.save-group__btn'
fi

dry-run 是這套系統的安全氣囊:草稿存起來、截圖留下來、Discord 照樣回報,只是不公開。開賽前的草稿測試靠它(state/ 裡還留著 draft-recheck.png 的存證),之後哪天要改腳本、或 iThelp 改版了,也先把 MODE 切回 dry-run 試水溫——流程炸了,炸的是草稿,不是賽程。

live 則要過三道關。第一道,Turnstile 複檢:展開下拉後再查一次 token,沒就緒就用 snapshot 找「verify you are human」的核取方塊,點它、等 token。第二道,點擊發佈:不依賴 snapshot ref,改用 JS 直接對按鈕 click()——下拉可能因上一次失敗而收合,所以每次重試前先用 eval 確保它展開。第三道,也是最重要的:不信單次點擊,輪詢 URL:

# 發佈重試 x3(節錄自 publish.sh,D1 的血淚教訓)
for attempt in 1 2 3; do
  "${AB[@]}" eval 'document.querySelector(".save-group__dropdown-btn--publish").click()'
  for i in $(seq 1 6); do
    sleep 5
    AFTER=$("${AB[@]}" get url)        # 成功特徵:被導向 /articles/10xxxx
    [[ "$AFTER" =~ /articles/10[0-9]+$ ]] && { PUBLISHED=1; break; }
  done
  [[ -n "$PUBLISHED" ]] && break
  sleep 20                             # 沒成功,等 20 秒再來一次
done

為什麼這麼偏執?因為 D1 開賽當晚 20:00,iThelp 正值鐵人賽尖峰,按了發佈它會「靜默無反應」——不報錯、不跳轉,當沒這回事。那晚一路折騰到深夜、靠人工重按才救回來,隔天這段輪詢重試就長出來了。機器要做的從來不只是按按鈕,而是「按了之後,確認世界真的變了」。

步驟四:存證與回報——讓每一發都可稽核

成功之後還有三個動作:截圖存進 screenshots/(檔名帶天數與時間)、touch 冪等 flag、在 state/published.log 記一行 日期|天數|文章網址,最後 Discord 回報。告警是分級的:發佈成功送 ok、tag 缺或 dry-run 送 warn、任何 die 送 error。這些 log 與截圖平常沒人看,但出事時是唯一的事實;月底的量化回顧,也得靠它們才有成績單可寫。

常見問題 / 踩坑記錄

  • Q:按了「發表文章」完全沒反應,也沒有任何錯誤訊息?
    A:iThelp 尖峰時段(20:00 前後大家都在擠發文)會靜默失敗,XHR 又沒回饋,壞了都看不出來。解法是步驟三的組合拳:輪詢 URL 判成敗、沒變就自動重點、重試前重新展開下拉。尖峰時段的單次點擊,約等於擲筊。
  • Q:headless 開發文頁被 403,補了反偵測參數發佈又拋 422?
    A:這是兩層不同的偵測。伺服器端擋 headless,必須 --headed 帶真視窗;Turnstile 驗的是瀏覽器指紋,三件套缺一不可——真 Chrome binary(不是 bundled Chromium)、--disable-blink-features=AutomationControlled 拿掉 navigator.webdriver、stealth init script。少一件,存檔或發文就被 422 打槍。
  • Q:20:15 的補發 job 重跑腳本,會不會發佈兩篇一樣的文?
    A:不會。發佈成功當下就 touch 當天的 flag,腳本開頭看到 flag 直接 exit 0。自動化系統裡,「重複執行是安全的」必須是設計出來的,不是運氣。
  • Q:launchd 觸發時腳本直接語法錯誤,手動跑卻好好的?
    A:D1 的教訓。手動跑是 bash,排程環境卻可能被 zsh 接手,read -a 這類 bash 語法直接炸掉。shebang 不保險,腳本第一行加了防呆:非 bash 執行就 re-exec 回 /bin/bash。

小結

  • 一鍵發文 = 一條龍八件事:守衛(flag 冪等、機敏掃描)→ 填表驗證 → MODE 分流 → 存證回報,每一步都有它存在的理由
  • dry-run 與 live 共用 95% 的流程,只差最後一顆按鈕;dry-run 是自動化的安全氣囊,改版先試水溫
  • 成敗判準不在按鈕回饋而在 URL;不信任單次點擊,輪詢 + 重試 x3 是尖峰時段的保命符
  • 冪等讓補發安全,截圖與 log 讓 20:00 那一發永遠可稽核

明日預告

下一篇我們要介紹「第三週回顧:Agent 已經會自己發文了」,把 W3 從 agent-browser 入門到一鍵發文的整段路做總整理——到此為止,Agent 已經能靠自己把文章送上 iT 邦幫忙,敬請期待!

參考資料:

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


上一篇
19 iT 邦幫忙發文實戰(上):編輯器解析
下一篇
21 第三週回顧:Agent 已經會自己發文了
系列文
自我耍廢組:全自動化の鐵人 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言