iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
佛心分享-SideProject30

30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化系列 第 25

第 25 天:同一個核心,Telegram 和 LINE 怎麼做兩種入口

  • 分享至 

  • xImage
  •  

開場故事

如果把 OpenClaw 比成一間大工廠,Telegram 和 LINE 就像兩個不同的前門。

一個前門比較適合快節奏、thread、streaming 和 command。
另一個前門比較適合 rich message、webhook 驗證和卡片式互動。

但奇妙的是,兩扇門後面走的,其實是同一套工廠流程。

第 25 天我想把這件事講完整:

同一個核心,Telegram 和 LINE 怎麼做兩種入口?

這篇不是要重新講前面四天,而是把前面拆過的點收回來,讓你看到「共用」和「差異」到底放在哪裡。

今天要解的問題

  • Telegram 和 LINE 到底共用了哪些核心?
  • 哪些部分是 channel-specific adapter?
  • 為什麼 OpenClaw 要把 channel plugin 做成這種形狀?
  • 同樣是 reply pipeline,為什麼兩邊表現差這麼多?
  • 如果未來再加一個 channel,這套架構還撐得住嗎?

架構總覽

我會用三層來看:

  1. 共用核心
  2. channel plugin adapter
  3. platform-specific transport / UX

共用核心負責:

  • routing
  • session
  • allowlist policy
  • reply pipeline
  • conversation binding
  • delivery semantics

channel plugin adapter 負責:

  • normalize target
  • resolve inbound conversation
  • resolve delivery target
  • transform reply payload
  • setup / status / gateway / outbound

platform-specific transport / UX 負責:

  • Telegram 的 webhook、thread、streaming、reaction、typing
  • LINE 的 raw body signature、reply token、rich message、quick reply

這樣看就會很清楚:

差異不在「是不是 bot」,差異在「這個 bot 對外講什麼語言」。

原始碼節錄

先看共用 reply pipeline。

📄 原始碼:src/channels/message/reply-pipeline.ts:53-123

export function createChannelReplyPipeline(params: {
  cfg: Parameters<typeof createReplyPrefixOptions>[0]["cfg"];
  agentId: string;
  channel?: string;
  accountId?: string;
  typing?: CreateTypingCallbacksParams;
  typingCallbacks?: TypingCallbacks;
}): ChannelReplyPipeline {
  const channelId = params.channel
    ? (normalizeChannelId(params.channel) ?? params.channel)
    : undefined;
  const plugin = channelId ? getChannelPlugin(channelId) : undefined;
  const transformReplyPayload = plugin?.messaging?.transformReplyPayload
    ? (payload: ReplyPayload) =>
        plugin.messaging?.transformReplyPayload?.({
          payload,
          cfg: params.cfg,
          accountId: params.accountId,
        }) ?? payload
    : undefined;
  return {
    ...createReplyPrefixOptions({
      cfg: params.cfg,
      agentId: params.agentId,
      channel: params.channel,
      accountId: params.accountId,
    }),
    ...(transformReplyPayload ? { transformReplyPayload } : {}),
    ...(params.typingCallbacks
      ? { typingCallbacks: params.typingCallbacks }
      : params.typing
        ? { typingCallbacks: createTypingCallbacks(params.typing) }
        : {}),
  };
}

這個函式是整張圖的骨架之一。

它先拿 channel plugin,再看看這個 channel 有沒有自己的 transformReplyPayload
也就是說,OpenClaw 不是把所有 channel 都硬塞成同一種輸出,而是保留一個共用管線,再給每個 channel 一個翻譯點。

再看 Telegram 的入口。

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

export const telegramPlugin = createChatChannelPlugin({
  base: {
    ...createTelegramPluginBase({
      setupWizard: telegramSetupWizard,
      setup: telegramSetupAdapter,
    }),
    allowlist: buildDmGroupAccountAllowlistAdapter({
      channelId: "telegram",
      resolveAccount: resolveTelegramAccount,
      /* ... */
    }),
    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,
    },
  },
});

再看 LINE。

📄 原始碼:extensions/line/src/channel.ts:41-82

