上一次,我把 LINE Cafe Bot 拉進群組,讓大家可以一起加入候選咖啡廳、投票,最後選出最想去的那一間。
原本以為店選好之後,聚會就已經成功一半。
但真正約過朋友就會知道,群組裡接下來往往還有另一關:
「那禮拜六下午呢?」
「我三點後才可以。」
「禮拜日會不會比較好?」
「我都可以,你們決定~」
所以店家雖然選出來了,行程卻可能又停在聊天訊息裡。
這次我想把流程再往後接一步:
既然 Bot 已經知道大家選了哪間咖啡廳,可不可以接著幫大家把時間也定下來?
於是我和 Codex 一起完成了 line-cafe-group-scheduler。
這次我沒有只給 Codex 一句「做一個時間投票」,就請它從空白開始。我請它先讀上一版的 line-cafe-group-planner,弄清楚選店結果存在哪裡、LINE 的 postback 怎麼分流,以及群組和成員分別用什麼 ID 辨識。
然後我說明真正的使用情境:不是某個人建好一張表請大家填,而是群組裡的人會一個接一個提時間,也可能隨時改變主意。
Codex 把這段口語需求整理成幾個必須共用的資料:
groupId → 投票屬於哪個群組
scheduleId → 按鈕屬於哪一輪時間投票
creatorId → 誰能截止與處理平手
userId → 是誰提時間、投票或改票
這是 Codex 在這次最重要的角色:我說「大家要一起選」,它幫我追問程式到底需要記得哪些事。
這個新功能不是另外做一個獨立的時間投票。
當咖啡廳投票出現單一最高票後,結果訊息會多一個按鈕:
接著一起選時間
按下後,Bot 會把剛才選出的咖啡廳直接帶進新流程,不用再輸入店名,也不用重新搜尋。
完整過程變成:
群組選出咖啡廳
↓
按「接著一起選時間」
↓
成員提出候選時間
↓
大家投票,也可以改票
↓
發起人截止投票
↓
加入 Calendar,並等待行程提醒
我很喜歡這種「接著做」的感覺。使用者不必理解 Bot 背後其實分成選店和選時間兩個功能,只會感覺自己正順著同一件事往下完成。

如果只讓發起人建立所有時間,他還是要先在群組裡問完每個人,再自己整理答案。
這樣只是把工作從聊天室搬到 Bot,並沒有真正變輕鬆。
所以這次的規則是:
成員按「提出候選時間」後,LINE 會直接打開 Datetime Picker。他不用自己輸入「這個禮拜六下午三點」,也不會出現每個人日期格式都不一樣的問題。
第一個人加入時間後,下一個人可以接著提第二個、第三個,直到找到幾個大家真的有可能配合的選項。

LINE Datetime Picker 會幫使用者打開選擇介面,但真正按下確定後,webhook 傳回的資料分成兩部分。
一部分是我放進按鈕的識別資料:
v=1&gs=add&s=schedule_123
它表示這是「新增時間」動作,而且屬於 schedule_123 這一輪。
另一部分則是 LINE 放在 postback.params.datetime 的選擇結果:
2026-09-12T15:00
Bot 收到後,還會檢查群組 ID、排程 ID、時區與日期範圍,不是看到一個日期就直接寫進去。
這樣即使聊天室裡同時留著舊卡片,Bot 還是能分辨這個時間究竟屬於哪個群組、哪一輪投票。
這部分也是 Codex 幫我補出的邊界。我原本只在意「選完日期能不能存下來」,它則往後想到:如果一個月後有人回頭按舊卡片呢?因此選擇結果不能只有時間,還必須帶著可以驗證的投票身分。
時間投票沿用了群組選店時很實用的規則:每個人只有一票,但截止前可以改。
例如我原本投了禮拜六下午,後來發現禮拜日比較方便,只要再按一次新選項,Bot 就會把我原本的票移過去。
它不會因為我按了兩次,就讓我多出一票。
資料上也不是每按一次就新增一張票,而是用 userId 記錄每個人目前選的 optionId:
{
"user_A": "option_2",
"user_B": "option_1",
"user_C": "option_2"
}
user_A 改投時,程式只會更新他的值。更新會放在 Firestore transaction 裡,避免多位成員同時操作時,後寫入的人不小心蓋掉剛才的結果。
投票後,Bot 會重新顯示所有候選和當前票數。如果聊天訊息太多,也可以輸入:
查看群組時間
隨時取得最新結果。
所有成員都能提時間、投票和改票,但只有原本的群組選店發起人可以截止。
我想保留一個明確的決定者,不然只要有人覺得票數差不多了,就可能在其他人還沒表態時直接結束。
截止後,會出現兩種結果:
單一最高票 → 直接確認聚會時間
最高票平手 → 由發起人從平手選項中決選
Bot 不會隨機幫大家選一個,因為投票的意義是讓結果反映群組的選擇。遇到平手時,把最後決定權交給原發起人,比假裝 Bot 有一個「最公平答案」更誠實。
程式裡會用三個狀態表示這段流程:
collecting → tie_break → confirmed
└───────────↗
正在收集時間時是 collecting;平手才進入 tie_break;只有選出單一時間後才會成為 confirmed。每個新增、投票或截止動作都會先檢查狀態,所以已經確認的行程不會被舊按鈕改掉。

