9 个 IM 渠道,怎么收敛成一套接入接口
接入 IM 机器人,最难的不是”接一个渠道”,而是”接很多渠道”。当你把飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord、WhatsApp 这 9 个渠道都接进来时,你会发现:每一个渠道都在用不同的方式表达”同一件事”。
我在做接入多端 IM 的项目(dsh-im),支持 9 个渠道,有扫码和凭据两种接入模式。这篇讲讲”多渠道”背后真正难的地方,和我是怎么收敛的。
难点:渠道差异是”漏出来的”
你可能会想:不都是收发消息吗?抽象一层不就行了?问题在于抽象层一旦封不好,渠道差异就会从缝隙里漏出来,而且漏得防不胜防。
举几个真实会踩的点:
- 消息体的结构完全不同:有的渠道把文本和图片分字段,有的塞在一个
content里,有的用数组; - 发送失败/成功的判定不一样:有的渠道失败会抛错,有的失败也返回 200,靠响应体里的
code判定——这正是很多”请求成功但业务失败”问题的来源; - 回调/事件格式不一样:webhook 的签名、验签、重试次数各不相同,有的是明文,有的是 JSON,有的是 form;
- 文件、图片上传的方式和限制也不同。
所以真正要做的是建一层能”吃掉这些差异”的适配层,而不是一个看起来很整齐、实则到处破洞的接口。
收敛思路:一套接口 + 每渠道一个适配器
我把它拆成两层:
- 对外稳定的一套接口:无论哪个渠道,接入方都用同一套语义。比如”发一条文本”“发一张图”“处理一条入站消息”。上层只看这一层,不用关心底下是飞书还是 WhatsApp。
- 每渠道一个适配器:把”这个渠道的请求格式 / 响应判定 / 回调事件”,在适配器内部翻译成统一语义。
这样做的价值是:新增一个渠道 = 新增一个适配器,而不是改动上层逻辑。上层稳定,渠道越多也越稳。
别让”懒”,把接口做成渠道的样子
最常见的坏味道是:接口里开始出现渠道专属的参数,比如 if (platform === 'wechat') { ... } 散落各处,或者某个字段叫 zhihu_xxx。
这其实是抽象漏了——你把”某个渠道的特殊性”暴露给了上层。正确做法是把特殊性留在适配器内部,上层拿到的是已经归一化的结果。判断标准很简单:上层代码里如果出现渠道名,就该回适配器里改。
两种接入模式:扫码 vs 凭据
除了消息格式,接入方式也不止一种。有的渠道你用扫码建 session,有的要应用凭据(app_id / secret)。
这两种模式也不能在接口层混在一起。更合适的是把它们当成”两种初始化/鉴权路径”,最终都收敛到”一个可收发消息的会话”这个统一概念。这样上层不关心你是扫码还是凭据进来的,只关心”我有没有一个能用的会话”。
“状态码不等于结果”的复现
很多渠道失败也回 200,真正结论在响应体里。所以适配器里必须要做业务码判定,并且回执要脱敏——不能把敏感字段原样抛给上层。相关的心跳、回执、失败用例,都需要在适配层统一处理,否则上层就得为每个渠道写一次。
最后几条
- 接入多渠道,真正的成本在差异收敛,不在”接一个”。
- 建一套稳定的接口 + 每渠道一个适配器,让新增渠道变成”加适配器”。
- 别让渠道命渗透到上层;出现
if (platform === ...)就该回适配器改。 - “失败也返回 200”的判定、业务码、脱敏回执,都要在适配层做掉。
- 扫码和凭据是”鉴权路径”,收敛到”一个能收发的会话”。
把差异吃得越干净,上层就越简单;而干净,是靠把复杂度留在适配器里换来的。