飞书机器人开发实战:用lark-harness快速构建企业级消息应用
1. 为什么需要lark-harness企业级机器人开发的真实痛点先聊点背景。我在企业协作平台上做过好几个机器人应用从最早一个人接私活式的“脚本机器人”到后来正经八百给几十人团队用的“值班机器人”“告警机器人”再到跨部门协同的“数据播报机器人”。这中间踩过的坑基本都集中在同一个地方飞书开放平台本身的东西其实不难难的是把机器人做成一个能长期维护、能上生产环境的工程。所谓“企业级机器人”不是说功能多炫而是它得满足这样几个硬性条件稳定收发消息、事件不丢不重、权限管控清晰、密钥安全可控、日志可查、出问题能快速定位。这些要求叠加在一起如果每次都是从零手写大量的时间会花在那些跟业务毫不相关的“脏活”上。1.1 飞书机器人的典型开发场景飞书机器人在实际企业环境里的用法我总结下来无非是这么几类。第一类是通知推送型。比如监控告警、发布结果通知、日报周报推送、工单状态变更提醒。这类机器人的核心能力是“往指定的群或个人发消息”要求发送可靠最好还能带上格式化的卡片。第二类是交互处理型。比如机器人作为一个入口用户可以在群里 它或者给它发私聊消息它返回查询结果或执行指令。典型场景是查排班、查订单、提交审批、查SQL执行结果。这类机器人需要处理消息事件要做指令解析和权限校验。第三类是工作流嵌入型。飞书自己的多维表格、审批、云文档都能跟机器人联动通过订阅事件实现自动化。比如多维表格里记录变更时就触发机器人发一条通知到群里。这类机器人要求对飞书的各类事件模型有深入理解。在这三类场景里工程上的共性问题非常明显都需要一个常驻服务都需要处理飞书开放平台的事件回调或长连接都需要管理访问凭证都需要处理签名校验和消息加解密。1.2 不套脚手架直接开发会踩哪些坑我先说说裸开发时最让人头疼的几个问题这些也是我在多个项目里反复踩过的。事件回调的方式很麻烦。如果不用长连接就要提供一个公网可访问的 HTTP 接口给飞书回调这意味着你得有公网IP、域名、HTTPS证书开发阶段还得配合内网穿透工具把本地服务暴露出去。一旦回调地址配错、签名校验不对、加解密失败排查起来非常费劲。消息收发链路上的细节特别多。飞书服务端要求请求头里带时间戳和签名消息内容有固定格式机器人发消息需要 tenant_access_token不同消息类型对应的 JSON 结构差别很大。很多同学第一次发文本消息成功后信心满满地去发卡片消息结果格式不正确卡片完全渲染不出来。再一个就是开发环境的调试体验。飞书开放平台通常不太支持本地事件的离线模拟每次测试都要真实发一条消息或触发一个事件。如果业务逻辑里还涉及回调、交互卡片按钮、快捷指令调试链路就更长了。1.3 lark-harness是怎么定位的lark-harness 这个脚手架就是为了解决上面这些问题而生的。我第一次接触到它的时候最大的感受是它把“跟飞书开放平台打交道”的那部分工程能力做成了标准件让开发者只专注于业务本身。它的定位不是一个“低代码平台”也不是一个“IM 中间件”。它更接近于一个开发骨架加工具集项目初始化、事件订阅与管理、长连接与回调双模式、消息发送封装、凭证自动管理、签名与加解密、本地调试工具、日志与错误处理这些能力开箱即用。我在实际使用中从git clone一个仓库到能本地跑通一个能响应 消息的机器人大概只花了十几分钟。这个效率在以前裸开发的时候是不可想象的光是把回调地址用穿透工具暴露出去、校验飞书服务端签名、处理好事件重试就够折腾一阵了。这篇博文我就从原理、实操到排障把用 lark-harness 从零构建一个企业级飞书机器人的完整过程写清楚包括每一步的选型理由和一些容易踩坑的地方。2. 核心架构原理解读lark-harness 帮我们做了什么很多脚手架类工具有个通病用起来方便但出了问题就抓瞎因为你不知道它背后帮你把事情做成什么样了。所以用 lark-harness 之前我建议还是先把它的核心机制搞明白。2.1 两类事件接入方式长连接与回调飞书开放平台给开发者提供了两种接收事件的方式lark-harness 对这两种方式都做了适配。一种是 HTTP 回调模式。你在开放平台配置一个回调地址飞书服务端在发生事件时向这个地址发一个 POST 请求。这种模式适合部署在具备公网入口的生产环境里比如 Kubernetes 集群暴露一个 Ingress。另一种是长连接模式。这是飞书后来推出的“长连接”接入方案应用通过 WebSocket 与飞书服务端建立一条长连接飞书服务端通过这条连接把事件主动推给应用不再需要公网回调地址。我用一张对比的视角来看这两种模式的取舍维度HTTP 回调模式长连接模式公网入口需要要配置域名和证书不需要网络策略需要开放 Ingress防火墙规则多只需服务端能访问飞书长连接域名开发调试需要穿透工具链路长本地起服务直接连体验极好生产部署适合大型网关场景适合大多数常规微服务运维复杂度较高需要关注回调超时与重试较低连接状态由 SDK 维护在实际项目中怎么选我的建议是开发环境和中小型项目优先用长连接如果公司有统一的 API 网关希望所有外部回调都走同一个 HTTP 入口做审计那就用回调模式。lark-harness 在这两种模式下采用同样的事件处理逻辑切换起来非常平滑。2.2 消息收发链路拆解无论哪种接入方式机器人处理一条消息的链路本质上是一样的分两个方向。入站方向也就是机器人收到用户消息。这条链路是这样的用户在群里 机器人或私聊机器人飞书服务端把im.message.receive_v1事件推给应用应用收到事件后做签名校验和解密得到明文消息体然后根据消息内容进行指令解析和路由分发最终把处理结果交给业务逻辑。出站方向也就是机器人主动或被动地回复消息。这条链路是业务逻辑组织好消息内容调用飞科消息发送 API带上访问凭证指定接收对象和消息类型飞书服务端把消息推给目标用户或群。lark-harness 在入站方向上做了一个很有意思的设计它把“事件处理”抽象成一个个 ceHandler开发者只需要关注消息内容本身不用关心签名校验、解密、事件去重。在出站方向上它封装了消息发送的公共参数和错误重试开发者传一个对象就能发消息。我想特别提一个细节事件去重。飞书在消息事件推送时由于网络原因可能会出现重复推送如果你的业务逻辑里有扣款、建单这类幂等性敏感的操作一定要在事件层做去重。lark-harness 在事件入口层做了基于事件 ID 的本地去重生产环境建议再接入外部存储做分布式去重。2.3 Token管理为什么是重中之重飞书开放平台的 API 调用有两种凭证我见过不少初学者把这两者搞混。tenant_access_token是应用身份凭证代表“这个应用本身”。机器人发消息、读取应用可见范围内的信息用的都是它。它的有效期通常是 2 小时。user_access_token是用户身份凭证代表“某个用户授权了应用”。需要读取用户个人数据、以用户身份执行操作时比如读取用户的日程、以用户名义发消息用的就是它。它的有效期更短而且需要走 OAuth 授权流程来获取。lark-harness 内置了令牌管理模块会自动处理tenant_access_token的获取、缓存、过期刷新和并发请求时的等待。这个能力看起来不起眼但在实际生产中非常重要。说说为什么。如果每次发消息都向飞书重新请求 token一方面增加了网络开销更关键的是当应用流量上来以后瞬间的大量请求可能触发飞书的频率限制。而如果不做 token 缓存每 2 小时过期后如果多个请求同时发现 token 过期它们会同时去刷新 token产生“惊群”问题。我在早期做机器人时就吃过这个亏。某个早上机器人推送一条批量消息触发了大量并发调用token 刚好过期结果所有请求同时去刷新导致部分请求拿到老 token、部分请求拿到新 token最后一批消息发送失败。用 lark-harness 之后再没遇到这个问题因为它在内部做了互斥的 token 刷新机制。3. 从零搭建一个机器人完整实操过程下面直接进入实操环节。我以一次完整的项目搭建为例从环境准备、应用创建、代码编写到本地启动整个过程尽量按实际开发顺序来写。3.1 环境准备与项目初始化开发环境我用的是 macOS Node.jslark-harness 对 Node 版本要求是 18 以上建议直接上 20 LTS。它本身是 TypeScript 写的脚手架工具会自动生成 TS 工程。第一步执行初始化命令。如果你习惯用 pnpm也可以把 npx 换成 pnpm dlx。npx create-lark-harnesslatest my-robot cd my-robot npm install npm run dev跑完初始化之后目录结构大概是这样的。我挑关键文件说一下my-robot/ ├── src/ │ ├── conf/ │ │ └── config.ts # 全局配置包括应用凭证、端口等 │ ├── handlers/ │ │ ├── im.message.receive_v1.ts # 消息接收事件处理 │ │ └── card.action.trigger.ts # 卡片回调事件处理 │ ├── services/ │ │ └── message.ts # 业务逻辑层消息构造和发送 │ ├── utils/ │ │ └── logger.ts # 日志封装 │ └── index.ts # 应用入口 ├── .env.example # 环境变量示例 ├── package.json └── tsconfig.json这里我多说一句conf/config.ts。lark-harness 默认会把应用凭证等信息放在环境变量里.env.example文件里列出了所有需要的变量名。这种做法的好处是代码仓库里不会出现明文密钥不同环境用不同的.env文件即可。3.2 在飞书开放平台创建应用这一步我强烈建议严格按照顺序来很多同学出问题都是因为跳步。打开飞书开放平台后台创建企业自建应用。拿到三个关键凭证App ID、App Secret、Encrypt Key。其中 App Secret 只显示一次配置到环境变量里后建议立刻保存到公司的密码管理工具中。接下来在应用功能里开启“机器人”能力这一步决定了应用是否有机器人身份。然后配置事件订阅这里有两种情况如果用长连接模式在“事件订阅”页面选择“使用长连接接收事件”然后添加你需要的“事件”和“权限”。如果用回调模式则需要配置“请求地址 URL”这个 URL 就是我们服务暴露出来的回调接口。这里要特别强调一下权限。飞书的事件订阅和 API 调用是两套体系但很多功能需要同时开通“事件”和“权限”才能正常工作。比如机器人要实现“接收群聊中 机器人的消息”需要开通im:message.receive_v1事件同时还要开通im:message或im:message:send_as_bot权限否则即使事件收到了也无法调用发送消息的 API。还有一个非常容易忽略的点修改权限后需要发布应用版本。开发阶段虽然可以在测试企业里直接生效但如果你的账号是通过“版本发布”管理的没发布新版本的话新权限不会真正生效。这个问题我们后面排查部分还会再提。把这些都配置好之后在.env文件里填入对应的值APP_IDcli_xxxxxxxx APP_SECRETxxxxxxxx ENCRYPT_KEYxxxxxxxx LARK_HOSThttps://open.larksuite.com # 如果使用回调模式还要配置 PORT9000 CALLBACK_PATH/lark/event3.3 编写第一个指令实现 机器人回复环境配置没问题后我们来写第一个真实业务功能。假设需求是用户在群里 机器人并发送“ping”机器人回复“pong”。在 lark-harness 里处理一条消息的逻辑集中在handlers/im.message.receive_v1.ts文件里。打开这个文件核心逻辑是这样的import type { LarkEvent } from lark-harness; export async function handleImMessageReceive(event: LarkEvent) { const { message, sender } event; // 只处理文本消息 if (message.message_type ! text) { return; } const text message.content; // message.content 是 JSON 字符串需要解析 const contentObj JSON.parse(text); const textContent contentObj.text ?? ; // 指令处理 if (textContent.trim() ping) { await event.replyText(pong); } }看到这里你可能会问event.replyText这个方法是哪里来的这就是 lark-harness 封装好的快捷回复能力它自动帮你处理了消息发送的完整链路包括获取 token、拼装消息结构、捕获发送错误。如果你要自定义回复内容也可以用消息服务类直接构建消息。如果不想用快捷方法也可以通过注入的消息服务发送逻辑更透明import { MessageService } from lark-harness; const msgService new MessageService(); await msgService.sendText({ receiveId: oc_xxx, receiveIdType: chat_id, text: pong, });这里的关键点是receiveIdType。飞书的消息接收对象可以是open_id、user_id、chat_id等不同类型对应的接收人语义不同。在群聊场景中用chat_id比较多它代表群 ID。第一个指令跑通之后整个机器人的骨架就算立住了。后续的业务功能基本都是在这个文件里加新的条件分支或者把指令路由单独抽成一个模块按关键词分发到不同的 handler。3.4 发送富文本与表格消息文本消息只是基础企业级场景里真正用的多的是富文本、卡片和表格消息。先看富文本。飞书消息中富文本与普通文本的结构不同它是通过post消息类型来实现的。在 lark-harness 里发送富文本消息的代码类似这样await msgService.sendPost({ receiveId: groupChatId, receiveIdType: chat_id, content: { zh_cn: { title: 要闻播报, content: [ [ { tag: text, text: 销量, style: [bold] }, { tag: text, text: 1289 件 }, ], [ { tag: text, text: 转化率, style: [bold] }, { tag: text, text: 3.27% }, ], ], }, }, });表格消息需要注意的是飞书机器人通过普通消息 API 发送的表格本质上也是富文本的一种但如果你要发真正意义上的“可编辑的在线表格”那应该用云文档 API 创建电子表格然后把链接发到群里。这两者的体验完全不同。我在做数据播报机器人时最初的方案是把数据拼成文本消息直接发出结果群里内容稍微长一点就非常难读。后来改成用表格卡片每行是一条记录字段对齐体验好了很多。3.5 交互卡片开发企业级机器人如果只有“被动响应”价值会大打折扣。真正的交互闭环靠的是卡片。飞书的“卡片 2.0”支持消息卡片内嵌按钮、下拉框、输入框等组件用户在卡片上的操作会触发card.action.trigger事件回调到我们的应用。lark-harness 对这个事件也提供了内置支持。卡片消息的发送稍微复杂一点因为卡片的 JSON 结构比较庞大。我建议先用飞书开放平台的“卡片搭建工具”拖拽生成 JSON再把它存到代码里作为模板。下面是一个最简单的带按钮的卡片消息发送示例import { MessageService } from lark-harness; const cardJson { config: { wide_screen_mode: true }, header: { title: { tag: plain_text, content: 任务确认 }, template: blue, }, elements: [ { tag: div, text: { tag: lark_md, content: 是否确认执行定时任务 }, }, { tag: action, actions: [ { tag: button, text: { tag: plain_text, content: 确认 }, value: { action: confirm } }, { tag: button, text: { tag: plain_text, content: 取消 }, value: { action: cancel } }, ], }, ], }; await msgService.sendCard({ receiveId: userOpenId, receiveIdType: open_id, card: cardJson, });当用户点了“确认”或“取消”按钮飞书服务端会向应用推送card.action.trigger事件。对应的事件处理文件里通过读取event.action.value就能拿到我们预设的action字段。export async function handleCardAction(event: LarkEvent) { const { action, operator } event; const value action.value; if (value.action confirm) { // 执行确认逻辑 await event.replyCard({ elements: [ { tag: div, text: { tag: lark_md, content: 已确认任务即将执行。 } }, ], }); } }这里有个经验值得分享卡片按钮的value字段是开发者自定义数据的关键建议用统一的 JSON 结构携带动作类型、相关业务 ID 和必要的上下文参数。比如value: { action: approve, approvalId: ap20250101, userId: ou_xxx }这样在事件处理时可以直接用来定位业务数据不用再查一次缓存。另外要注意卡片回调事件与普通消息事件有个重要差异卡片回调中如果应用在 3 秒内没有响应用户操作飞书在“回调模式”下会认为请求超时下一次点击按钮时用户能明显感觉到延迟。因此凡是涉及耗时的业务逻辑建议先把“处理中”的状态响应给用户再用异步任务去执行真正的业务。4. 常见问题与排查技巧实录这一部分我把实际开发和线上运维中碰到过的高频问题整理成速查表并写一些排查思路。4.1 事件回调不生效现象本地服务起了长连接也建立了但在群里 机器人没有任何反应。排查思路按顺序来。第一步确认事件订阅是否添加了正确的事件第二步确认应用是否有im:message.receive_v1权限第三步确认代码里的 handler 是否注册到了正确的事件名上。如果用的是回调模式还有一个高频原因回调地址验证失败。飞书在配置回调 URL 时会向这个地址发送一个用于验证的请求应用需要按照协议返回相应的响应。lark-harness 在初始化时已经内置了这个验证逻辑但如果你在代码里把事件入口给拦截了比如加了全局的鉴权中间件就可能影响验证请求。调试阶段我推荐一个小技巧在事件处理入口打一条结构化日志把事件类型、事件 ID、接收时间、请求头都打出来。这样机器人没反应时先看日志里有没有事件进来没有事件就一定是订阅或网络链路的问题有事件就是业务逻辑的问题能快速缩小排查范围。4.2 消息发送失败和权限报错消息发送失败的错误码比较多我列几个最常见的错误码含义处理思路99991663应用无权限发送消息检查是否开通im:message权限并发布版本99991668机器人不在目标群内警告机器人必须先被拉入群聊才能发消息99991672消息内容为空或格式错误检查 JSON 结构尤其是卡片消息的 elements 格式99991400参数错误检查 receive_id、receive_id_type 是否正确对应99991661发送频率超限降低发送频率或分批发送这里面 99991668 是我刚接触飞书机器人时最容易忽略的。你可以在开放平台调试里用 API 直接发消息成功但部署到线上发不到某个群很大概率就是机器人没有被拉进那个群。这个不涉及权限配置纯粹是群维度成员关系的问题。权限报错是另一个高频问题。记住一个原则开放平台的权限分两层一是应用权限配置二是用户或租户授权。即使应用管理后台把某个权限开关打开了如果应用未发布且未在目标租户内启用实际调用时依然会被拒绝。上线流程里发布应用版本这一步千万别省。4.3 Token过期和并发刷新用 lark-harness 自带的令牌管理后这类问题基本不会再出现。但如果你是自己手写的 token 管理我给一个检查和改进的清单一是确认 token 缓存是否设置了接近过期时间的提前刷新建议在过期前 1 分钟主动刷新。二是确认并发场景下 token 刷新是否有互斥锁避免多个请求同时刷新。三是确认 token 获取失败时是否有熔断避免在飞书服务端异常时反复打请求。另外lark-harness 支持自定义 Token 存储你可以把 token 缓存放到 Redis 里。对多实例部署的应用来说这是很有必要的。否则两个实例各自缓存一份 token可能导致其中一个实例的 token 被另一个实例刷新后失效。4.4 开发调试的几条实用经验最后分享几个让我效率提升比较多的调试技巧。第一是多看飞书开放平台后台的“调试工具”。那里可以模拟大部分的事件推送不需要真的在群里发消息。对于事件处理逻辑的单元测试这是非常有用的手段。第二是本地起服务时用长连接模式配合 nodemon 或 ts-node-dev改完代码自动重启开发体验非常接近写普通后端服务。如果用回调模式每次改代码还要重启穿透工具并确保公网地址不变体验就差很多。第三是善用日志。我给生产环境的机器人加了两个维度的关键日志一个是“事件进入”一个是“消息发送结果”。事件进入日志可以让我知道飞书到底有没有把事件推送过来消息发送结果日志可以帮我追踪发送失败的根源。这两类日志打好了线上问题基本一眼就能定位。5. 一些更贴近实战的经验补充5.1 机器人代码怎么组织才能扛住迭代如果只是写两三个指令的玩具机器人怎么组织代码都无所谓。但企业级机器人随着业务增多很容易演变成一个上百个事件处理分支的“面条工程”。我的组织思路是按“指令-服务-数据访问”三层来做。事件 handler 只做解析和响应不写业务逻辑。真正的业务逻辑放在 service 层数据访问放在数据层。这样指令多了以后新增一个功能基本就是新增一个 handler 文件不太会动到已有的代码。lark-harness 项目本身的目录结构是这样建议的但执行得彻不彻底还是看开发者自己。我建议大家在项目早期就定好这个规范否则等到 handler 文件超过几百行再重构成本会很高。5.2 关于机器人的测试和监控机器人的业务逻辑跟 Web 服务相比测试难度在于外部依赖多需要飞书服务端真实响应需要拿到真实的用户 ID 和群 ID。我的实践经验是把核心业务逻辑跟飞书 API 依赖解耦。具体做法是发送消息的动作封装成接口在单元测试时 mock 掉。这样测试的重点就落到指令解析、参数校验和业务规则上不需要真的调飞书 API。监控方面我建议重点盯两个指标事件处理耗时和消息发送成功率。事件处理耗时上涨通常意味着业务逻辑里有慢查询或者外部依赖变慢。消息发送成功率下降大概率是 token 问题或者 API 调用频率超限。lark-harness 提供了简单的耗时日志把这些日志采集到监控系统里可以很直观地看到运行状况。5.3 长连接模式的部署注意事项如果你用的是长连接模式部署时有一个细节值得注意长连接本身会占用一个到飞书服务端的持久连接多个实例同时运行时会建立多条连接。飞书服务端对同一应用的长连接通常会做负载均衡或冲突处理如果你启动了多个实例建议确认平台侧是支持多连接还是只允许单连接否则可能出现事件被随机分配到不同实例的情况。这个问题在多实例部署时是很典型的。我在实践中的做法是对于需要保证消息不重复处理的场景在生产环境用单实例部署靠进程守护保证可用性。如果需要横向扩容再考虑把事件去重放到 Redis 等共享存储中。5.4 如果要做更复杂的集成lark-harness 解决了机器人应用的基础框架问题但企业级机器人往往不只是“收发消息”这么简单它还需要跟内部系统对接比如读取企业数据库、调用内部接口、触发工作流等。我的建议是把 lark-harness 当成应用的一个入口层后面接你自己的业务服务。不要在机器人项目里写太多跟飞书无关的重业务逻辑比如大数据处理、复杂异步任务。如果机器人项目本身包揽了太多业务时间久了它就会变成一个难以维护的单体应用。更好的做法是机器人服务只做指令解析、消息组织和消息发送具体的数据计算和业务处理通过 HTTP 或消息队列转发给后端服务。这样机器人的逻辑保持简单出问题也好排查。6. 结尾的几句心里话做飞书机器人的这几次经历下来我最大的体会有两点。一是类似 lark-harness 这样的脚手架真正的价值不在于省去的那几百行代码而在于它把踩坑经验固化成了默认行为。二是任何脚手架都只能帮你把路修好往哪个方向走、车里装什么货始终是你自己的事。如果你正准备在公司里做一个飞书机器人不管是从零起步还是已经写了一版代码正想重构我都建议先花一个下午把脚手架跑通。先让它处理一条最简单的消息再逐步把事件订阅、卡片交互、权限校验加进来。每一步都亲手验证之后再往前走后续的开发和维护会轻松很多。