这篇文章不讨论模型效果,只看一个更具体的问题:一个 Java 项目把飞书接入到 Agent 系统时,需要处理哪些工程细节。
飞书通道看起来只是"收消息、回消息",但真正接入后会遇到很多边界情况:长连接会断、事件会回放、群聊要不要 @ 才响应、文件和图片只有 file_key 或 image_key、语音没有现成文本、长回复直接发文本体验不好、生成文件不能只给一个内部链接,写操作也不能默认自动执行。
MateClaw 近期围绕飞书渠道做了一组实现,比较适合作为企业 IM 接入 Agent 的源码案例来看。

一、整体链路
飞书消息进入 MateClaw 后,大致会经过四步。
第一步是通道接入。飞书可以走 WebSocket 长连接,也可以走 Event Subscription Webhook。WebSocket 模式减少了公网回调配置成本;Webhook 模式则要求配置 encrypt_key,否则启动时直接拒绝,避免未认证回调触发消息处理。
第二步是消息归一。飞书原始事件会被解析成统一的通道消息,包括发送人、群聊、消息 ID、文本内容和多模态 content parts。
第三步是 Agent 执行。归一后的消息交给后续路由和 Agent 运行时处理。如果中间触发工具调用,还会进入工具权限和审批链路。
第四步是飞书侧输出。普通文本直接发送,长内容可以走 CardKit 流式卡片,生成文件会转成飞书原生附件,高风险工具调用会生成审批卡片。

二、长连接稳定性:重点不是"能连上",而是"长期能收消息"
飞书 WebSocket 接入里,比较容易忽略的是长期运行稳定性。
MateClaw 的飞书适配里做了几类处理:
- 启动时刷新
tenant_access_token,并设置定时刷新; - WebSocket 连接由适配器自己控制重连,不直接依赖 SDK 自动重连;
- 注册多个 IM 事件的空处理器,避免未处理事件触发
HandlerNotFoundException; - 记录最近事件时间,用静默断连看门狗发现"连接还在但不收事件"的情况;
- 根据消息创建时间过滤旧事件,避免重连后重复处理过期消息;
- 关闭 WebSocket 时调用 SDK 的
close(),释放连接、ping 循环和线程池。
这些处理更偏工程稳定性,不属于模型能力,但在企业 IM 场景里很重要。通道层如果偶发失联或重复执行,后面的 Agent 再强也会被放大成系统问题。
三、群聊过滤:require_mention 不能靠字符串猜
群聊里通常不希望机器人看到每句话都响应。飞书通道提供了 require_mention 配置,用于控制群聊中是否必须 @机器人 才处理。
这里的实现没有靠文本前缀判断,而是读取飞书事件里的 mentions 字段,并提前获取 bot 自己的 open_id。如果获取 bot 身份失败,会进入短时间失败缓存,避免每条群消息都同步请求一次接口。
这个细节说明一个问题:IM 通道接入不能只看消息正文。消息元数据、发送人、群聊类型、mentions 字段,都会影响后续路由。
四、文件和图片:不能只把 file_key 交给模型
飞书文件、图片、音频、视频消息通常包含资源 key。这个 key 对模型没有直接意义,后续工具也未必能直接访问飞书 CDN。
MateClaw 的处理方式是:在通道层下载资源,把下载结果写入 MessageContentPart,包括本地文件路径、文件名、content type,以及面向前端展示的临时文件 URL。
这样做的好处是后续 Agent 不再只看到 [文件] 或 [图片] 这种占位符,而是能拿到可读的文件对象。图片可以交给视觉模型,文档可以交给文件读取工具,视频和音频也能保留为后续处理材料。
五、最近文件缓存:解决"帮我看刚才那个文件"
企业 IM 里经常出现这种对话:
用户先上传一个文件
随后补一句:帮我总结一下
如果系统只处理当前这一条文本消息,Agent 会不知道"刚才那个文件"是什么。
飞书通道里有一层最近文件缓存。文件消息进入后,会按会话缓存最近几个文件,并设置过期时间。后续同一会话的文本消息进来时,适配器会把最近文件注入到当前 content parts 里。
这不是复杂算法,但很贴近日常使用习惯。它把"人类自然表达里的上下文"补成了机器可处理的上下文。
六、语音消息:先下载,再走 STT
飞书语音消息和部分其他 IM 渠道不同,事件里通常没有直接给出 ASR 文本。适配器拿到的是音频资源信息。
MateClaw 的实现是先下载音频文件,再调用 SttService 转写。转写成功后,会把文本作为一个普通 text part 插到音频 part 前面。这样后续 prompt 构建时,Agent 看到的是语音内容,而不是只有 [音频]。
同时这一步是 best-effort:如果 STT 没有配置、下载失败或转写失败,消息仍然可以继续流转,只是退化成音频占位。
七、输出侧:流式卡片和原生附件
普通 IM 机器人经常把长回答一次性发出去,或者把生成文件变成一个内部链接。这两种方式在飞书里都不够自然。
MateClaw 飞书通道做了两类输出适配。
第一类是 CardKit 流式卡片。长回复生成时,先创建一个 streaming card,然后持续 append 内容,最后关闭 streaming 状态。这样用户看到的是飞书卡片内逐步更新的内容。
第二类是生成文件回传。适配器会识别回答里的生成文件 URL,通过 GeneratedFileScrubber 取到文件字节,再用飞书媒体上传能力发成原生附件。卡片或文本里则保留文件名提示,而不是直接暴露内部下载地址。

