你可以把 Telegram plugin 想像成一個前台。
有人敲門、前台先看證件、驗明正身、確認這個人有沒有資格進來,然後才把訊息交給後面的工作台。
工作台不是立刻回一段字而已,它會先判斷這是哪個 session、哪個 thread、是不是要 streaming、要不要 typing、要不要先送 preview、最後還有沒有 fallback。
所以第 22 天要看的不是「Telegram 收到 webhook 後呼叫了誰」,而是:
Telegram 的訊息流,從收到到回覆,到底經過哪些關卡?
這裡最重要的一件事是:
OpenClaw 不把 webhook 當終點,它只是入口。
Telegram 的訊息流,我會切成六段:
這幾段看起來像是很多 function,但本質上只有一句話:
先確保這包 update 是真的,再把它交給 OpenClaw 的工作流。
先看 webhook 入口怎麼守門。
📄 原始碼:
extensions/telegram/src/webhook.ts:911-927
const handler = grammy.webhookCallback(bot, "callback", {
secretToken: secret,
onTimeout: "return",
timeoutMilliseconds: TELEGRAM_WEBHOOK_CALLBACK_TIMEOUT_MS,
});
if (
!applyBasicWebhookRequestGuards({
req,
res,
rateLimiter: telegramWebhookRateLimiter,
rateLimitKey: resolveTelegramWebhookRateLimitKey(req, path, opts.config),
})
) {
return;
}
const secretHeader = resolveSingleHeaderValue(req.headers["x-telegram-bot-api-secret-token"]);
if (!hasValidTelegramWebhookSecret(secretHeader, secret)) {
res.shouldKeepAlive = false;
res.setHeader("Connection", "close");
respondText(401, "unauthorized");
return;
}
這裡先做兩件事:
再看 body 怎麼進來。
📄 原始碼:
extensions/telegram/src/webhook.ts:931-946
const body = await readJsonBodyWithLimit(req, {
maxBytes: TELEGRAM_WEBHOOK_MAX_BODY_BYTES,
timeoutMs: TELEGRAM_WEBHOOK_BODY_TIMEOUT_MS,
emptyObjectOnEmpty: false,
});
if (!body.ok) {
if (body.code === "PAYLOAD_TOO_LARGE") {
respondText(413, body.error);
return;
}
if (body.code === "REQUEST_BODY_TIMEOUT") {
respondText(408, body.error);
return;
}
if (body.code === "CONNECTION_CLOSED") {
respondText(400, body.error);
return;
}
respondText(400, body.error);
return;
}
Telegram update 還沒真的進 bot 之前,OpenClaw 先用 body limit 和 timeout 把入口收緊。
這不是保守而已,這是讓 webhook 入口在網路世界裡不要太好欺負。
最後才把 body 交給 handler。
📄 原始碼:
extensions/telegram/src/webhook.ts(本機版本行號已變動,僅標到檔案)
await handler(body.value, reply, secretHeader, unauthorized);
if (!replied) {
respondText(200);
}
這一步結束後,才算真的把 update 送進 Telegram bot 的流程。
接著看 reply side。
📄 原始碼:
extensions/telegram/src/bot-message-dispatch.ts:2119-2119
const { onModelSelected, ...replyPipeline } = (
telegramDeps.createChannelReplyPipeline ?? createChannelReplyPipeline
)({
cfg,
agentId: route.agentId,
channel: "telegram",
accountId: route.accountId,
typing: {
start: sendTyping,
onStartError: (err) => {
logTypingFailure({
log: logVerbose,
channel: "telegram",
target: String(chatId),
error: err,
});
},
},
});
這裡就是 Telegram 開始進入 OpenClaw 共用 reply pipeline 的地方。
再往後,真正的分發是這樣:
📄 原始碼:
extensions/telegram/src/bot-message-dispatch.ts:1787-1789
const result = await (telegramDeps.deliverReplies ?? deliverReplies)({
...deliveryBaseOptions,
replies: [payload],
onVoiceRecording: sendRecordVoice,
silent: silentErrorReplies && payload.isError === true,
mediaLoader: telegramDeps.loadWebMedia,
});
if (result.delivered) {
deliveryState.markDelivered();
}
這段很重要。
它告訴你 reply 不只是「把字發出去」。
它還要經過:
最後還有 cleanup 和 fallback:
📄 原始碼:
extensions/telegram/src/bot-message-dispatch.ts:2955-2964
if (
dispatchError ||
(!deliverySummary.delivered &&
(deliverySummary.skippedNonSilent > 0 || deliverySummary.failedNonSilent > 0))
) {
const fallbackText = dispatchError
? "Something went wrong while processing your request. Please try again."
: EMPTY_RESPONSE_FALLBACK;
const result = await (telegramDeps.deliverReplies ?? deliverReplies)({
replies: [{ text: fallbackText }],
...deliveryBaseOptions,
silent: silentErrorReplies && (dispatchError != null || hadErrorReplyFailureOrSkip),
mediaLoader: telegramDeps.loadWebMedia,
});
sentFallback = result.delivered;
}
OpenClaw 的習慣不是「失敗就算了」。
它會盡量補一個看得見的回覆,至少讓使用者知道系統有反應。
你如果把 webhook 想成「外部來一個 JSON,裡面有 update」,就太天真了。
真正先做的事其實是:
這些動作都是在保護後面的主流程。
Telegram webhook 進來的 payload 只是「看起來像 update」。
OpenClaw 會把它交給 bot,但 bot 之後還會根據:
來決定這包訊息到底怎麼處理。
這一點很關鍵。
Telegram 的送出流程會用到 Telegram-specific delivery,但「reply 決策」本身是 OpenClaw 共用邏輯:
所以 Telegram 不是自己重造一套答案生成器。
它只是把自己的 UI 能力接到共用 pipeline 上。
Telegram 很適合做 preview:
但這件事很容易留下骯髒狀態。
所以你會看到它很在意:
這不是多做,而是在避免聊天室變成垃圾場。
Telegram 這條線很像一條生產線。
你只看最前面的輸入口,會覺得很簡單;你一路跟到最後,才知道它其實很會收尾。
第 23 天我要換到 LINE。
你會看到同樣是 plugin,但 LINE 的驗證方式、事件模型、rich message 表達法,跟 Telegram 完全不是同一種氣質。