專案筆記

claude_linebot 架構解析:webhook 管線、回覆經濟學與兩段式附件

2026-08-09 #Claude Code#LINE#Python#asyncio#架構設計

上一篇講了 claude_linebot 的動機和 LINE 的三大平台難題;這篇拆開實作:一則 LINE 訊息的完整旅程、reply/push 的成本決策、以及「圖片沒有 caption」逼出來的兩段式附件設計。

檔案結構

1
2
3
4
5
6
7
8
9
lineBot/
├── bot.py ← aiohttp webhook server + 指令 + 訊息流程
├── line_client.py ← LINE API 包裝:reply/push 路由、分段、打字動畫
├── claude_runner.py ← async wrapper 包 `claude --print`(與 remotetools 幾乎相同)
├── attachment_store.py ← 圖片/檔案落地 + 待處理清單
├── session_store.py ← session_id 持久化
├── usage_tracker.py ← RPM + 每日上限 + 成本累計
├── keep_awake.py ← macOS caffeinate / Windows SetThreadExecutionState
└── state/ ← sessions.json / usage.json / bot.log / uploads/

一則訊息的十道關卡

1
2
3
4
webhook POST ─▶ ① 驗 X-Line-Signature ─▶ ② webhookEventId 去重 ─▶ ③ 立刻回 200
─▶ ④ 解析 source 取兩種 key ─▶ ⑤ 白名單 ─▶ ⑥ lock(佔用即拒絕)
─▶ ⑦ 用量原子預留 ─▶ ⑧ spawn claude subprocess
─▶ ⑨ 每 55 秒續 loading animation ─▶ ⑩ 存 session、依 reply/push 規則送出

幾個關卡值得展開:

① 驗簽:對 request body 算 HMAC-SHA256(key 是 channel secret)、base64 後與 X-Line-Signature header 比對,不過直接 400。這是 webhook 模式的必要防線——你的 endpoint 在公網上,任何人都打得到。順帶一提,這也讓離線測試變得容易:自己簽一個假 webhook POST 到 127.0.0.1:8000/callback,不需要真的 LINE 就能走完整條管線。

③ 立刻回 200:LINE 要求 webhook 快速回應,逾時會重送(所以才需要②的去重)。實際處理丟進背景 asyncio task,webhook handler 永遠秒回。

⑥ lock 佔用即拒絕:這是跟 remotetools 的一個刻意分歧。Telegram/Discord 版的 per-對話鎖是排隊——前一個請求跑完,下一個接著跑。LINE 版改成直接拒絕:排隊期間 reply token 早就過期了,等排到的時候只能用付費 push 回覆,等於白花額度。與其默默扣錢,不如立刻告訴你「上一個還在跑」。

三組 key,各管各的

跟 Discord 版同一套拆法:

用途 key 理由
白名單 source.userId 「誰能驅動 bot」跟在哪聊無關
用量上限 source.userId 配額保護是 per-person
session / lock / /more 暫存 session_key 每段對話獨立脈絡

session_key 的規則:1:1 用 userId、群組用 groupId、聊天室用 roomId。所以你私聊問到一半的東西,不會污染群組裡的對話。

群組還有一個順序陷阱:訊息要判斷「是不是對 bot 說的」(有沒有 claude 前綴),做白名單檢查。順序反過來的話,群組成員正常聊天會被 bot 回「未授權」洗版——這是真實發生過才寫進文件的教訓。

回覆經濟學:ReplyContext

LINE 的計費模型:reply 免費但 token 一次性、約一分鐘失效;push 扣官方帳號的免費月額度。Claude 跑任務動輒數分鐘,所以「怎麼送回覆」需要一套決策邏輯,集中在 line_client.pyReplyContext

  • 50 秒內(REPLY_TOKEN_BUDGET_SECONDS)跑完 → 用 reply,免費
  • 超過 → 用 push,扣一則額度,log 會誠實印出 used a PUSH message
  • reply 失敗(token 被判定失效)→ 自動 fallback 到 push,代價跟直接 push 一樣,所以 budget 可以放心用滿
  • 執行中的「還在跑」提示用 loading animation——獨立 API,不算訊息、不扣額度(但只有 1:1 有效,群組裡 Claude 跑的時候畫面完全沒動靜)
  • 回覆太長只送前 5 則(MAX_MESSAGE_CHUNKS),其餘存在記憶體等 /more——/more 是新訊息、帶新 reply token,免費

整套邏輯的優先序就一句話:能免費就免費。專案的 CLAUDE.md 特別警告未來的維護者(包括 AI):不要為了程式簡潔一律改用 push。

兩段式附件:平台限制逼出來的好設計

LINE 的圖片訊息沒有 caption——圖片和說明文字是兩則獨立的 webhook 事件。收到圖就立刻跑 Claude 沒有意義(還不知道你要問什麼),所以:

1
2
你:[一張截圖]            ← bot 安靜收下,存進 state/uploads/,排進 pending
你:這個錯誤是什麼意思? ← 文字 prompt 帶著圖的絕對路徑一起送進 Claude

可以連傳五張再一次提問,全部一起送。這裡藏著一個併發細節:每個 webhook 事件各自一個 task,五張圖是併發下載的;如果你手速快、圖剛送出就送問題,文字可能比下載先完成——所以 prompt 在取用附件前會等在途下載歸零(上限 8 秒)。拿掉這個等待,bug 只會在慢網路下出現,最難查的那種。

追問計時器與 reply token 的精算

收到圖時 bot 刻意不回「已收到」。原因還是 token 經濟學:reply token 一次性,拿去 ack 就沒了,之後想追問只能走付費 push。做法是把 token 留在一個計時器裡:

1
2
3
傳圖 ──30s──▶ 追問「請問你想問什麼?」──30s──▶ 靜默丟棄 pending
│ │
└── 中途傳文字就取消計時器,圖照樣一起送 ──┘

追問本身就兼任 ack,而且用的是留下來的免費 token。追問後仍無下文就靜默丟棄(不發通知——那時 token 已用掉,通知會是一則付費 push)。磁碟上的檔案另由背景任務清理(預設保留 3 天,每 6 小時掃一次)。

一個真實的 bug:64KB 的 readline 上限

claude_runner.py 本來跟 remotetools 逐字元相同,但圖片支援逼出了一個分叉:Claude 讀圖時,stream-json 會把整張圖的 base64 塞進同一行 JSON,遠超 asyncio readline 預設的 64KB 上限——LimitOverrunError 拋出、整個請求靜默崩潰,使用者只看到「沒反應」。修法是 create_subprocess_exec 加上 limit=64MB,並把該例外收成明確的錯誤訊息。remotetools 沒有圖片支援所以永遠碰不到這個 bug——同一份程式碼,新的使用情境挖出了潛伏的地雷。

其他移植差異速覽

主題 Telegram / Discord 版 LINE 版
傳輸 long polling aiohttp webhook server
進度 每 2 秒編輯訊息 loading animation(每 55 秒續一次)
併發 排隊等 lock 佔用即拒絕
長訊息 全部切片送出 送 5 則,其餘等 /more
防睡眠 Win32 API macOS caffeinate(保留 Windows 分支)

下一篇實戰:從 LINE Developers Console 到 Cloudflare Tunnel,把整套系統部署起來

Creative Commons 姓名標示 非商業性

本文採用 CC BY-NC 4.0 授權

歡迎轉載與引用,請標明出處(claude_linebot 架構解析:webhook 管線、回覆經濟學與兩段式附件 — EmptyWu);禁止用於商業用途。

商業使用或合作提案,歡迎來信洽談:[email protected]

留言
分享

留言