前两篇讲的是 BizBuddy 渠道网关的架构和实现,这篇讲落地。
写这篇的起因是:真正接渠道时,代码很快就写完了,耗时间的是落地那几天------建库脚本没跑、权限没开、身份没绑、容器栈把主机压垮......每一样都能让联调停半天。
BizBuddy 是通用底座,不直接面向最终客户,通常作为二次开发的基础平台。所以下面的经验面向基于它做二次开发、交付和运维的团队,内容偏向"照做",能直接当接入手册用;有些地方我会说明白"这一步不做会报什么错"。
一、先定选型,别急着写代码
选型就三个问题,问完基本就定了:
| 问什么 | 怎么答 | 落到哪儿 |
|---|---|---|
| 部署环境有公网可达地址吗? | 没有 | 只能挑有长连接形态的平台(钉钉,或飞书选 ws) |
| 是企业自建应用吗? | 不是 | 飞书长连接用不了,只能 Webhook |
| 用户需要手机端随时找到 AI 吗? | 需要 | 优先钉钉 / 企微 / 飞书,别自建网页入口 |
结论很干脆:只有钉钉稳吃长连接(免公网),飞书可选长连接或 Webhook,企业微信和 GitHub 只有 Webhook,必须有公网可达地址。
所以"没有公网 IP 怎么接渠道"这个问题,标准答案是钉钉,或者飞书选长连接形态。
一个容易忽略的差异:钉钉的长连接帧是 JSON,可以零依赖自己实现;飞书的长连接是私有 protobuf 协议,只能引入官方 SDK。选型时如果在意依赖体积,这也是一个考虑项。
二、数据模型:一列决定形态
渠道表只加一列 mode,取值 webhook 或 ws,默认 webhook。钉钉不读这列(它恒为长连接),这样老数据不用迁移。
真正容易翻车的不是加列本身,而是建库脚本只在数据卷为空时首次执行 。已经有数据的库不会重跑脚本,升级后启动会直接报 column "mode" does not exist。
所以升级须知里必须带幂等增列:PostgreSQL 用 add column if not exists;MySQL 老版本没有这个语法,需要先查 information_schema.columns 再决定是否执行 alter。两种方言都要给,并且把这段直接写进 deploy/README.md,否则每次发版都会有人再踩一遍。
三、平台后台要配什么
钉钉(走长连接,免公网)
- 开放平台 → 新建企业内部应用
- 记下 AppKey / AppSecret
- 开机器人 能力,消息接收模式选 Stream(长连接)
- 用到什么权限就申请什么(比如按手机号查成员,要
qyapi_get_member_by_mobile) - 发布上线,把机器人拉进群或开启单聊
飞书(走长连接)
- 新建企业自建应用(只有自建应用支持长连接)
- 记下 App ID / App Secret
- 事件订阅方式选长连接
- 订阅事件
im.message.receive_v1 - 如果要用按手机号查标识的功能,另外申请
contact:user.id:readonly一类 scope,并且发版生效
飞书 / 企微 / GitHub(走 Webhook)
- 准备好公网可达的 HTTPS 回调地址
- 飞书:填 Encrypt Key 和 Verification Token,处理好 URL 校验握手
- 企微:填
EncodingAESKey和回调 Token - GitHub:填 Webhook Secret(HMAC-SHA256 验签)
四、平台侧配置:字段就这些
同一张表单按平台切换,能复用的列尽量复用:
| 表单字段 | 落库列 | 钉钉 | 飞书 | 企微 | GitHub |
|---|---|---|---|---|---|
| 应用标识 | app_id |
AppKey | App ID | CorpID | --- |
| 应用密钥 | app_secret |
AppSecret | App Secret | Secret | --- |
| 机器人 / 应用 ID | platform_agent_id |
RobotCode | --- | AgentId | Bot 登录名 |
| 回调 Token | verification_token |
--- | Verification Token | Token | --- |
| 加密密钥 | encrypt_key |
--- | Encrypt Key | EncodingAESKey | --- |
| 签名密钥 | webhook_secret |
--- | --- | --- | Webhook Secret |
| API Token | api_token |
--- | --- | --- | Personal Token |
| API 基址 | api_base |
默认官方 | 长连接 domain | 默认官方 | 默认官方 |
| 接入形态 | mode |
不读 | 必选 | 不读 | 不读 |
界面上有几个细节值得照做,都是被问出来的:
飞书选长连接 时,把 Encrypt Key、Verification Token、回调地址直接隐藏,并写一句"无需公网 IP 与回调地址;后台事件订阅须选『长连接』"。用户不用翻文档就知道该干什么。
长连接形态下,api_base 的说明也从"OpenAPI 基址"改成"长连接 domain,留空用官方默认"。不然总有人以为填个内网地址就行。
列表里的"回调地址"列和"连接状态"列,只对长连接形态(含恒长连接的钉钉)显示。
凭据一律密文落库,页面和接口都不回显明文,只在运行期装配时解密。
五、配完不等于能对话:两张映射
渠道配置保存成功,只是说明凭据能用。真正要能对话,还得配两张映射。
外部身份映射。 外部标识绑平台账号。没绑的一律拒绝,绝不回退默认身份,日志长这样:
ini
渠道入站被拒(外部身份未映射):channelCode=xxx, externalUserId=xxx
这是刻意的安全边界:外部消息的能力,就是被绑账号的权限。每个要用机器人的同事各绑一条。
顺手做了个小功能省事:渠道列表行内一个图标,输手机号查出标识,一键预填到绑定表单,平台账号还是管理员自己选。
企业微信和 GitHub 不支持这个查询------企微那个接口需要通讯录同步的独立 secret,渠道表里只存了应用 secret;GitHub 压根没有手机号这个概念。所以这两个平台的界面上不显示这个图标。能力边界如实暴露,比做成"看起来能用但一点就报错"好。
会话映射。 外部线程绑到平台会话,可以续接;会话结束之后再发消息自动新建并重绑。
六、实战里最容易翻车的几个点
这一节都是上线后真撞出来的,比接口文档更值得先看。
1. 文本能回,图片不回:群聊和单聊的报文不一样
我们第一次遇到"钉钉发文字正常、发图片完全没反应",排查半天以为是下载权限问题。实际原因是钉钉群聊图片不走 picture 类型,而是 richText :正文是一个数组,文本项和图片项混在一起,图片项带 downloadCode。
框架自带的报文映射只认 picture,遇到 richText 就当成未知消息丢掉了,所以表现成"消息没收到"。修复并不复杂:把 richText 里的文本项合并为正文,带 downloadCode 的图片项转成图片内容块,再交给后续的多模态链路。
这里最值得记住的一条经验是:群聊和单聊要用两套报文分别验收 。只测单聊图片(picture),永远不会发现群聊(richText)的问题。修复后我们群聊、单聊各发一张,下载大小分别约 133 KB 和 144 KB,都能按图面内容正确回答。
另外,遇到不认识的入站报文不要静默丢弃。把 msgtype 和字段形状打一条 WARN,既不落正文也不落下载码,下次再遇到新形态至少能一眼定位。
2. 图片下载 403:签名链接别再编码一次
钉钉返回的媒体地址是带签名的 OSS 链接,URL 里本来就有 %2F 这类转义。如果经过 HTTP 客户端再编码一次,会变成 %252F,签名直接对不上,结果是 403。
这类问题日志里通常只看到"下载失败",很容易误判成权限或网络问题。处理原则很简单:签名 URL 原样使用,不要再拼参数、不要再编码。验收时直接看下载字节数,比看 HTTP 状态码更可靠。
3. 长连接:单副本,还要单独配注册地址
长连接是"谁连上谁收消息",多副本会重复消费,所以按单副本接收部署。Webhook 形态可以多副本,但幂等必须做好(同一条消息重复投递只处理一次)。
另一个隐蔽的坑:长连接注册地址不跟 api_base 走。有人把 API 基址改成内网地址,以为长连接也会跟着走,结果一直连不上,日志还没什么信息。需要显式派生注册地址。
4. 媒体能力边界要如实写出来
入站支持文本、图片和可解析文档;语音和视频只使用平台侧转写结果;钉钉群聊不支持语音、文件和视频;@提及目前不单独解析。
这些不是"以后再说"的待办,而是上线前就要和业务方对齐的边界。能力边界写清楚,比让用户点了之后报错要好得多。
5. 新 jar 可能被生产配置自检拦住
平台有启动自检:JWT 秘钥、模型加密根、数据库 / Redis / Milvus 口令、SnailJob 令牌等如果还是出厂示例值,会直接拒绝启动。
本地开发栈如果确实要沿用出厂值,需要在 deploy/.env 里把自检模式设为 warn(仅非 prod 生效)。我们这次重建 MySQL 栈时就遇到了:老镜像跑得好好的,换新 jar 后起不来,日志第一行就是"配置收口自检未通过"。换镜像不等于换配置,升级前先看自检清单。
6. 别同时跑多套完整栈
本机同时跑 PG 和 MySQL 两套完整栈(数据库、Redis、Milvus、MinIO、SnailJob、后端、前端),资源不够时最先倒下的是后端。前端表现是"后端接口连接异常",很容易被当成接口 Bug 查半天。
上线验收时只保留一套栈;确实要并存,先确认内存和端口,别让容器互相抢资源。
七、上线检查表
照着打勾就行。
前置
- 部署形态定了(没公网就钉钉 / 飞书
ws) - 平台后台:应用建好、能力开通、事件订阅、已发布上线
- 平台后台:需要的权限都申请通过了(长连接形态不用配回调地址校验)
部署
mode列已建,存量库也执行了幂等增列- 长连接形态按单副本接收部署
- 主机和容器资源够用------别同时跑多套完整栈
- 生产配置自检通过(或本地开发显式设为
warn)
配置
- 凭据密文落库,页面不回显明文
- 渠道列表的连接状态 = 运行中(失败的话点开看原因)
- 身份映射至少配了管理员本人一条
验收
- 在 IM 里 @ 机器人能拿到回答(不是"未绑定"提示)
- 群聊和单聊各发一条文本、各发一张图片,都能正常回答
- 平台侧能查到该渠道来源的会话和工具调用留痕
- 同一条消息重复投递只处理一次
- 写类工具在渠道链路上被拒绝(无人值守,不该让 AI 自己动数据)
八、报错对照表
下面这些报错都是我们自己撞过的,按现象查就行。
| 现象 | 真正的原因 | 怎么办 |
|---|---|---|
| 文本正常、图片无回复 | 群聊图片是 richText,未被报文映射识别 |
归一化 richText(文本合并 + downloadCode 转图片块),群聊/单聊分别验收 |
| 图片下载 403 | 签名链接被二次编码(%2F → %252F) |
签名 URL 原样使用,验收看下载字节数 |
| 前端提示后端接口连接异常 | 后端压根没起来(资源不足、多套栈把主机压垮)------不是接口 Bug | 看后端日志有没有"启动成功",停掉多余的容器栈,再查网关指向 |
| 启动报"配置收口自检未通过" | 口令 / 秘钥仍为出厂示例值 | 替换配置;本地开发在 .env 设 BIZBUDDY_SECURITY_GUARD_MODE=warn |
column "mode" does not exist |
存量库没执行增列,建库脚本只在空卷跑 | 照升级须知执行幂等增列 |
| 提示当前账号尚未绑定平台用户 | 不是故障,是身份没绑定的安全拦截 | 去"外部身份映射"把标识绑到平台账号 |
钉钉报 errcode=88 / subcode=60011,应用尚未开通权限 |
平台侧权限没申请 | 报文里带了一键申请链接,开通后重试(错误我们原样透传,就是想让你能自助解决) |
| 连接状态显示启动失败 | 凭据错、出网受限、后台没选长连接模式 | 点开看失败原因,逐项核对 |
| 访问返回 502 | 网关指向了旧的后端容器 IP(后端重建后 IP 变了) | 重启网关容器,让它重新解析 |
| 长连接一直连不上,日志也没信息 | 注册地址走了默认公网地址(它不跟 API 基址走) | 显式派生长连接注册地址 |
九、最后
把 IM 接入做成企业级 Harness 平台的一部分,关键不在调通某个接口,而在这几件事:形态选对(它决定内网能不能落地)、凭据和权限收敛好(密文落库、最小授权、能力边界如实暴露)、身份和会话显式映射(这是可控的前提,不是可选项)、无人值守链路收口(只读放开、写操作拒绝)、运维看得见(连接状态、幂等、留痕)。
关于 BizBuddy
BizBuddy 是一个全量开源的企业级 Harness 平台 (MIT License):RuoYi-Vue-Plus(Spring Boot 4.1.1 / JDK 21)做底座,AgentScope Java Harness 做智能体内核,纯 Java 全栈、不含 Python,权限、审计、配额、多渠道接入等能力全部随仓库开源。本文讲的渠道网关核心类在 core/channel 包,架构与限制口径在仓库 docs/ 下。
欢迎 Star、Issue 与共建。