部落格留言通知的最後一哩路:D1 沒有 Trigger,那就在 Worker 層搞定它

有一天我意識到,我的部落格有留言系統,但我完全不知道有人留言了。

不是漏掉一兩則——是完全不知道。要等 comment-monitor agent 輪詢時才會「被動發現」,再靠一段文字通知告訴我「哦,有人留言,而且我幫你起草了一個回覆,但我不太有把握」。

這整個流程最慢可能延遲到下一個輪詢週期(幾十分鐘)才通知我。對用戶來說,等了半小時,博主毫無反應,下次大概不會再留言了。

這是留言互動的體驗黑洞。

問題拆解:70% 已有,缺的是最後那 30%

盤點了一下現有的基礎設施,其實狀況比我想像的好:

  • getLatestComments() — 輪詢拉取新留言,已有
  • postReply() — 呼叫 Blog API 發出回覆,已有
  • 低信心度通知 — 有新留言且 AI 不確定怎麼回,會發一條純文字 Telegram 通知,已有

所以問題很具體:缺的是「推送端(push)」和「一鍵核准互動」

現在的架構是 pull-based(bot 主動去問有沒有留言),我需要的是 push-based(有留言的瞬間,系統主動告訴我)。


第一個坑:D1 根本沒有原生 Trigger

我第一個想到的方案是資料庫層 trigger:用戶送出留言 → D1 寫入 → trigger 觸發 → 通知我。

簡潔,直覺。但查了文件之後發現:Cloudflare D1 的雲端環境不支援 CREATE TRIGGER

這個語法在本地 wrangler dev 環境中可以用,一部署到 Cloudflare 就失效。這是個設計上的刻意選擇——D1 是分散式 SQLite,跨 PoP 的 trigger 語義本來就複雜,Cloudflare 現階段直接不支援。

所以 trigger 這條路死了。

正確做法是:在 Pages Functions(處理 POST /api/comments/:slug 的那隻 Worker)執行完 INSERT 之後,在同一個 request handler 裡手動 fetch() 呼叫 Telegram Bot API。同一個 fetch handler,同步完成,不需要額外排程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Pages Functions: /functions/api/comments/[slug].js
export async function onRequestPost({ request, env }) {
const body = await request.json();

// 1. 寫入 D1
const result = await env.DB.prepare(
"INSERT INTO comments (slug, author, content) VALUES (?, ?, ?)"
).bind(slug, body.author, body.content).run();

const commentId = result.meta.last_row_id;

// 2. 立即推送 Telegram 通知(同一個 handler)
await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: env.ADMIN_CHAT_ID,
text: `📬 新留言 #${commentId}\n\n作者:${body.author}\n內容:${body.content}\n\nAI 草稿生成中...`,
})
});

return new Response(JSON.stringify({ success: true }), { status: 201 });
}

約 20 行。就這樣。


第二個關鍵:Inline Keyboard 讓核准變成一個按鈕

通知我有留言,只是第一步。更重要的是:我要怎麼核准 AI 草稿後自動發出回覆?

最笨的做法是:收到通知 → 打開後台 → 找到那條留言 → 編輯回覆 → 手動發送。這完全違背了「AI 幫我做事」的初衷。

Telegram 的 Inline Keyboard 正是為這種場景設計的:通知訊息裡直接附帶按鈕,點 ✅ 就核准,點 ❌ 就略過。

1
2
3
4
5
6
7
8
{
"reply_markup": {
"inline_keyboard": [[
{ "text": "✅ 發出回覆", "callback_data": "approve:42" },
{ "text": "❌ 略過", "callback_data": "reject:42" }
]]
}
}

callback_data 裡帶著 comment ID,按下按鈕後 Telegram 把 callback_query 事件送給 bot,bot 就知道要對哪條留言做什麼。

Bot 側需要加一個 handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// grammY 的寫法
bot.callbackQuery(/^approve:(\d+)$/, async (ctx) => {
const commentId = parseInt(ctx.match[1]);

// 查暫存的 AI 草稿
const draft = await getPendingDraft(commentId);

// 發出回覆
await postReply(draft.slug, draft.content);

// 消除 loading spinner(必須呼叫,否則按鈕會一直轉)
await ctx.answerCallbackQuery({ text: "✅ 回覆已發出" });

// 編輯原始訊息,移除按鈕
await ctx.editMessageText(`✅ 已回覆留言 #${commentId}`);
});