export const linePlugin: ChannelPlugin<ResolvedLineAccount> = createChatChannelPlugin({
  base: {
    id: "line",
    ...lineChannelPluginCommon,
    setupWizard: lineSetupWizard,
    messaging: {
      normalizeTarget: (target) => {
        const trimmed = target.trim();
        if (!trimmed) {
          return undefined;
        }
        return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
      },
      resolveInboundConversation: lineBindingsAdapter.resolveInboundConversation,
      transformReplyPayload: ({ payload }) => {
        if (!payload.text || !hasLineDirectives(payload.text)) {
          return payload;
        }
        return parseLineDirectives(payload);
      },
    },
  },
});

把這兩段放一起看,差異就很清楚了:

  • Telegram 比較偏向 session/thread/streaming/delivery 的整合
  • LINE 比較偏向 target normalization / directive transform / rich message

白話拆解

1. 共用核心其實比想像中大很多

兩個 channel 看起來不同,但它們都需要:

  • 找到對的人
  • 綁定對的 session
  • 檢查有沒有權限
  • 把 inbound 變成 message context
  • 把 reply 變成可送出的 payload

這些事情如果每個 channel 都自己做一份,系統一定會碎掉。

所以 OpenClaw 把真正的共通點抽出來:

  • routing 在 core
  • session binding 在 core
  • reply pipeline 在 core
  • reply history / policy 在 core

channel plugin 只做那些跟平台有關的差異。

2. Telegram 和 LINE 的差異,不只是 UI

很多人會把差異想成:

  • Telegram 比較像文字
  • LINE 比較像卡片

其實還不只這樣。

Telegram 的差異還包括:

  • thread / topic
  • streaming preview
  • typing / reactions
  • webhook body and secret token

LINE 的差異還包括:

  • raw body signature
  • reply token
  • replay cache
  • rich message directives
  • group / room / user ID 的處理

也就是說,它們差的不只是顯示風格,而是整條 message lifecycle 的形狀。

3. transformReplyPayload 是這種架構最漂亮的地方

這個 hook 很像翻譯官。

core 先產生「通用語意」:

  • 這是一段回覆
  • 這裡有文字
  • 這裡有附件
  • 這裡有錯誤提示

然後 channel plugin 再把它翻成平台語言。

Telegram 可能把它變成:

  • reply-to
  • streaming draft
  • HTML rendering

LINE 可能把它變成:

  • quick reply
  • flex card
  • template message

這就是正確的抽象。

4. 為什麼這樣設計比較能長大

因為新 channel 加進來時,你不用重寫 core。

你只要回答幾個問題:

  • 這個 channel 的 target 長什麼樣?
  • 進來的事件怎麼變成 conversation?
  • reply 要怎麼轉成它的 payload?
  • webhook 安全模型是什麼?

如果這幾個問題能被 adapter 解決,OpenClaw core 就可以不動。

這就是平台化架構最重要的能力。

設計取捨

  • 好處是共用核心穩定,channel 差異被限制在 adapter
  • 好處是 Telegram 和 LINE 都能保留自己的平台特性
  • 好處是 reply pipeline / session / routing 不需要每次重寫
  • 好處是以後加新 channel 時,路線已經清楚
  • 代價是抽象層數偏多,剛進來會覺得每個地方都不是最後一層
  • 代價是 debug 時要同時看 core 和 plugin,不能只看其中一邊

但這就是成熟系統的樣子。
你不可能既想要平台一致性,又想要每個平台都完全不同,還不想付出抽象成本。

今天的結論

  • Telegram 和 LINE 共用的是 OpenClaw 的核心工作流,不是各自一套聊天引擎
  • 差異主要放在 plugin adapter 層:target、conversation、payload、webhook、安全
  • createChannelReplyPipeline 是共用 reply 結構的關鍵
  • transformReplyPayload 是 channel-specific 表達的翻譯點
  • 這種分層讓 OpenClaw 可以同時保留核心一致性和平台差異

下一步

前 25 天算是把 Telegram / LINE 這一段收完了。
接下來第 26 天會切到 ClawHub,開始看 skill 和 plugin 怎麼在平台上被管理、被挑選、被組合。


上一篇
第 24 天:LINE plugin 的事件,驗證與常見坑
系列文
30 天走進 OpenClaw:一個 AI Agent 的誕生、掙扎與進化25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言