當店家和時間都確定後,Bot 會整理出一則完整結果:
每個人都可以自己按「加入 Calendar」,不用再從聊天訊息裡複製店名和時間。
Bot 預設會在聚會前 60 分鐘提醒整個群組。如果大家選的時間已經很近,它也不會安排一個早就過去的提醒,而是改在「現在到聚會之間的一半」送出。
例如 30 分鐘後就要見面,Bot 會在 15 分鐘後提醒,不會因為來不及提前一小時,就完全不做任何事。
提醒不是靠 Bot 一直在背景倒數,而是在時間確認後建立 Cloud Task。Task 到點才呼叫提醒入口,而 Firestore 會記錄 scheduled → sending → sent 狀態。即使過程中需要重試,同一個行程也不會因此對群組連續提醒好幾次。

LINE 群組裡的舊訊息會一直留著。如果上週的時間卡片還可以改到這週的投票,大家就不會知道眼前的結果還能不能相信。
因此每一個時間按鈕都會對應特定的群組和這一輪投票。Bot 會拒絕:
而且當群組已經有一筆還沒到來的確認行程時,Bot 不會讓新投票把它蓋掉。
這些規則平常不太會被注意,但它們決定了一個群組工具用久之後會不會變得很亂。
一開始,我的需求其實只有一句:選完咖啡廳之後,幫大家把時間也定下來。
Codex 幫我往下補齊了很多實際會發生的問題:
我們的分工比較像這樣:
我:提出朋友真正會怎麼約、哪裡讓我覺得卡
Codex:讀懂現有程式,把情境轉成資料、權限和狀態
我:回到 LINE 裡實際點按鈕,確認用起來像不像真實對話
Codex:根據操作結果繼續追 handler、Firestore 和訊息流程
Codex 並沒有為這個功能重寫整隻 Bot。它保留原本的群組選店,從「單一最高票店家」這個已有結果開始,再將新的 schedule store、Datetime Picker action、時間卡片和提醒一層一層接上去。
它也沒有為了省事,在平手時隨機挑一個。當 Codex 把「平手怎麼辦」放回來問我時,我才確定最後決定權應該留給原發起人。這種來回確認,讓最後的規則不是 AI 自己猜的,而是真正符合我想做的產品。
最後,Codex 才把這些規則接回原本的群組選店、LINE Datetime Picker、Google Calendar 與提醒流程。
不過,功能完成後,我真的打開 LINE 操作,還是馬上找到了兩個問題:
這兩個都不是功能完全壞掉,而是「程式做得到,但使用者走不下去」。
下一篇,我會專門整理這兩個問題,以及我為什麼開始覺得:一個好的 Bot 不只要回答正確,還要把下一步留在使用者面前。
GitHub:
https://github.com/zonawang/line-cafe-group-scheduler
LINE Messaging API — Datetime picker action:
https://developers.line.biz/en/reference/messaging-api/#datetime-picker-action
LINE Messaging API — Group chats:
https://developers.line.biz/en/docs/messaging-api/group-chats/
LINE Messaging API — Quick reply:
https://developers.line.biz/en/docs/messaging-api/using-quick-reply/
Google Cloud Tasks — Create HTTP target tasks:
https://cloud.google.com/tasks/docs/creating-http-target-tasks
Cloud Firestore — Transactions:
https://firebase.google.com/docs/firestore/manage-data/transactions