我同時幫幾個彼此無關的生意跑付費投放——兩間民宿(cometrue-bnb、super-inn)跟一間餐廳(tonytony)。它們的產品毫無共通點,但跑 Google Ads 的機制幾乎完全一樣:拉搜尋字詞、升級 BROAD 比對、去除重複關鍵字、確認轉換追蹤真的有觸發。所以這批工作主要就是在劃一條線:什麼屬於單一站點,什麼屬於引擎。
關鍵的那條切分線
專案結構直接把界線寫死。站點專屬的東西全放在 sites/<name>/ 底下——一個 site-config.yaml、一份 business-context.md、產出的 ads-changes/*.yaml。而 src/engine/ 跟 scripts/ 裡的所有東西完全不知道任何特定帳號的存在;它只吃一份 config 跟一個 customer ID,然後執行。
當我做轉換追蹤驗證時,這個切分的好處立刻浮現。最初版本把 tonytony 的追蹤怪癖寫死在 context markdown 裡——拿來做一次性稽核還行,當成可重複的檢查就沒用。feat(engine): 轉換路由驗證(CI 自動執行,site-agnostic) 這個 commit 把它抽進 src/engine/conversion-verify.ts。現在驗證邏輯只有一份;每個站點不同的只是 config。
我低估的一點是:轉換追蹤是無聲壞掉的。廣告繼續花錢、點擊持續進來,轉換欄位卻默默讀到零,因為某個重新導向鏈吞掉了 gtag 觸發,或感謝頁的路由改了。你不會收到錯誤,你收到的是慢性失血。所以驗證會走完真正的轉換路由——真實使用者抵達事件的那條路徑——並斷言追蹤像素在終點是可達的。把它放進 CI,意味著路由迴歸會在 workflow run 裡浮現,而不是三週後在花費檢討時才發現。
為什麼一切都變成 YAML diff
d15fba09(BROAD 升級 + keep_one 去重自動化)是清理工作落地的地方,而那裡重要的決定不是去重的啟發法,而是任何東西都不直接改帳號。腳本產出 ads-changes/*.yaml 檔:cleanup-*.yaml、volume-*.yaml、pending-*.yaml。再由獨立的 ads-mutate.ts 去套用它們。
這刻意是一個兩階段提交。產生變更很便宜也可逆;套用變更會花錢、會動到正在運作的帳號。把中間產物做成簽入版控的 YAML 檔,我在任何事情發生前就有一份可讀的 diff,事後也有一份究竟套用了什麼的紀錄。pending- 前綴是人工關卡——那些是還沒被核准的提案。restore super-inn 週報(2026-06-20 Wayne 明確同意) 這個 commit message 本身就是同樣的直覺:破壞性或花錢的動作都要拿到明確的人工 ack,而我把這個 ack 發生過的事實寫下來。
keep_one 去重是不起眼的部分。經過一輪輪關鍵字擴張後,你會在多個廣告群組裡累積同一個字詞的 BROAD、PHRASE、EXACT 版本。天真的修法是刪掉重複;正確的修法是剛好留一個——而留哪個取決於比對類型優先序,以及哪個廣告群組真的有成效。這邏輯值得自動化,正是因為手動做的時候特別容易出錯。
沒人要求但我需要的 dispatch UI
最小的那個 commit 是我的最愛:weekly-report dispatch 改 checkbox UI(單選/多選/全跑)。週報 workflow 跑在多個站點上。原本是全有或全無。當我在 debug 某個站點的報告時,我不想重新產生四份。GitHub Actions 的 workflow_dispatch 支援 type: boolean 輸入,會被渲染成 checkbox,所以我每個站點開一個、再加一個「全部」開關。五行 YAML 改動,消掉了一個反覆出現的煩躁——這正是那種你每天碰 workflow 就會累積的摩擦。
我想標記出來的問題
context markdown 檔(ads-health-context.md、business-context.md)身兼二職,既是文件也是 AI 輔助分析的 prompt 輸入。refactor(ads-health): 兩條通用 context 抽升 engine 那個 commit 是我開始拆解這件事——把真正可重用的分析框架從每個站點的筆記裡抽出來。還沒做完。現在一個 context 檔混了「這個帳號怎麼運作」跟「引擎該怎麼推理」,而這兩者以不同速率漂移。那是下一條要劃的界線。
整體模式——站點設定當資料、引擎當程式碼、變更當可審查的產物、花錢的動作鎖在明確 ack 後面——是唯一讓這個多帳號廣告操作沒有變成一堆我不敢執行的一次性腳本的東西。