第一階段結束,資料都進來了,也出了第一個畫面,但目前 dashboard 有個問題:它必須在自己的電腦上運行。
而重訓是在健身房記的、體重是在房間量的、跑完步是在路邊看錶的,這些時刻都是用手機在處理,因為不可能隨時帶著筆電。
所以第二階段的第一件事,是找一個隨身的入口。
寫個 App 當然可以,但那是另一個成本的專案,而聊天軟體本來就滿足了幾個關鍵條件:手機上可下載使用、可以打字也可以傳照片,且支援按鈕介面、而且有現成的 Bot API。
選 Telegram 的好處:Telegram 的 Bot API 對個人開發者友善,不需要企業帳號、不需要審核,申請完直接拿到 token 就能開始寫。
Bot 要收到訊息有兩種方式,這是動手前第一個要決定的事。
Webhook:自己提供一個網址給 Telegram,有新訊息時它主動打你的伺服器。優點是即時、省資源;缺點是你必須有一個公開、有 HTTPS 憑證的網址。
Long polling:自己的程式主動去問 Telegram「有新訊息嗎」,沒有就把連線掛著,有了就回。優點是不需要任何對外的網址,在自己電腦上跑就行。
補充說明token 對兩種方式都需要,跟 BotFather 申請完拿到的那串 token 是呼叫 Bot API 的通行證,不管你是主動去 getUpdates,還是被動等 Telegram 打過來,都得帶著它。
webhook 真正多出來的是三樣東西:一個公開的 HTTPS 網址、一次 setWebhook 把網址註冊給 Telegram、以及自己跑一個能接 POST 的 web server。
而且因為那個網址是公開的,任何人都能對它發假訊息,所以 setWebhook 還可以帶一個 secret_token 參數—之後 Telegram 每次打過來都會在 X-Telegram-Bot-Api-Secret-Token 這個 header 帶上它,讓你確認請求真的是 Telegram 發的。long polling 沒有這層顧慮,連線是自己主動發起的。
另外,兩者是互斥的:設了 webhook 之後 getUpdates 就會失敗,要先 deleteWebhook 才能切回 polling。
這個專案選了 long polling,程式碼就一行:
app.run_polling()
理由跟架構有關,而且是刻意的取捨。
一般的 bot 會部署到雲端,24 小時在線。但這個 bot 不行—它需要存取本機的東西。
它會做這些事:讀寫本機 coach/logs/ 底下的訓練紀錄檔、透過藍牙讀家裡的體重機、呼叫本機安裝的 claude CLI。
這三件事全部離不開這台電腦:體重機的藍牙訊號抵達不了雲端機房,訓練紀錄的 markdown 檔也在本機。
既然程式本來就得跑在自己電腦上,webhook 那個「公開網址」的門檻就變成純粹的麻煩—需要買網域架 HTTPS,或是開 ngrok 之類的隧道。而 long polling 完全不需要,插上電就能跑。
不過代價就是:你的電腦要一直開著,bot 才會在線。
因為服務需要跑在自己的設備上,變成一個硬體需求,但對一個自己用的工具來說,還可以接受—剛好桌機每天就都開著。
Bot 註冊了這些指令:
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("new", cmd_new))
app.add_handler(CommandHandler("run", cmd_run)) # Garmin
app.add_handler(CommandHandler("sync", cmd_sync))
app.add_handler(CommandHandler("weight", cmd_weight)) # 體重機
app.add_handler(CommandHandler("workout", cmd_workout)) # Notion
前十天做的那些腳本,在這裡各自變成一個指令:/run 去 Garmin 撈活動、/weight 開藍牙監聽、/workout 爬 Notion 重訓紀錄。
也做了一個小東西,讓使用上更方便—啟動時把指令註冊進 Telegram 的選單:
async def _post_init(app: Application):
await app.bot.set_my_commands([
BotCommand("run", "從 Garmin 同步跑步/活動"),
BotCommand("workout", "從 Notion 同步重訓"),
BotCommand("weight", "讀體重機"),
BotCommand("new", "開新對話、清空記憶"),
])

這樣在輸入框旁邊點一下就有清單,不用記指令怎麼拼,能少動手就少^0^
這是一支能讀寫本機檔案、能呼叫本機 CLI 的 bot,如果任何人都能對它下指令,等於把自己的電腦開放給全世界。
所以每個 handler 的第一行都是同一件事:
if not _authorized(update):
await update.message.reply_text("未授權的使用者。")
return
比對的是 .env 裡設定的 Telegram 數字 ID,沒設等於讓任何人遠端驅動你本機的 claude。
Telegram bot 的 token 一旦洩漏,任何人都能找到你的 bot 並對它下指令,這道檢查非常必要。
到目前為止,這個 bot 只是一組指令的遙控器—/run 去撈資料、/workout 爬紀錄。
但我們想要的不是遙控器,是一個能理解「今天 12K 有點喘」這種對話的東西,明天要處理的就是這一層!