! 本篇文章將會介紹 全管線整合:19:00 生成 → 20:00 發文,期望大家都能看懂兩個排程 job 怎麼靠狀態檔接成一條龍 :D
W4 開張,先還昨天盤點出的第一筆欠帳。D3 誕生的 opencode run 生成器和 D19、D20 組裝的發文腳本,出生日相差十六天,過去三週它們的「合作」其實只是剛好被同一張班表排在一起:一個 19:00 上班、一個 20:00 接手,中間靠檔案系統心電感應。文章倒是天天準時上架(到昨天為止 20 篇全數自動發出),但默契不等於合約——今天把交接明文化:那一小時的緩衝到底在緩衝什麼、兩個 job 之間傳遞了哪些狀態檔、每個環節掛掉時的失敗半徑有多大。而且這篇文章本身就是最好的驗證樣本:你正在讀的這一段,就是那條管線今天 19:00 的產物,一小時後它會被同一套流程送出去。
讀完這篇你會學到:
com.benben.ironman.generate / publish / republish
scripts/generate.sh(W1)與 scripts/publish.sh(W3),都在 /Users/benben/ai/automations/ironman/scripts/
# 進到系列 repo
cd /Users/benben/ai/automations/ironman
# 確認排程三兄弟都在線
launchctl list | grep ironman
# 三班制的時刻表(改了要重跑 scripts/install-launchd.sh 重新產生 plist)
grep -E "GENERATE_TIME|PUBLISH_TIME|REPUBLISH_TIME" config.sh
# GENERATE_TIME="19:00"
# PUBLISH_TIME="20:00"
# REPUBLISH_TIME="20:15"
先攤開一天的時間軸:
# 19:00 ── generate.sh:opencode run 生成 + 品質判準 + 重試
# └→ 成功後 Discord 回報「d22 已生成,20:00 自動發佈」
# 20:00 ── publish.sh:找文章 → 掃描 → agent-browser 填表發佈
# 20:15 ── republish job:重跑 publish.sh,flag 在就跳過
19:00 和 20:00 之間這一小時不是隨便抓的,它同時扛三件事:
ExitTimeOut 1800 兜底,就算 agent 鬧起來,launchd 半小時後強制收工,絕不賴到發文班次之後。兩個 job 不共用任何變數,交接全靠 state/ 與 articles/ 裡的檔案。一天一枚,攤開來看:
# 以昨天 d21 為例,管線的四份憑證:
articles/2026-10-01-d21.md # ① 成品:generate 寫,publish 讀
state/body-d21.md # ② 內文暫存:publish 剝掉 frontmatter 的進料
state/published-20261001.flag # ③ 冪等憑證:0 byte,touch 即「已發佈」
state/published.log # ④ 帳本:日期|天數|URL,一行一發
①是兩個 job 之間唯一的正式合約:frontmatter 的 title 必須是「DD+大綱標題」——generate.sh 的品質判準出廠前逐字比對 outline,publish.sh 用 awk 照同樣格式解析。上游怎麼寫、下游就怎麼讀,介面一天都沒變過,這是 20 天零斷更的底層原因。②是暫存不是存檔:publish 每次都從①重新 awk 出內文(昨晚是 9,320 bytes),餵給 CodeMirror 的 setValue 填進編輯器。③只有 0 byte,卻是 20:15 補發 job 的唯一判準;④則是帳本,30 天成績單的原始數據全在裡面。
小小小測驗:你知道 20:15 的「補發保險」和 20:00 的正式班次,跑的是同一支 publish.sh 嗎?
答案:是。republish 的 plist 裡沒有第二套邏輯,就是再執行一次同一個腳本,全靠一枚 0 byte 的 flag 分辨「發過了」與「還沒發」。同一支腳本+冪等設計,保險本身就不需要維護第二份程式。
設計講完了,用昨天的 log 走一遍完整流程,時間戳全部真實:
# generate-20261001.log 節錄:生成 5 分 36 秒
[2026-10-01 19:00:05] 開始生成 d21:第三週回顧:Agent 已經會自己發文了
[2026-10-01 19:05:41] 成功:/Users/benben/ai/automations/ironman/articles/2026-10-01-d21.md(1838 個中文字)
# publish-20261001.log 節錄:從起床到上架 21 秒
[2026-10-01 20:00:05] d21 發文來源:/Users/benben/ai/automations/ironman/articles/2026-10-01-d21.md
[2026-10-01 20:00:13] 鐵人草稿確認(article_type=ironman)
[2026-10-01 20:00:20] 點擊發佈(第 1/3 次)
[2026-10-01 20:00:26] 發佈成功:https://ithelp.ithome.com.tw/articles/10419752
[2026-10-01 20:15:05] 今日已發佈過(flag 存在),跳過
生成 5 分 36 秒、發佈 21 秒、補發 0 秒(跳過也是一種成功)。這條管線還藏著一道雙保險:同一篇內文會被 secret_scan 掃兩次——19:00 出廠檢驗,命中就丟進 state/quarantine-* 隔離重試;20:00 送出前再攔一次,命中直接 die 絕不出手。state/ 裡 d04、d09 共六個隔離檔,時間戳全落在 19:04–19:25 的生成時段,就是第一道保險的戰利品。
管線整合的最後一塊拼圖是誠實盤點:每個環節壞掉,傷害範圍到哪裡?
die 收工 + error 告警,寧可不發也不發錯至於為什麼用檔案系統當資料庫、不裝個 queue 或 DB?三個理由:ls 就能觀察、改檔就能干預、零依賴不會自己壞。代價是人工介入後要記得補狀態——這是踩坑記錄第一條的血淚。
touch——你在網頁上按發佈,系統完全不知情,20:15 一看 flag 不在,就照流程再發一篇一模一樣的。正確姿勢二選一:手動重跑 ./scripts/publish.sh(成功後它會自己補 flag),或自己補一枚 touch state/published-$(date +%Y%m%d).flag。人工搶救完,記得補上班卡。articles/ 來源 awk 剝出來的暫存,20:00 一到就被覆寫。單一事實來源永遠是 articles/*.md——狀態檔設計第一課:分清楚「事實」與「加工品」。ExitTimeOut 1800 兜底保證不溢出到下一班下一篇我們要介紹「備援設計:斷更即淘汰的保命機制」,把今天的失敗半徑分析升級成重試鏈、通用備稿與告警升級的完備備援——目前系統只有單層備稿,明天把它補齊成真正的保命機制,敬請期待!
參考資料:
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D