整個核准流程,從收到通知到發出回覆,不需要離開 Telegram。


第三個決策:AI 草稿什麼時候生成?

這是整個設計裡最微妙的問題。

方案 A:在 Pages Functions 裡即時呼叫 Claude 生成草稿。優點是通知訊息一發出就附帶草稿和按鈕,用戶體驗最好。缺點是 Pages Functions 的執行時間有限制(免費版 10ms CPU time),呼叫 Claude 的延遲會遠超過這個上限,用戶的留言請求也會被卡住等待 AI 回應。

方案 B:先推送「有新留言」的即時通知,bot 的 comment-monitor 輪詢後跑完 Claude,再追發「AI 草稿 + 核准按鈕」。用戶體驗稍差(兩條訊息),但架構乾淨,不影響留言寫入的延遲,也不用擔心 Worker 超時。

我傾向方案 B。

理由很簡單:用戶送出留言後,最關心的是「有沒有成功提交」,不是「博主有沒有立即看到」。把即時通知和 AI 草稿解耦,讓每個步驟各司其職,更健壯。


暫存草稿:D1 比 KV 更合適

當 bot 生成了 AI 草稿,等待我核准,這段期間草稿要放在哪裡?

KV(Cloudflare Workers KV)看起來很方便,但它的索引能力不足——我只能根據 key 查詢,無法做「查詢所有 pending 狀態的草稿」之類的操作。

更好的方案是在同一個 D1 資料庫加一張 pending_replies 表:

1
2
3
4
5
6
7
8
9
CREATE TABLE pending_replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
comment_id INTEGER NOT NULL,
post_slug TEXT NOT NULL,
draft_reply TEXT NOT NULL,
telegram_msg_id INTEGER, -- 用於後續 editMessage 更新通知訊息
status TEXT DEFAULT 'pending', -- pending / approved / rejected
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

telegram_msg_id 這個欄位特別有用:核准後可以用它呼叫 editMessageText(),把原本的通知訊息從「待核准」改成「✅ 已發出」,避免我看到一堆帶按鈕的舊通知搞不清楚狀態。


最後:完整的 Pipeline

把所有東西串起來,理想流程是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用戶送出留言


[Cloudflare Pages Functions]
POST /api/comments/:slug
├─ INSERT INTO comments (D1) ← 記錄留言
└─ fetch(TG sendMessage) ← 即時推送「有新留言」通知

▼ (非同步,幾分鐘後)
[comment-monitor agent 輪詢]
├─ 偵測到新留言
├─ 呼叫 Claude 生成草稿
├─ INSERT INTO pending_replies ← 暫存草稿
└─ 追發 TG 通知(帶 inline keyboard)

我點 ✅ 核准


[Bot callback_query handler]
├─ 查詢 pending_replies
├─ 呼叫 postReply() 發出回覆
├─ UPDATE status='approved'
└─ editMessageText() 顯示「已發出」

現有 70% 基礎設施不動,新增的核心程式碼:Pages Functions 約 20 行,Bot handler 約 30 行,D1 建表一條 SQL。


還沒解決的問題

有幾個邊界情況我還在思考:

1. 並發留言洪水:短時間內多人留言,每條都發一個 Telegram 通知,訊息會淹沒 Telegram。需要 debounce 或批次策略(例如 5 分鐘內累積後送出摘要)。

2. 原子性:Pages Functions 裡 INSERT 成功但 fetch() 通知失敗,該怎麼處理?重試?還是接受「最終一致性」?目前傾向接受偶爾的通知遺漏,畢竟 comment-monitor 還在跑,頂多延遲被發現而已。

3. comment_id 時序:Pages Functions 需要先拿到 lastInsertRowid 才能存 pending_reply 和發通知,flow 順序要仔細確認,避免競態條件。


構建這種「人在迴路」(human-in-the-loop)系統的核心挑戰,不是技術,而是決策:什麼時候讓 AI 自動做,什麼時候讓人拍板

留言回覆的場景裡,我選擇讓 AI 起草,讓人決定發不發。不是不信任 AI 的品質,而是這條訊息代表我作為博主的聲音——在我建立足夠的信心之前,這個按鈕還是應該讓我來按。

一見生財,2026-03-10

📡 想看更多?加入 AI 印鈔指南 頻道,每日推送 AI 技術前沿 + 加密貨幣投資情報

留言

載入留言中...

留下你的想法