上一篇写了怎么把 OpenClaw 接入飞书,有读者问了个好问题:"它底下是怎么做到同时接这么多平台的?每个平台 API 都不一样,消息格式也不一样,不会乱吗?"
不会乱。但要做到不乱,底下的架构得下功夫。
今天深入拆解 OpenClaw 的渠道系统(Channel System)——它怎么用一套接口抽象和统一消息格式,同时对接 Telegram、WhatsApp、Discord、Signal 四个 API 风格完全不同的平台。这篇偏架构设计,会涉及接口定义、字段映射、限流算法和配置结构,适合做过 Bot 开发或对 AI Agent 工程架构感兴趣的读者。

1. 定位与职责
渠道系统是 Gateway 的"耳朵和嘴巴"——往外连各类 IM 平台,往内翻译成 Agent 能理解的统一格式。
用户在 Telegram 发了条语音,在 Discord 发了段 Markdown,在 WhatsApp 发了张图——这三条消息到了 Agent 面前,长得一模一样。Agent 不需要知道消息从哪个平台来,也不需要关心回复该用什么格式发回去。这些脏活,全是渠道系统的事。
拆开看就五件事:
1. 连接平台 API,维持住连接 2. 接收消息 → 标准化为 InternalMessage 3. 发送回复 → 从 OutboundMessage 转为平台格式 4. 处理重连、限流、媒体上传/下载 5. 管理平台特有交互(按钮、编辑、线程)
架构上,每个平台对应一个 Adapter,所有 Adapter 注册到 Channel Registry,通过统一的 Channel Bus 汇入 Gateway 的 Event Router。
2. Channel 接口设计
整个渠道系统的核心是一个 Channel 接口。所有平台适配器都实现这个接口,Gateway 层只跟接口交互,不碰任何平台特有的东西。
interface Channel { readonly id: ChannelType; readonly displayName: string; status: ChannelStatus; connect(config: ChannelConfig): Promise<void>; disconnect(): Promise<void>; send(message: OutboundMessage): Promise<SendResult>; editMessage(platformMsgId: string, newContent: string): Promise<void>; getCapabilities(): ChannelCapabilities; getPairedUsers(): PairedUser[]; onMessage: (msg: InternalMessage) => void; onError: (err: ChannelError) => void; onStatusChange: (status: ChannelStatus) => void;}几个设计要点:
• send返回SendResult,包含platformMessageId——后续做消息编辑(流式回复)靠的就是这个 ID• getCapabilities()让上层知道当前平台支持什么能力(能不能编辑消息、最大文本长度多少),发送前据此做格式适配• 三个回调 onMessage、onError、onStatusChange把事件往上推,Gateway 不轮询 Adapter
ChannelCapabilities 是决定发送行为的关键结构:
interface ChannelCapabilities { maxTextLength: number; // Telegram: 4096, Discord: 2000 supportsMarkdown: boolean; supportsInlineButtons: boolean; supportsReactions: boolean; supportsThreads: boolean; supportsVoice: boolean; supportsFiles: boolean; supportsEditMessage: boolean; // 流式回复依赖这个 maxFileSize: number; // bytes}SendResult 里有个容易忽略的字段 cost——WhatsApp Cloud API 是按条计费的,需要追踪 API 调用次数:
interface SendResult { platformMessageId: string; timestamp: string; cost?: { apiCalls: number };}3. InternalMessage:统一消息格式
这是渠道系统最核心的设计决策——定义一个跟平台无关的消息结构,所有入站消息必须转成这个格式。
interface InternalMessage { id: string; // UUID v7(自带时间排序) channel: ChannelType; direction: "inbound" | "outbound"; sender: { userId: string; // 平台用户 ID username?: string; // @username displayName?: string; paired: boolean; permissions: "full" | "standard" | "readonly" | "none"; }; context: { chatId: string; // 对话 ID (DM=userId, Group=groupId) chatType: "private" | "group"; threadId?: string; // 话题/线程 ID replyToId?: string; // 引用消息的 platformMsgId platformMessageId?: string; // 原始平台消息 ID }; content: { text: string; media: MediaAttachment[]; metadata: Record<string, unknown>; // 平台特有元数据透传 }; timestamp: string; // ISO 8601 UTC}sender.permissions 分四级:full 可以执行危险命令,standard 执行常规操作,readonly 只读,none 就是没配对的陌生人——这直接跟安全模型挂钩。
content.metadata 是个开口——各平台有自己的私有字段(Telegram 的 message_thread_id、Discord 的 guild_id),不方便都抽到顶层,就透传到这里,Adapter 自己取。
媒体附件也做了统一抽象:
interface MediaAttachment { type: "image" | "audio" | "video" | "document" | "voice" | "sticker"; url?: string; // 平台 CDN URL localPath?: string; // 下载后的本地路径 mimeType: string; size: number; // bytes filename?: string; caption?: string; duration?: number; // 音视频时长(秒) width?: number; height?: number; transcription?: string; // 语音转文字结果}voice 类型的 transcription 字段是语音消息的关键——Agent 直接读这个文字,不需要自己处理音频。
4. 消息幂等性
同一条消息被平台推送两次,比你想的常见。Telegram 的 getUpdates 可能重复返回,WhatsApp 的 Webhook 会重试,用户手快双击也算一种。
处理方式很朴素——LRU 缓存去重:
key = `${msg.channel}:${platformMessageId}`缓存容量: 10000 条TTL: 10 分钟onMessage(msg): if (recentMessageIds.has(key)): 跳过 recentMessageIds.set(key, Date.now()) processMessage(msg)没什么花活儿,但不做的话 Agent 会对同一条消息回复两次,用户体验直接炸。
5. 四个适配器的差异
四个平台的 API 风格和能力差异很大。先看一张全景对比表:
*粗体*_斜体_ | ||||

没有两个平台的能力组合是一样的。 这就是为什么 ChannelCapabilities 声明如此重要——上层不能假设任何能力,必须查了再做。
5.1 Telegram Adapter
Telegram 是功能最全的渠道。两种连接方式:长轮询(默认,getUpdates 循环,无需公网 IP,延迟 1s)和 Webhook(需 HTTPS 公网,延迟 100ms)。文件限制:发送 50MB、下载 20MB(云端 Bot API);自部署 Local Bot API Server 均可达 2GB。
连接流程:getMe() 验证 Token → setMyCommands() 注册斜杠命令 → 进入消息循环。
Telegram 消息到 InternalMessage 的字段映射:
message.message_id | context.platformMessageId | |
message.from.id | sender.userId | |
message.from.username | sender.username | |
message.from.first_name | sender.displayName | |
message.chat.id | context.chatId | |
message.chat.type"private" / "group" / "supergroup") | context.chatType"private" / "group") | |
message.text | content.text | |
message.photo | content.media[0]type: "image" | |
message.voicefile_id → 下载 → STT) | content.media[0]type: "voice" + transcription | |
message.reply_to_message | context.replyToId | |
message.message_thread_id | context.threadId |
Telegram 的 MarkdownV2 是个坑——_*[]()~\>#+-=|{}.!` 这些字符全要转义,否则消息直接发送失败。OpenClaw 在格式转换层统一处理。
5.2 WhatsApp Adapter
WhatsApp 走 Cloud API,只能 Webhook 模式。需要 Meta Business 账号,消息通过 graph.facebook.com 发送。
最大的限制是 24 小时消息窗口——用户先发消息,你有 24h 可以自由回复;超过这个窗口,只能用审批过的 Template Message。
Webhook 验证是标准的 Hub Challenge 流程:
GET /webhook/whatsapp ?hub.mode=subscribe &hub.challenge=CHALLENGE_STRING &hub.verify_token=MY_VERIFY_TOKEN→ 验证 verify_token → 返回 hub.challengeWhatsApp 不支持 Markdown 渲染、不支持线程回复。消息编辑仅限发送后 15 分钟内且只能改文本(官方 Cloud API 原生支持有限),对流式回复场景不实用,退化为进度消息模式。
5.3 Discord Adapter
通过 Discord.js 走 Gateway WebSocket 连接。需要的 Intents:Guilds、GuildMessages、DirectMessages、MessageContent(最后这个是特权 Intent,需要在 Developer Portal 申请)。
Discord 的群组触发条件值得注意:@Bot mention、Slash 命令、引用回复 Bot 消息、或者 Bot 参与的 Thread 内的所有消息——这四个满足任意一个才触发响应。
消息长度 2000 字符是个现实问题。Agent 的回复经常超限,处理策略是先按段落分割,超限严重的转为文件附件。Discord 还支持 Embed(富文本卡片),适合用来做结构化输出。
5.4 Signal Adapter
Signal 通过 signal-cli 或 signald 做命令行桥接,接口是 JSON-RPC over stdin/stdout 或 Unix Socket。需要一个绑定了手机号的账号。
功能最弱但隐私最强——端到端加密自动保持。没有按钮、没有消息编辑、没有线程。适合对隐私要求极高的场景。
6. 消息发送与格式适配
Agent 回复生成后,到用户真正看到中间要过六关:

