如果把 OpenClaw 比成一間大工廠,Telegram 和 LINE 就像兩個不同的前門。
一個前門比較適合快節奏、thread、streaming 和 command。
另一個前門比較適合 rich message、webhook 驗證和卡片式互動。
但奇妙的是,兩扇門後面走的,其實是同一套工廠流程。
第 25 天我想把這件事講完整:
同一個核心,Telegram 和 LINE 怎麼做兩種入口?
這篇不是要重新講前面四天,而是把前面拆過的點收回來,讓你看到「共用」和「差異」到底放在哪裡。
我會用三層來看:
共用核心負責:
channel plugin adapter 負責:
platform-specific transport / UX 負責:
這樣看就會很清楚:
差異不在「是不是 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);
},
},
},
});
把這兩段放一起看,差異就很清楚了:
兩個 channel 看起來不同,但它們都需要:
這些事情如果每個 channel 都自己做一份,系統一定會碎掉。
所以 OpenClaw 把真正的共通點抽出來:
channel plugin 只做那些跟平台有關的差異。
很多人會把差異想成:
其實還不只這樣。
Telegram 的差異還包括:
LINE 的差異還包括:
也就是說,它們差的不只是顯示風格,而是整條 message lifecycle 的形狀。
transformReplyPayload 是這種架構最漂亮的地方這個 hook 很像翻譯官。
core 先產生「通用語意」:
然後 channel plugin 再把它翻成平台語言。
Telegram 可能把它變成:
LINE 可能把它變成:
這就是正確的抽象。
因為新 channel 加進來時,你不用重寫 core。
你只要回答幾個問題:
如果這幾個問題能被 adapter 解決,OpenClaw core 就可以不動。
這就是平台化架構最重要的能力。
但這就是成熟系統的樣子。
你不可能既想要平台一致性,又想要每個平台都完全不同,還不想付出抽象成本。
createChannelReplyPipeline 是共用 reply 結構的關鍵transformReplyPayload 是 channel-specific 表達的翻譯點前 25 天算是把 Telegram / LINE 這一段收完了。
接下來第 26 天會切到 ClawHub,開始看 skill 和 plugin 怎麼在平台上被管理、被挑選、被組合。