! 本篇文章將會介紹 iT 邦幫忙發文實戰(下):一鍵全自動發文,期望大家都能打造出一條從 markdown 檔案到正式上架、可以放心交給排程的發文管線 :D
昨天當了一整天偵察兵,把 iThelp 發文頁拆成零件攤在桌上:入口的 article_type 陷阱、SimpleMDE 底下的 CodeMirror 真身、select2 的遊戲規則。今天,組裝時刻——把這些零件連同 W2 練好的防禦工事(重試、截圖、告警)全部裝進一支 scripts/publish.sh。它每天 20:00 被 launchd 叫醒,接手 19:00 generate.sh 產出的文章,一鍵送上 iT 邦幫忙。而這篇你正在讀的文章,等一下就會被這支腳本親手發佈——內容與系統互相印證,正是這個系列的驗收時刻。
讀完這篇你會學到:
publish.sh 的一條龍骨架:冪等守衛、找稿、機敏掃描、填表、驗證、送出、存證、回報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 分流(節錄自 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 與截圖平常沒人看,但出事時是唯一的事實;月底的量化回顧,也得靠它們才有成績單可寫。
--headed 帶真視窗;Turnstile 驗的是瀏覽器指紋,三件套缺一不可——真 Chrome binary(不是 bundled Chromium)、--disable-blink-features=AutomationControlled 拿掉 navigator.webdriver、stealth init script。少一件,存檔或發文就被 422 打槍。touch 當天的 flag,腳本開頭看到 flag 直接 exit 0。自動化系統裡,「重複執行是安全的」必須是設計出來的,不是運氣。read -a 這類 bash 語法直接炸掉。shebang 不保險,腳本第一行加了防呆:非 bash 執行就 re-exec 回 /bin/bash。下一篇我們要介紹「第三週回顧:Agent 已經會自己發文了」,把 W3 從 agent-browser 入門到一鍵發文的整段路做總整理——到此為止,Agent 已經能靠自己把文章送上 iT 邦幫忙,敬請期待!
參考資料:
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D