[1] 格式检测 — 扫描回复内容:有没有 Markdown、代码块、表格、图片?文本多长?
[2] 平台能力查询 — 调 getCapabilities() 拿到目标平台的能力声明。
[3] 格式转换 — 这一步差异最大:
**bold***bold*,其余格式 strip 为纯文本 | |
[4] 长度分割 — 超过 maxTextLength 就得切。优先按段落切,段落太长按句子切,代码块不能从中间断开,实在超限转为文件附件。返回 messages[] 按顺序发送。
[5] 媒体处理 — 本地文件直接上传,URL 先下载再上传,Base64 落盘后上传。各平台上传方式不同:Telegram 用 sendPhoto/sendDocument,Discord 走附件上传,WhatsApp 要先获取 media_id 再发。
[6] 发送 + 确认 — 成功了记录 platformMessageId(后续编辑用),失败重试 3 次(指数退避),仍失败降级纯文本再试,最终失败记错误日志。
7. 流式回复策略
Agent 的回复是流式生成的,怎么展示取决于平台能不能编辑已发消息。

策略一:编辑消息(Telegram、Discord)
[1] 发送初始消息 "▍"(光标占位)→ 记录 platformMsgId[2] 流式文本累积到 buffer[3] 每 STREAM_THROTTLE_MS(500ms): editMessage(platformMsgId, buffer + "▍")[4] 工具调用开始 → editMessage(..., buffer + "\n🔧 执行中...")[5] 工具调用完成 → 新一轮流式输出[6] 最终完成 → editMessage(platformMsgId, finalText)体验接近 ChatGPT 的逐字输出效果。
节流是必须的——Telegram 每聊天每分钟约 20 次编辑(社区实测值),Discord 每 5 秒约 5 次(经验值,实际以 X-RateLimit-* 响应头为准)。节流间隔要动态调整,贴着限额跑但不能超。
策略二:进度消息(WhatsApp、Signal)
[1] 发送 "⏳ 正在处理..."[2] Agent 处理中 → 无更新[3] 工具调用 → 发送 "🔧 正在查询邮件..."[4] 最终完成 → 发送完整回复无法编辑,只能在关键节点发通知,最后一次性推最终结果。体验差一截,但平台不支持就只能这样。
8. 语音消息处理管道
用户发了语音消息,处理链路:

