iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

開場故事

假設你今天不是在寫一個聊天機器人,而是在替 OpenClaw 裝一扇新門。

這扇門剛好開在 Telegram 上。

有人從 Telegram 傳訊息進來,OpenClaw 要認得他、知道他屬於哪個 agent、要不要放行、要不要綁定 session,最後還要把回覆送回 Telegram。
如果只用「讀到訊息就回一句」的思維,很容易把整件事想得太小。

真正麻煩的是:

  • Telegram 只是入口,不是核心
  • 入口要接到 OpenClaw 的 routing、session、allowlist、delivery
  • 入口還要自己處理 webhook、token、thread、群組、命令、反應、streaming

所以第 21 天我要看的不是「Telegram 能不能收訊息」,而是:

Telegram plugin 到底怎麼接進 OpenClaw 的核心?

這一篇是後面 Telegram 訊息流的前奏。
先把門框裝好,才有資格談門裡面怎麼走。

今天要解的問題

  • Telegram plugin 在 OpenClaw 裡扮演什麼角色?
  • 它是怎麼被載入的?
  • 哪些邏輯屬於 Telegram,哪些邏輯其實是 OpenClaw core?
  • 為什麼它不是一個單純的 webhook handler?
  • 它怎麼把 setup、security、routing、delivery 接成一條線?

架構總覽

Telegram plugin 的接法其實可以拆成兩層。

第一層是「外殼」:

  • extensions/telegram/index.ts
  • defineBundledChannelEntry(...)

這一層負責告訴 OpenClaw:

  • 這個 plugin 的 id 是什麼
  • 要去哪裡載入真正的實作
  • secrets 和 runtime 要去哪裡接

第二層是「核心設定」:

  • extensions/telegram/src/channel.ts
  • createChatChannelPlugin(...)

這一層負責告訴 OpenClaw:

  • 這個 channel 怎麼做 allowlist
  • 怎麼做 binding
  • 怎麼解讀 inbound / outbound target
  • 怎麼接 setup、status、gateway、actions、directory

換句話說:

  • index.ts 像是把 Telegram 插件插上電
  • channel.ts 才是 Telegram 跟 OpenClaw 對接的轉接頭

原始碼節錄

先看最外層的入口。

📄 原始碼:extensions/telegram/index.ts:2-20

import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";

export default defineBundledChannelEntry({
  id: "telegram",
  name: "Telegram",
  description: "Telegram channel plugin",
  importMetaUrl: import.meta.url,
  plugin: {
    specifier: "./channel-plugin-api.js",
    exportName: "telegramPlugin",
  },
  secrets: {
    specifier: "./secret-contract-api.js",
    exportName: "channelSecrets",
  },
  runtime: {
    specifier: "./runtime-api.js",
    exportName: "setTelegramRuntime",
  },
});

這段很像是在報戶口。

它沒有真的開始收訊息,也沒有真的打 Telegram API。
它只是把 Telegram plugin 的身分、入口、secret、runtime 告訴 OpenClaw。

再看真正的核心對接。

📄 原始碼:extensions/telegram/src/channel.ts:730-922

export const telegramPlugin = createChatChannelPlugin({
  base: {
    ...createTelegramPluginBase({
      setupWizard: telegramSetupWizard,
      setup: telegramSetupAdapter,
    }),
    allowlist: buildDmGroupAccountAllowlistAdapter({
      channelId: "telegram",
      resolveAccount: resolveTelegramAccount,
      normalize: ({ cfg, accountId, values }) =>
        telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
      resolveDmAllowFrom: (account) => account.config.allowFrom,
      resolveGroupAllowFrom: (account) => account.config.groupAllowFrom,
      resolveDmPolicy: (account) => account.config.dmPolicy,
      resolveGroupPolicy: (account) => account.config.groupPolicy,
      resolveGroupOverrides: resolveTelegramAllowlistGroupOverrides,
    }),
    bindings: {
      selfParentConversationByDefault: true,
      compileConfiguredBinding: ({ conversationId }) =>
        normalizeTelegramAcpConversationId(conversationId),
      matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) =>
        matchTelegramAcpConversation({
          bindingConversationId: compiledBinding.conversationId,
          conversationId,
          parentConversationId,
        }),
      resolveCommandConversation: ({ threadId, originatingTo, commandTo, fallbackTo }) =>
        resolveTelegramCommandConversation({
          threadId,
          originatingTo,
          commandTo,
          fallbackTo,
        }),
    },
    messaging: {
      normalizeTarget: normalizeTelegramMessagingTarget,
      resolveInboundConversation: ({ to, conversationId, threadId }) =>
        resolveTelegramInboundConversation({ to, conversationId, threadId }),
      resolveDeliveryTarget: ({ conversationId, parentConversationId }) =>
        resolveTelegramDeliveryTarget({ conversationId, parentConversationId }),
      resolveSessionConversation: ({ kind, rawId }) => resolveTelegramSessionConversation({ kind, rawId }),
      parseExplicitTarget: ({ raw }) => parseTelegramExplicitTarget(raw),
      inferTargetChatType: ({ to }) => parseTelegramExplicitTarget(to).chatType,
    },
    setup: telegramSetupAdapter,
    status: createComputedAccountStatusAdapter<ResolvedTelegramAccount, TelegramProbe>({
      /* ... */
    }),
  },
});

