群組選時間功能完成後,我就直接在 LINE 裡開始操作。
這一次,我和 Codex 的開發沒有在程式寫完的那一刻結束。我把 Bot 當成真的群組成員來使用,只要有一個當下不知道怎麼往下走的時刻,就直接用白話把現象交給 Codex。
因此這篇不只是「我發現 bug,Codex 幫我改好」。更接近真實的過程是:我提供 LINE 畫面上看到的症狀,Codex 則從這句描述往後找到真正斷掉的程式路徑。
整個流程看起來很順:選好咖啡廳、加入時間、投票,最後截止。
但當我刻意讓「不是發起人的成員」去按截止時,Bot 雖然正確回答:
只有這次投票的發起人可以截止投票。
卻也就停在這裡了。
這句話沒有錯,權限也確實擋下了不該發生的操作,但群組裡的原發起人看到這則訊息後,卻沒有一個明顯的下一步可以接著做。
這是我這次最有感的發現:
錯誤訊息只說明「你不能做什麼」還不夠,它還應該讓「可以做的人」馬上接手。
在群組功能裡,一個人按下按鈕,回覆卻是給整個群組看的。
這和一對一聊天很不一樣。非發起人的誤觸雖然是個人行為,但 Bot 的下一則訊息會變成大家現在看到的最新狀態。
原本的做法只回覆一行權限提示:
非發起人按截止
↓
Bot 拒絕操作
↓
顯示「只有發起人可以截止」
↓
流程中斷
問題不在於原本的投票資料不見了,而是新訊息沒有把操作入口一起帶回來。原發起人可能要往上翻卡片,或者根本不知道要輸入什麼才能繼續。
技術上,原本所有群組投票錯誤會走進同一個通用處理:
catch (error) {
reply(errorText(error));
}
forbidden 被正確翻譯成「只有發起人可以截止」,但這個 handler 不知道使用者剛才在做什麼,也不會去取得最新投票。
這讓我發現,有些錯誤適合只回文字,有些錯誤則必須保留「發生在哪個動作」的上下文。
我當時只告訴 Codex:「它說只有發起人可以,但發起人沒有下一步。」
Codex 先沿著 postback 進入點往下看,找到 finish 會呼叫截止投票的 function;接著看到非發起人被 store 拋出 forbidden;最後追到 handler 的 catch,發現所有錯誤最後都只被壓成一則純文字。
我看到的:發起人沒有按鈕
Codex 追到的:finish → forbidden → generic errorText
這是我覺得 Codex 很適合參與這種修正的地方。我不必先知道錯在哪個檔案,但要把當下的使用情境說清楚;它再負責把畫面症狀對回實際的程式流程。

現在,當非發起人按下截止時,Bot 不再只回一句話。
它會先說明:
🙋 只有原發起人可以截止投票。
我已重新顯示目前票數,
請原發起人按下方的「截止投票」。
緊接著重新顯示:
流程因此變成:
非發起人按截止
↓
Bot 拒絕操作
↓
重新顯示最新投票與截止按鈕
↓
原發起人直接接手
這個修正同時放進了「咖啡廳投票」和「時間投票」。因為兩邊都有截止權限,也都可能遇到同一種群組斷點。
修正後,handler 會特別辨識「在 finish 動作遇到 forbidden」這個組合。概念上像這樣:
if (action === 'finish' && error.code === 'forbidden') {
const latestVote = await getCurrentVote(groupId);
reply([
ownerRequiredMessage,
...createVoteMessages(latestVote)
]);
}
重點不是繞過權限,finalize 仍然會檢查操作者是不是發起人。這段新處理只會重新讀取最新狀態,然後用同一組產生投票卡片的 function,把票數和截止按鈕再畫一次。

接著我又遇到另一個很直覺的問題。
按下「接著一起選時間」後,我先加入一個日期時間。Bot 正確告訴我:
✅ 已加入候選時間(1/5)
但下方只剩一個選項:
查看並投票
這裡的矛盾很明顯:文字說還可以有 4 個,介面卻像在說「時間已經收集完了」。
當下的我當然還知道自己想測什麼,但真正的群組成員不會知道背後還支援 5 個候選。
如果畫面沒有按鈕,對使用者來說,那個能力幾乎等於不存在。