八、飞书原生工具:从消息通道走向工具通道
飞书通道不只是消息收发,也可以把飞书 OpenAPI 暴露为 Agent 工具。
当前飞书工具提供器里可以看到几个代表性工具:
feishu_calendar_list_events:读取日历事件;feishu_doc_read:读取飞书文档纯文本;feishu_doc_create:创建飞书文档。
其中读取类工具默认可用,写入类工具默认关闭,并会被标记为需要更高等级管控。工具输入输出走 JSON,便于模型后续分支处理,也便于系统做审计。

九、ToolGuard 审批卡片
Agent 能调用工具后,权限问题会立刻出现。
例如创建飞书文档、发送消息、修改配置这类操作,不能简单等同于模型"回复一句话"。MateClaw 的处理方式是让高风险工具调用进入 ToolGuard,再由飞书卡片承载审批动作。
审批卡片里会展示工具名、风险等级、参数摘要,并提供批准和拒绝按钮。用户点击后,卡片事件会回到后端处理,审批完成后卡片状态也会更新。
这里的价值不在于按钮本身,而在于把"工具调用"变成了一个可暂停、可确认、可追踪的运行流程。

十、源码里能看到的分层
从模块职责看,飞书接入没有全部堆在一个方法里,而是拆成了几层:
- 飞书适配层:负责事件接入、消息解析、媒体下载、消息发送;
- 卡片层:负责普通卡片、流式卡片、审批卡片;
- 媒体层:负责上传、大小策略、生成文件清理;
- 工具层:负责把飞书日历、文档等能力注册为 Agent 工具;
- 审批层:负责写操作和高风险工具调用的人类确认。
这种拆法的好处是:消息通道、工具通道、审批通道可以各自演进。后续要扩展企微、钉钉、Slack,也可以复用其中一部分通道抽象。
十一、总结
飞书渠道适配看上去是一个"IM 机器人"问题,实际涉及连接稳定性、事件去重、群聊过滤、多模态资源下载、语音转写、最近文件上下文、流式卡片、原生附件、工具注册和审批治理。
MateClaw 近期这组实现的特点,是把这些问题放在同一条运行链路里处理:消息进来时尽量还原上下文,Agent 执行时保留工具边界,结果返回时尽量使用飞书原生形态。
如果只做问答机器人,很多细节可以省掉;但如果要让 Agent 进入企业协作系统,这些通道层细节迟早都要补上。