這段的重點不是每個 helper 的名字,而是它把 Telegram 對接成一個完整 channel。

OpenClaw 不是只要知道「這裡是 Telegram」就夠了。
它還要知道:

  • 這個 Telegram 帳號怎麼設定
  • 哪些人可以進來
  • 一個 Telegram 的 conversation 要怎麼對應到 session
  • 回覆要送回哪個 chat、哪個 thread

白話拆解

1. defineBundledChannelEntry 是把插件接到 loader

這個函式像是一個包裝器。

你可以把它想成:

  • 這不是任意一個 JS module
  • 這是 OpenClaw 可以認得的 bundled channel entry

它要回報給 runtime:

  • 插件 id
  • 顯示名稱
  • 讀哪個 export 才能拿到 plugin
  • 讀哪個 export 才能拿到 secrets
  • 如果有 runtime hook,要去哪裡接

所以 Telegram plugin 不是「寫好就算」。
它必須先進入 OpenClaw 的插件契約。

2. createChatChannelPlugin 才是核心裝配

這個工廠函式把 Telegram 包成一個真正的 channel plugin。

裡面每個區塊都不是多餘的:

  • base:基本能力和 setup
  • allowlist:誰可以進來
  • bindings:conversation 怎麼綁 session
  • messaging:target 怎麼解、回覆怎麼送
  • setup:怎麼安裝、怎麼填 token
  • status:怎麼檢查狀態

換句話說,Telegram 不是一個孤零零的 webhook。
它是一整組可以被 OpenClaw core 管理的能力集合。

3. Telegram plugin 是「入口定義」,不是「業務本體」

真正的業務本體其實在 OpenClaw core:

  • routing
  • session binding
  • reply pipeline
  • allowlist policy
  • conversation resolution

Telegram plugin 只是把這些核心能力套到 Telegram 的語境裡。

所以你會看到它一直在翻譯:

  • Telegram 的 chat / topic
  • Telegram 的 user id / group id
  • Telegram 的 webhook update
  • Telegram 的命令與回覆

它不是在重新發明一套 agent system。
它是在把 Telegram 世界翻成 OpenClaw 可以理解的世界。

4. 為什麼這樣切是對的

如果把 Telegram 邏輯全部塞進 core,core 會變得很髒。

如果把 core 邏輯全部塞進 Telegram plugin,每個 channel 又會各寫一份。

OpenClaw 的做法比較像:

  • core 負責共通語意
  • channel plugin 負責 channel-specific adapter

這樣 Telegram、LINE、Discord、Slack 才能共用同一套心臟。

設計取捨

  • 好處是入口清楚,loader 很容易知道要載入什麼
  • 好處是 channel plugin 可以很薄,核心能力集中在 core
  • 好處是 Telegram 的特殊規則可以獨立演進,不會污染其他 channel
  • 代價是抽象層變多,第一次看會覺得「只是收訊息而已,為什麼要這麼多層」
  • 代價是 debug 時要先分清楚是入口問題、binding 問題、還是 messaging 問題

但這種複雜度是值得的。
因為 Telegram 只是第一個 plugin,真正重要的是這個框架以後能不能接更多 channel。

今天的結論

  • Telegram plugin 不是單一 handler,而是 OpenClaw channel contract 的一部分
  • index.ts 負責讓 loader 找到 plugin,channel.ts 負責把 Telegram 裝進 core
  • allowlist、binding、messaging、setup、status 都是同一條線上的不同節點
  • OpenClaw core 負責共通語意,Telegram plugin 負責 channel 翻譯
  • 這種切法讓 Telegram 不會綁死整個系統,也讓 core 不會被某個 channel 污染

下一步

第 22 天我會接著看 Telegram 的訊息流。
入口裝好了之後,訊息怎麼進、怎麼驗證、怎麼進到 reply pipeline,才是最有戲的地方。


上一篇
第 20 天:前 20 天收斂,把 OpenClaw 畫成一張地圖
下一篇
第 22 天:Telegram plugin 的訊息流,從收到到回覆
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言