現在每次加入新時間後,Bot 會同時留下兩個入口:
[提出候選時間] [查看並投票]
因此:
當候選達到 5/5 時,「提出候選時間」才會消失,畫面只保留「查看並投票」。
這樣畫面表達的規則,才和 Bot 真正支援的功能一致。
實作上,Quick Reply 不再是寫死只有一個「查看」按鈕,而是依目前候選數量組出來:
const items = [
...(options.length < 5 ? [createDatetimePicker()] : []),
createViewAndVoteAction()
];
這段判斷很短,卻將介面和原本的上限規則接在一起:還有名額就繼續邀請提案,滿了才把大家導向投票。
這一題也是同樣的協作方式。我沒有說「請修改 createGroupScheduleOptionAddedMessage」,而只是告訴 Codex:「我選了一個時間,怎麼只剩查看並投票?」
Codex 先確認後端真的支援 5 個候選,才去找「新增時間後那則回覆」是在哪裡產生。它發現問題不在 Datetime Picker,也不在 Firestore,而是成功訊息的 Quick Reply 從頭到尾只放了「查看並投票」。
因此我們不是無條件多放一個按鈕,而是讓 Codex 把介面接回已經存在的業務規則:候選數量小於 5 才顯示 Datetime Picker,到達上限就自動收起。

這次的兩個問題,後端其實都有正確處理:
但光是「系統知道」還不夠。
如果介面只說你做錯了,卻沒有把正確動作放回來,那是一條死路。
如果系統允許再新增 4 個時間,畫面卻沒有入口,那則是被藏起來的功能。
所以我這次給自己留下一個很實用的檢查方法:
每當 Bot 回覆完,畫面上還有沒有一個很明顯的下一步?
這個問題對群組 Bot 特別重要,因為前一個人的操作,常常正是下一個人的起點。
這兩個問題都不是我一開始寫需求時想到的。
我是真的在 LINE 群組裡點了按鈕,才發現自己不知道接下來要做什麼。
我把當下看到的情況直接告訴 Codex:
不是發起人按截止後,
雖然有提示,但發起人沒有下一步。
以及:
我選了一個時間後,
怎麼只剩下查看並投票?
Codex 沒有只改那句錯誤文字,而是往前後查看整條互動:是誰觸發、回覆後還留下哪些按鈕,以及下一個應該行動的人是誰。
它還把第一個問題從時間投票往前檢查,發現咖啡廳投票也有相同的截止權限斷點。所以修正不只照著我看到的單一畫面打補丁,而是同時讓選店和選時間使用同一種「拒絕後重新交棒」流程。
第二個問題則反過來,讓 Codex 查看除了成功訊息之外,其他時間投票卡片是不是仍然有「提出候選時間」。這樣才能確定修正後的入口不會在下一則訊息又消失。
最後修的不是一個按鈕的文字,而是兩條斷掉的使用路徑。而我也從這個過程學到,和 Codex 合作時不一定要先猜技術解法;把「我做了什麼、看到什麼、期待下一步是什麼」說完整,往往更有幫助。
如果只看功能清單,這個 Bot 原本已經能做到:
新增多個候選時間
限制發起人才能截止
但第一版介面並沒有讓人順利完成這兩件事。
真正修完後,我對「功能做完了」的定義也有點不一樣:
不是程式裡有這個能力,而是使用者不需要猜,就能一步一步走到結果。
這次加上的只是幾個按鈕和重新顯示的訊息,卻讓群組時間投票從「應該可以用」,變成真的有人能接著用完。
GitHub:
https://github.com/zonawang/line-cafe-group-scheduler
LINE Messaging API — Quick reply:
https://developers.line.biz/en/docs/messaging-api/using-quick-reply/
LINE Messaging API — Postback action:
https://developers.line.biz/en/reference/messaging-api/#postback-action