於是使用者在做 live A2A demo、外部 agent 正在填表的同時,我在旁邊「自己驗證自己的 code」,每跑一輪就把對方填的資料洗掉一次。使用者連問三次「為什麼資料一直不見」我才意識到:我把「我的開發測試」當成看不見的背景工作,但它跟使用者的正式環境是同一台 server、同一個 DB。
正解是測試隔離:資料庫路徑、埠號、debug 模式全部吃環境變數,測試跑在獨立的 data/test/ + 另一個埠,跑前後驗證 live 資料庫的修改時間沒變,來證明真的沒碰到。一個小插曲:第一次選的測試埠被機器上別的服務佔了,回的是英文 HTML,害測試的 .json() 解析爆掉——所以測試埠要挑冷門的、並先確認是空的。
更廣的一條:不要在使用者正在用的共用 live 系統上,未經告知就跑會破壞狀態的操作。而且使用者說「我的資料怎麼不見」時,要用證據查清楚再回答——我一開始斷定「是我的 reseed」,查 server log 才發現那個時間點其實是 agent 自己的寫入(檔案修改時間任何寫入都會更新,分不出來源)。先看 log,承認不確定,別急著講一個乾淨的故事。
UnsatisfiedDependencyException: Error creating bean 'channelJobConsumer'
→ parameter 5: 'processExcelBatchHandler'
→ parameter 0: No qualifying bean of type
'com.simpleec.core.repository.ExcelUploadBatchRepository' available
但人類的弱點也明顯:我不會主動去 grep log、不會耐心讀 100 行 Spring 啟動 trace 找 root cause、也記不住 HibernateJpaAutoConfiguration 跟 JpaRepositoriesAutoConfiguration 的差別。我需要 AI 把我的直覺翻譯成具體修法。
真正的 GAP 不在能力,而在溝通的精準度。當我說「retry-job 不該重」,如果 AI 沒抓到我意思是「不該載 Hibernate」,可能就會修錯方向。當 AI 報告「OOM crash 修好了」,如果我沒追問「真的 1 小時都穩定嗎?」,可能就會在 commit 後 production 出包。
驗證標準要明確 — 不要說「跑 90 秒看穩不穩」(會被 Up time 假象騙),要說「看 RestartCount 趨勢、看 Spring 啟動 log 有沒有 Started XxxApplication in N seconds」。
AI 給結論時人要追問「真的嗎」 — 第一次說「OOM 修好了」,我點頭就會直接 commit。改成「我們先觀察 1 小時」這個本能,救了這次 deploy。
「以為修好其實沒」要記成 brain — 我有一套 brain file 系統累積各種坑(訓馬筆記裡有詳細介紹)。這次 Spring autoconfig 的雜食陷阱、Lombok Boolean wrapper getter、grep 假陽性、docker Up time 假象,全部寫進 brain,下次任何 Java 專案都會 trigger 警示。
技術細節 AI 挖、業務 frame 人類訂、修法兩個一起決 — 不要讓 AI 完全自主,也不要把 AI 當 stack overflow 查工具。最大產出來自「兩個視角持續對話」。
總結
這次除錯從 ccbot 沒回應開始,連鎖挖到 channel-job + retry-job 兩個 Spring DI 真因,過程中 AI 被自己的 grep 騙、被 docker Up time 假象騙、被「修一個 service 就 OK」的局部視角騙。最後修對的關鍵是兩件事 — AI 對自己之前的結論保持懷疑願意重 grep,跟我用業務直覺把方向拉回來。
我也越來越相信:AI 除錯不會取代人類,只會讓「業務直覺好的人」更強。如果你只是想找個工具按 enter 就把 production 修好,AI 會給你看似合理但實質繞遠路的修法。如果你能用業務直覺糾正方向,AI 就是你身邊那個記憶力極好、bash 寫得飛快、能同時跑 8 個 background task 的隊友。
Description shift tag (DAY/NIGHT/GRAVEYARD) 自動 prepend
✅
pending notes queue(文字先到、照片後到的情境)
✅
systemd service 開機自動啟動
✅
PTZ ONVIF 控制(暫時不用,因為 pivot 到手機路線)
✅ 備用
每日 SOP:
晚上 8 點我拍一張白板照片
Telegram 分享給 @your_bot_name
Bot 跑 OCR + 寫 iDempiere + 回覆結果(10~20 秒)
看到有讀錯的,打開 iDempiere 手動改
居服員白天在 LINE 的回報,我看到時直接貼到 bot,自動 prepend 到今天的 Description
月底回診時點開 iDempiere 給醫生看趨勢
總結:這篇文章的「二階 vibe coding」
這篇文章本身也是 vibe coding 的產物 —— 我叫 AI 把兩天的工作回溯成一篇技術文,自己只負責審核跟指正敏感資料。這種「AI 幫你跟 AI 協作的過程寫回顧」也算是一種 meta-loop:決策的當下有 AI 幫忙驗證,事後有 AI 幫忙紀錄。我負責方向跟價值判斷,剩下的雜活都給 AI。
兩天下來最重要的一句話:「vibe coding 不是減少思考,是加速思考。」 AI 幫你快速驗證假設、生成原型、寫測試、跑實驗、甚至幫你寫反思文章。但你要知道該往哪個方向走、什麼時候該 pivot、什麼時候該停手。AI 是踏板車,你是駕駛。
大多數 JSON 數值欄位回傳的是字串("id":"1")。但 $wpdb->get_results() 對 int DB 欄位有時會回傳 PHP native int(JSON 裡沒有引號)。解法:所有 nullable String 欄位一律用 ?.toString(),避免 type 'int' is not a subtype of type 'String?'。
def click_text(driver, text, timeout=15):
"""Click flt-semantics element whose innerText contains text."""
end = time.time() + timeout
while time.time() < end:
selectors = [
"flt-semantics[role='button']",
"flt-semantics[role='tab']",
"flt-semantics[flt-tappable]",
]
for sel in selectors:
for el in driver.find_elements(By.CSS_SELECTOR, sel):
t = driver.execute_script("return arguments[0].innerText", el) or ""
if text in t:
driver.execute_script("arguments[0].click()", el)
return
time.sleep(0.5)
raise AssertionError(f"Clickable element '{text}' not found")
九、方法論:Flutter Web Debug 的正確排查流程
當 Selenium 測試與 Flutter Web 出現異常時,依照以下順序排查,可以最快定位問題:
// EventHandler 在 PO_BEFORE_CHANGE 攔截
if ("A".equals(oldStatus) && "I".equals(newStatus))
throw new AdempiereException("使用中字軌不可降回未啟用(台灣稅法)");
if ("C".equals(oldStatus))
throw new AdempiereException("已用完字軌不可再變更狀態");
雙月申報期別計算
// 從發票月份算期別
int period = (month - 1) / 2 + 1; // 1=1-2月, 2=3-4月 ... 6=11-12月
七個真實踩坑紀錄
坑 1:事件主題比較,一個字毀掉一切
這個 bug 讓狀態驗證完全靜默失效了很久。症狀是:程式碼看起來完全正確,但驗證從來不觸發。
// 錯誤 — 永遠不會觸發
if (topic.endsWith("po_before_change")) { ... }
// 正確
if (IEventTopics.PO_BEFORE_CHANGE.equals(topic)) { ... }
解法:對這類「系統可能已存在」的標準欄位,PackOut.xml 使用固定的 placeholder UUID。PackIn 遇到重複就 UPDATE 而不是 INSERT。升版 UUID 策略:已存在的 field → 保留原 UUID;新增的 field → 才用新的 uuid4。不要為了「整齊」換掉既有 UUID。
坑 7:清除 DB 做全新安裝——FK 刪除順序
當要做「完整卸載 → 清 DB → 重裝」的驗證時,刪資料的順序錯了好幾次。
正確順序:AD_Field → AD_Tab → AD_Window,每一步都有 FK 指向下一個。特別注意:AD_Preference 和 AD_Menu 也會 FK 到 AD_Window,忘記刪這兩個就卡住。另外:ad_package_imp 記錄了 2Pack 安裝歷史,不清掉的話 Incremental2PackActivator 判定「已安裝過同版本」就跳過不執行。
如果重來一次,我們會怎麼做
1. 先建立 PackOut.xml 驗證腳本
每次 2Pack 安裝後自動跑這些 SQL,出問題立刻知道,不用等到 UI 爆炸:
-- SeqNoGrid 是否設定
SELECT tablename, count(*) FILTER (WHERE seqnogrid > 0) ok
FROM ad_table JOIN ad_tab ... JOIN ad_field ...
WHERE tablename LIKE 'TW_%' GROUP BY tablename;
-- _UU 欄位是否 updateable
SELECT columnname, isupdateable FROM ad_column
WHERE tablename LIKE 'TW_%' AND columnname LIKE '%_UU';
-- 確認表存在
SELECT tablename FROM pg_tables WHERE schemaname='adempiere' AND tablename ILIKE 'tw_%';
-- 刪除表
DROP TABLE IF EXISTS adempiere.TW_InvoicePrefix;
DROP TABLE IF EXISTS adempiere.TW_Invoice_Prefix_Map;