下载 → Telegram 通过 file_id → getFile() 拿到下载链接(OGG/Opus),WhatsApp 直接从 media_url 下载(OGG/AAC),保存到 tmp/downloads/voice-{uuid}.ext。
格式转换(如需)→ 大部分 STT 模型直接支持 OGG,不支持的用 ffmpeg 转 WAV。
Speech-to-Text → 三个 Provider 按优先级选择:
构造消息 → 转写结果填入 content.text,原始语音文件引用保留在 content.media[0]。Agent 看到的是文字,但需要时可以回溯到原始音频。
语音相关配置:
{ "channels": { "voice": { "sttProvider": "openai", "localModel": "ggml-medium", "language": "auto" } }}9. 媒体处理管道
入站(下载)
平台消息包含媒体时:提取 URL 或 file_id → 下载到本地 tmp/downloads/(超时 60s,大小限制 50MB)→ 按类型处理:
• image:保留原图 + 生成缩略图 • voice:走 STT 转文字(上面讲的链路) • video:提取关键帧供视觉模型分析 • document:保留原文件
处理完填充 MediaAttachment 的 localPath、mimeType、size、transcription 等字段。
出站(上传)
Agent 回复包含媒体时:先检查文件大小 vs 平台限制(Telegram 50MB、Discord 25MB、WhatsApp 16MB 音视频 / 100MB 文档)。超限先压缩(图片降分辨率、文档 zip),压缩后仍超限提示用户文件已保存到本地。
各平台上传方式不同——Telegram sendPhoto/sendDocument,Discord 附件上传,WhatsApp 需要先调接口获取 media_id 再引用发送。
临时文件保留 24 小时,每日 03:00 自动清理。
10. 重连与限流
10.1 重连策略
标准的指数退避加抖动:
retry_delay = min(base × 2^attempt + jitter, max)base: 1000msmax: 300000ms (5 分钟)jitter: random(0, 1000ms)attempt 0: ~1s → attempt 1: ~2s → attempt 2: ~4s → ... → attempt 8+: 5min 封顶成功连接后 attempt 归零。各 Channel 独立管理——Telegram 断了不影响 Discord。
10.2 限流控制
各平台限流参数差异大:
实现上用 Token Bucket 算法:
bucket = TokenBucket(rate, capacity)send(msg): if bucket.tryConsume(1): doSend(msg) // 有令牌,直接发 else: queue.push(msg) // 无令牌,排队 scheduleFlush() // 下次有令牌时发被平台返回 429(Rate Limited)时:解析 Retry-After 头,等待指定时间后重试,同时动态降低 Token Bucket 的填充速率。
这种东西不难,但不做的话你会在凌晨两点被报警叫醒。
11. DM 与群组策略
私聊(DM):所有消息直接交给 Agent,一个 chatId 对应一个 Session,无需 @mention 触发。权限等于用户配对时授予的级别。
群组(Group):触发条件四选一——@Bot mention、Slash 命令、引用回复 Bot 消息、Bot 参与的 Thread 内的消息。不会对群里每条消息都响应。
Session 策略(groupSessionMode):
"shared" | |
"per-user" |
群组权限(groupPermission):
"paired-only" | |
"admin-only" | |
"all-members" |
12. Webhook 部署
WhatsApp(以及 Telegram Webhook 模式)需要公网 HTTPS 端点。三种部署方案:
方案一:Nginx 反向代理(VPS 部署)
server { listen 443 ssl; server_name claw.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location /webhook/ { proxy_pass http://127.0.0.1:18789; proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; }}方案二:Cloudflare Tunnel(无需公网 IP)
cloudflared tunnel --url http://localhost:18789# 自动分配 xxx.trycloudflare.com → 设为 Webhook URL# 优点:无需域名/证书,本地即用# 缺点:免费模式 URL 不固定,需持久隧道配置方案三:ngrok(开发测试用)
ngrok http 18789# → https://xxx.ngrok-free.app → 设为 Webhook URL# 仅开发测试,不推荐生产13. 完整配置示例
一份典型的多渠道配置:
{ "channels": { "telegram": { "enabled":true, "mode": "long-poll", "token": "${TELEGRAM_BOT_TOKEN}", "allowedUpdates": ["message", "callback_query", "edited_message"], "groupSessionMode": "shared", "groupPermission": "paired-only", "streamStrategy": "edit-message", "streamThrottleMs": 500 }, "whatsapp": { "enabled":false, "mode": "cloud-api", "phoneNumberId": "${WA_PHONE_NUMBER_ID}", "accessToken": "${WA_ACCESS_TOKEN}", "verifyToken": "${WA_VERIFY_TOKEN}", "webhookPath": "/webhook/whatsapp" }, "discord": { "enabled":false, "token": "${DISCORD_BOT_TOKEN}", "intents": ["Guilds", "GuildMessages", "DirectMessages", "MessageContent"], "slashCommands":true, "streamStrategy": "edit-message", "streamThrottleMs": 800 }, "signal": { "enabled":false, "mode": "signal-cli", "signalCliPath": "/usr/local/bin/signal-cli", "account": "+1234567890" }, "voice": { "sttProvider": "openai", "localModel": "ggml-medium", "language": "auto" } }}注意 Telegram 和 Discord 的 streamThrottleMs 不同(500 vs 800)——因为两个平台的编辑频率限制不一样。Discord 更保守是因为它的 rate limit 更严。
14. 新增渠道开发指南
添加一个新 Channel(比如 Slack)的步骤:
1. 创建 src/channels/slack.ts,class SlackChannel implements Channel2. 实现消息标准化:Slack Event API → InternalMessage 映射,处理 text、blocks、files、threads、reactions 3. 实现回复格式化:OutboundMessage → Slack Block Kit 格式 4. 声明 Capabilities: maxTextLength: 40000,supportsMarkdown: true,supportsEditMessage: true,supportsThreads: true5. 实现流式回复:通过 chat.updateAPI 走编辑消息模式6. 注册到 Registry: channelRegistry.register("slack", SlackChannel)7. 添加配置 Schema: openclaw.json → channels.slack8. 跑测试清单:DM 收发 / 群组 @mention / Thread 回复 / 文件上传下载 / 流式回复编辑 / 重连恢复 / 限流处理
Channel 接口的抽象做得足够好,大部分工作量在理解目标平台 API 的脾气上。
15. 性能数据
消息标准化本身 5ms 可以忽略。延迟大头在网络 I/O——语音下载、STT 转写、跟平台 API 的交互。Webhook 模式相比长轮询延迟低一个数量级(100ms vs 1s),对响应体验敏感的场景值得配。
小结
渠道系统不是 OpenClaw 里最 Fancy 的部分——没有大模型推理,没有 RAG 检索,没有 Agent 编排。它做的事情很朴素:把不同平台的消息翻译成一种格式,再把回复翻译回去。
但这是整个系统里最容易被低估的一层。精力全砸到模型和 Prompt 上,渠道层随便写写——然后 MarkdownV2 转义没处理好消息全是乱码、WhatsApp 24h 窗口没考虑回复静默失败、流式回复没做节流直接被封号。这些都是实际会踩的坑。
OpenClaw 的渠道设计值得看的地方在于:一个干净的 Channel 接口 + 一个严格的 InternalMessage 格式 + 每个平台老老实实做适配。没有黑魔法,就是把该处理的边界情况处理干净了。想加新平台?实现接口、注册、测试,完事。
做基础设施的人不总能被看到,但他们决定了上层建筑能盖多高。
参考资料
1. OpenClaw 官方文档 - Channels 2. OpenClaw 源码仓库 3. Telegram Bot API 官方文档 4. Telegram Bot FAQ(限流、文件大小等) 5. Telegram Local Bot API Server 6. WhatsApp Cloud API 官方文档 7. WhatsApp Cloud API - Reaction Messages 8. WhatsApp Cloud API - Webhook 验证 9. Discord API 官方文档 10. Discord Rate Limits 11. Discord MESSAGE_CONTENT Privileged Intent FAQ 12. signal-cli GitHub 仓库 13. signal-cli JSON-RPC 接口文档 14. OpenAI Whisper API 定价 15. whisper.cpp - 本地语音转文字
夜雨聆风