! 本篇文章將會介紹 第二週回顧:排程系統拼圖完成,期望大家都能看懂三個 plist 如何把「生成 → 發文 → 補發」排成一張無人值守的班表 :D
鐵人賽賽程過半的前哨站到了。W1 我們把 agent 訓練成會寫文章的員工,W2 則是給它排班表、發哨子、教它跌倒自己爬起來——六天拼上六塊拼圖:exit hooks、launchd vs cron、第一個 plist、環境變數地雷、錯誤處理、Discord 告警。今天照慣例盤點:把班表攤開來看、用 logs/ 裡的真實數字交成績單,還有這一週最戲劇性的兩場事故(一篇被機敏掃描連殺五次、一次發文 job 當場爆炸)。先講結論:13 天零斷更,但其中一天你實際上讀到的是備稿——細節全部寫在下面,證據都在 repo 裡,歡迎查帳。
讀完這篇你會學到:
# 進到系列 repo
cd /Users/benben/ai/automations/ironman
# W2 裝好的三個排程 job(19:00 / 20:00 / 20:15 各一個)
ls ~/Library/LaunchAgents/ | grep ironman
# 目前載入狀態:第一欄減號代表「此刻沒在跑」,日曆任務跑完就退場,不是掛掉
launchctl list | grep ironman
先把這週六篇文章收攏成一張地圖:
bootstrap 讓 plist 立刻生效、kickstart 手動試跑它們合體之後的樣子,就是 scripts/install-launchd.sh 一次產出的三個 plist。班表長這樣:
| 時間 | job | 任務 | 失敗了怎辦 |
|---|---|---|---|
| 19:00 | generate | opencode 生成當日文章 | 內建重試 → 備稿頂班 |
| 20:00 | publish | agent-browser 操作 iThelp 發文 | 點擊重試 3 次 |
| 20:15 | republish | 檢查 flag,沒發出去就重跑 publish | 冪等,不會重複發文 |
plist 裡有兩個容易被忽略但很關鍵的設定(真實片段):
<!-- 逾時上限:超過 30 分鐘直接判死,交給下一班救場 -->
<key>ExitTimeOut</key>
<integer>1800</integer>
<!-- stdout/stderr 全部落檔,事後屍檢全靠這兩個 -->
<key>StandardErrorPath</key>
<string>/Users/benben/ai/automations/ironman/logs/launchd-publish.err</string>
而 d11 的 PATH 地雷,最終解法樸素到不行——在腳本開頭把路徑全部自己 export 一遍(install-launchd.sh 第一行有效程式碼就是它):
# launchd 的環境是白紙,nvm 跟 opencode 都不在預設 PATH 裡
export PATH="$HOME/.opencode/bin:$HOME/.nvm/versions/node/v25.9.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
數字全部出自 logs/generate-*.log 與 state/published.log,沒有一個是編的:
| 天 | 生成耗時 | 嘗試 | 字數 | 備註 |
|---|---|---|---|---|
| 08 | 6 分 06 秒 | 1 | 1,633 | 一次過;當晚 publish 出事,見下方屍檢 |
| 09 | 破功 | 6 稿 | — | 機敏掃描連環攔截,備稿 ff2 頂班 |
| 10 | 5 分 27 秒 | 1 | 1,523 | agent 自我修正「字數不足+誤用簡體字形(虛的簡體寫法)」 |
| 11 | 3 分 44 秒 | 1 | 1,543 | 全週最快,一次過 |
| 12 | 6 分 04 秒 | 1 | 2,048 | 19:13 才開跑(排程遲到 13 分鐘),全系列最長文 |
| 13 | 5 分 28 秒 | 1 | 1,630 | Discord 心跳上線,webhook 回 HTTP 204 |
發文端:state/published.log 六天全數上架,d08 到 d13 的文章 URL 依序是 10413289 → 10416073,每天一張 screenshots/publish-dXX-*.png 截圖存證。表面上歲月靜好,實際上藏了兩場事故。
事故一:d09 被自己的保鑣連殺五次。 d09 主題是 launchd vs cron,照理說跟機敏值八竿子打不著,但 generate 判準裡的機敏掃描(config.sh 的 secret_scan)從 19:04 到 19:25 連續隔離了五稿——state/ 裡 quarantine-d09-*.md 排好排滿。重試預算燒完,腳本啟動備稿 fallback/ff2-cli-coworker.md 頂班上陣,20:00 準時發佈——所以那天讀者拿到的其實是一篇「計畫外」的 CLI 隨筆,正牌的 launchd vs cron 是事後補寫回 articles/2026-09-19-d09.md 的。這不是壞事:防禦工事的意義就在這裡,寧可發備稿也不要斷更或洩密。
事故二:d08 發文 job 當場爆炸。 9 月 18 日 20:00 的 publish 觸發後直接炸掉,logs/launchd-publish.err 留下遺言:
# publish.sh 用了 bash 的 read -a,被非 bash 的 shell 執行直接陣亡
scripts/publish.sh:read:88: bad option: -a
scripts/publish.sh:89: TAGARR[@]: parameter not set
20:14 我手動修好重跑,發佈成功。重頭戲在 60 秒後:20:15:00 republish job 準時起床,檢查當日 flag 發現已存在,安靜退場——publish-20260918.log 最後一行就寫著「今日已發佈過(flag 存在),跳過」。補發保險的第一次實戰,完美守住「不重複發文」的底線。事後 publish.sh 開頭補了 re-exec 防呆(腳本第 9 行註解就寫著「2026-09-11 D1 教訓」),這種「同樣的坑不踩第二次」的紀律,正是 W2 想傳達的事。
小小小測驗:你知道 launchd 的 StartCalendarInterval 遇上電腦睡眠而錯過排程時,會怎麼處理嗎?
答案:只要還在同一天內,喚醒後會補跑一次。d12 那天 19:00 機器正在睡,19:13 醒來後 launchd 把 job 補上了——log 第一行「19:13:03 開始生成」就是活證據。所以排程「遲到」不一定是 bug,先查 log 再罵 launchd :D
用幾個指令就能自己驗收:
# 每天一枚的發佈勳章(flag 檔 = 冪等憑證)
ls state/ | grep published- | wc -l # 目前 12 枚,今晚 20:15 後變 13
# 生成側的體檢表:耗時、嘗試次數、隔離事件全在這
ls logs/generate-*.log | wc -l # 16 份紀錄,從 09-09 煙霧測試到今天
現況一句話總結:生成、發文、補發、告警四件事,機器自己轉了六天,人只負責事後看 log 蓋章。 Discord 那邊每天 20:00 收到心跳(DISCORD_WEBHOOK 的真值住在 secrets.env,設定檔裡只有 https://discord.com/api/webhooks/<ID>/<TOKEN> 這種佔位符)、出事時收到告警。剩下的缺口很明確:發文這步目前還是我的 agent-browser 手感在撐,還沒系統化——這正是 W3 的全部內容。
[A-Za-z0-9+/]{40,} 這類 pattern。短期靠備稿保命,長期解法是讓 prompt 明確要求佔位符寫法(如 <ID>/<TOKEN>),並保留 quarantine 檔做事後複盤——五份屍體擺著,才知道殺手是誰。launchctl list | grep ironman 確認有載入、翻 logs/launchd-*.err 看遺言、確認是不是睡眠錯過(會補跑)。d12 遲到 13 分鐘就是睡眠補跑,不是故障。read -a 是 bash 語法,zsh 沒有(zsh 是 read -A);PATH 也會少一大截。解法雙保險:plist 的 ProgramArguments 寫死 /bin/bash,腳本開頭再放 re-exec 防呆與完整 export PATH。下一篇我們要介紹「agent-browser 入門:給 Agent 一雙手」,W3 正式開張——安裝 agent-browser、open/click/fill/snapshot 四個核心操作,把發文這步從手感煉成系統,敬請期待!
參考資料:
有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D