同一套渠道网关,先接钉钉,再接飞书。两边都踩了坑,但真正需要改代码的只有两处,其余是环境、升级、权限和身份绑定的问题。
把过程记下来,给后面接入的人少走点弯路。
先定接入形态
钉钉只有 Stream 长连接,没有 Webhook 可选。飞书两种都支持:有公网可达的 HTTPS 地址就走 Webhook,没有就选长连接(ws)。
长连接的好处是没有入站连接,不需要公网 IP、域名备案和内网穿透,回调地址的 URL 校验、Encrypt Key、Verification Token 也都不用参与。代价是连接生命周期要自己维护,钉钉还有 3 秒 ACK 的限制,集群部署时只能单副本接收。
这些在渠道表里落到一列 mode,取值 webhook 或 ws,默认 webhook。钉钉不读这列,因为它恒为长连接。
钉钉:从建应用到群里说话
开放平台新建企业内部应用,记下 AppKey 和 AppSecret;开机器人能力,消息接收模式选 Stream(长连接),拿到 RobotCode。
平台侧在渠道管理里填平台类型、AppKey、AppSecret、RobotCode。钉钉不需要回调地址,保存后启动,日志里能看到:
ini
运行期装配已建立:channelCode=dd_robot, platform=dingtalk
渠道长连接已启动:channelCode=dd_robot
还差一步身份绑定。外部标识不是昵称,也不是手机号,而是钉钉侧的 userId。最省事的确认方式是让使用者先发一条消息,然后去日志里找 externalUserId,再把它绑到平台账号上。
做完这些,群里 @ 机器人或单聊都能收到回复。
钉钉排障记录
保存渠道时提示"后端接口连接异常"
填完参数点保存,前端弹了一句"后端接口连接异常"。第一反应是接口写错了,但先别急着看代码,确认后端还活着没有。
直接请求后端健康接口,超时;走 nginx 请求,502;看容器状态,后端一直停在启动中,日志里始终没有 "Started DromaraApplication"。
宿主机 16G 内存,同时跑着两套完整的容器编排栈,可用内存只剩 1.5G 左右。那段时间 WSL 和 Docker 守护进程反复重启,整栈被重建了好几次。
停掉其中一套栈,数据卷留着,另一套稳定下来之后接口立刻恢复。未登录请求返回的是正常的 {"code":401,"msg":"登录状态异常"},不再是 502。
能拿到业务响应,就说明网关和后端都健康了。后来再遇到"接口连接异常",先问三件事:后端启动成功了吗,主机资源够吗,网关指向对吗。大部分情况是后端压根没起来。
升级后启动报 column "mode" does not exist
渠道表新加了 mode 列。新库正常,有数据的旧库一启动就报错。
原因是建库脚本只在数据卷为空时首次执行,已有数据的库不会重跑,新列自然不存在。
补一条幂等的增列语句,两个方言都要。PostgreSQL 支持 add column if not exists,直接在原有建库脚本后面追加即可;MySQL 老版本没有这个语法,得先查 information_schema.columns,确认列不存在再执行 alter。两个版本的语句都要写进部署 README。
动了表结构,就要同时给存量库的升级脚本,并写进部署文档。"新库脚本会自动生效"是升级事故里最常见的一类误判。
机器人回复"当前账号尚未绑定平台用户"
渠道配好了,群里 @ 机器人,收到的不是回答,而是一句提示:当前账号尚未绑定平台用户,请联系管理员在「渠道接入 → 外部身份映射」中绑定后再试。
翻日志能看到一条 warn:
ini
渠道入站被拒(外部身份未映射):channelCode=dd_robot, externalUserId=xxxxxxxx
映射表里 0 条记录。这不是故障,是设计:外部标识必须显式绑定平台账号,没绑的一律拒绝,不回退默认身份。
去「外部身份映射」加一条就好。上线第一天这个提示一定会被当成 Bug 报上来,提前跟大家说清楚它是安全设计,比事后解释省事。
按手机号查标识返回 errcode=88
为了省事,我们把"按手机号查外部标识"做成了产品功能:渠道列表点图标,输手机号,查出标识并预填到绑定表单。
第一次真调,平台原样返回:
ini
errcode=88, subcode=60011
应用尚未开通所需的权限:[qyapi_get_member_by_mobile]
申请链接:https://open-dev.dingtalk.com/appscope/apply?content=xxxxx
链路是通的:登录、换 token、调接口都正常,只是平台侧权限没申请。按报文里的申请链接开通,管理员同意后重查,立刻命中:
ini
渠道外部用户标识查询命中:channelCode=dd_robot, platform=dingtalk, mobile=138****0000
这里有两件事值得坚持。第一,不要把平台的错误信息吞掉,errcode、subcode 和申请链接都原样透传,用户往往能自己解决。第二,手机号是敏感信息,日志里必须脱敏,接口也不要记操作日志。
长连接注册地址不跟 API 基址走
把 API 基址改成内网代理后,OpenAPI 调用都正常,长连接却一直连不上,日志里没有有效信息。
翻框架实现才看明白:OpenAPI 基址和长连接注册地址是两个独立字段,后者默认硬编码在公网默认地址上,不会跟着 api_base 走。
装配长连接时把注册地址显式拼出来:以配置的 API 基址为前缀,接上固定的长连接注册路径;API 基址留空时,才交给框架用默认地址。
内网代理、出口受限的环境,一定要确认长连接往哪建连。它的表现是"连不上但没有任何错误信息",属于最难查的一类问题。
图片消息:downloadUrl 被二次编码
钉钉图片消息进来后,先调 /v1.0/robot/messageFiles/download 用 downloadCode 换 downloadUrl,再下载文件。
这里踩过一次:
把下载地址直接交给 WebClient 的字符串 URI 方法,它会按 URL 模板再编码一遍,签名里的 %2F 变成 %252F,OSS 直接返回 403。改成先把下载地址原样构造为 URI,再发起请求就正常了。
另外,钉钉的报文类型比文档里列的杂。遇到没识别的消息类型,框架层会打 WARN 而不是静默丢弃,排障时这条日志很有用。
飞书:建应用、长连接、事件订阅
飞书新建企业自建应用(只有自建应用支持长连接),记下 App ID 和 App Secret;开机器人能力;事件订阅选长连接(如果用 ws),订阅 im.message.receive_v1。
权限按实际用到的能力开,至少要能收发消息、读取消息里的图片和文件资源。权限改完要发版生效,这一步很容易漏。
平台侧填 App ID、App Secret,mode 选 ws。长连接模式下,Encrypt Key、Verification Token、回调地址在界面上直接隐藏,免得有人以为还要配公网回调。
飞书的外部标识是 open_id,绑定方式和钉钉一样。
飞书排障记录
机器人说"只收到图片占位,没有可读的图片内容"
文字消息正常,一发图片就回复这句。第一反应是图片格式不支持,或者应用没有读图片的权限。
但这次日志里没有"权限不足",而是根本没有"渠道图片已装配"这条记录。再往前翻,容器日志里躺着:
arduino
org.springframework.core.io.buffer.DataBufferLimitException:
Exceeded limit on max bytes to buffer : 262144
262144 就是 256KB,Spring WebClient 的默认响应缓冲上限。那张飞书图片 337571 字节,下载到一半就失败了。
问题不在飞书,也不在模型。网关把下载失败降级成了 [image] 占位文本,模型只看到占位,自然说没有图片内容。
修法是给媒体下载单独放宽缓冲,统一到 16MB。飞书、企微、钉钉三条下载链路共用这套策略,普通 OpenAPI 请求仍用默认值。
下载通了,类型还得靠魔数
缓冲区修好之后还有第二个问题:飞书图片消息只给 image_key,没有文件名,也没有后缀。装配内容块时需要 MIME,按文件名猜是猜不出来的。
改成读字节头:
| 类型 | 文件头 |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
| WebP | 52 49 46 46 + WEBP |
| BMP | 42 4D |
| TIFF | 49 49 2A 00 / 4D 4D 00 2A |
| HEIC | 66 74 79 70 + heic / heix / mif1 |
识别不出来就 WARN,然后按原有降级路径回退成占位文本,不阻断回答。
修完后再发同一张图,日志里是:
ini
渠道图片已装配:channelCode=feishu_bot, mediaType=image/jpeg, bytes=337571
机器人这次读到了图片内容,能正常回答。
Webhook 模式的两件事
如果飞书走 Webhook,除了公网 HTTPS 地址,还要处理 URL 校验握手,并填 Encrypt Key 和 Verification Token。长连接模式下这些都用不上,两者不要混配。
消息幂等和防自环由网关统一做,按 message_id 去重、按渠道限流,不需要在平台后台额外配置。
链路跑通后核对这几处
钉钉和飞书都按同一套治理面核查:
| 看什么 | 正常表现 |
|---|---|
| 渠道来源会话 | 生成 source=channel 的会话 |
| 外部线程映射 | 群聊或单聊线程指向该会话 |
| 消息 | 多轮问答都在,工具调用类问题也能走通 |
| 长连接状态 | 日志有"运行期装配已建立"和"渠道长连接已启动" |
| 身份映射 | 外部标识绑到了平台账号 |
| 图片 | 有"渠道图片已装配"日志,bytes 与源文件一致 |
从入站、身份、会话,到无人值守执行、出站回复,整条链路才算跑通。
回头再看
这几个问题里,真正改代码的只有两处:长连接注册地址,以及媒体下载的缓冲和 MIME 识别。其余都是环境资源、建库升级、安全设计、平台权限。
写渠道接入,代码只是其中一部分。剩下的是把部署、升级、身份和平台配置都提前想到,以及在出问题时能先分层判断:后端起来没有,网关通不通,平台侧权限开了没有,日志里有没有降级记录。
平台的错误信息尽量原样透传,手机号这类敏感字段记得脱敏。这两条在排障时能省掉很多来回。
关于 BizBuddy
BizBuddy 是一个全量开源的企业级智能体平台(MIT License):RuoYi-Vue-Plus(Spring Boot 4.1.1 / JDK 21)做底座,AgentScope Java Harness 做智能体内核,纯 Java 全栈、不含 Python,权限、审计、配额、多渠道接入等能力全部随仓库开源。
欢迎 Star、Issue 与共建。