
上一篇我们把核心逻辑跑成了本地命令行 Demo说实话那个版本只适合自己敲着玩。真要给客户用总不能让对方也搭一套 Python 环境。我当时的想法很简单客户在飞书里发一句“帮我查一下订单状态”这个 Demo 能把查询结果原样回过去。于是就有了这篇——把飞书机器人接入 Demo让它接收客户话题并回复查询结果。这篇适合刚做完一个可用 Demo、想快速把它暴露给真实用户的人也适合想搞懂飞书机器人消息链路的人。我会把从建应用、配权限、选事件模式到写代码、踩坑的完整过程都过一遍。1. 为什么要把一个 Demo 接到飞书1.1 本地程序与真实用户之间的最后一公里很多团队做 Demo 的习惯是先做核心逻辑做成命令行工具或者本地函数验证算法通、结果对然后就得面对一个棘手问题怎么让非技术用户用上它。我上一篇就是这样查询函数写得挺顺但同事想看结果得跑过来让我在终端里执行再把输出截图发给他。一次两次还行次数多了就很尴尬。本质问题是Demo 缺少一个“用户入口”。飞书机器人恰好补上了这一环。它把飞书当成交互界面客户在会话窗口里发消息机器人后台调用 Demo 的业务函数再把结果回复到会话里。客户不需要知道背后是什么语言、跑在哪台机器上他只感知到“在飞书里跟一个机器人对话就有结果”。这层封装对交付 Demo、内部试用、收集需求反馈特别实用。1.2 飞书机器人处理这类场景的几个天然优势选飞书不是因为它多高大上而是因为它解决了我在那个阶段最头疼的三件事。第一不用单独做前端页面。做 Web 页面得考虑布局、接口、部署域名一套下来小半天就没了。飞书机器人的交互界面是现成的输入框、会话列表、消息气泡全都有我只要处理消息文本。第二客户端直达。现在企业里飞书覆盖率很高用户本来就在飞书里工作让他在同一个工具里发消息查结果比让他跳到另一个系统自然得多。第三有一个完整开放平台支撑。应用管理、权限、事件订阅、消息接口都是现成的不太需要从零造轮子。虽然飞书的开放平台文档有时也让人头大但总体来说接入路径是清晰可控的。2. 飞书机器人的两种形态选错形态等于白做2.1 自定义群机器人只能单向发送飞书里的“机器人”其实分两类。第一类叫自定义群机器人在群设置里添加 Webhook 地址就能用。它的用法很粗暴往 Webhook 地址 POST 一段 JSON飞书就把消息发到群里。整个过程是单向的。机器人没有身份、不能收消息、没法和用户产生会话上下文更不可能接收客户话题再回复。我最初也动过这个念头既然只是把结果通知出去Webhook 群机器人似乎够了。但很快发现需求对不上——我们的场景是“客户先提问机器人后回答”。这需要机器人有接收消息的能力自定义群机器人完全做不到。所以如果你也打算做类似“提问-回答”的交互可以直接排除这一形态。2.2 应用机器人能收能发的正路第二类叫应用机器人基于飞书开放平台的企业自建应用创建。它本质上是一个拥有机器人身份的飞书应用用户在会话里它或单聊它机器人都能收到事件消息处理后调用开放接口主动回复。它的关键是“事件订阅”机制飞书服务器收到用户发给机器人的消息后把消息事件推给应用。推送成功后应用可以调用 API以机器人的身份在单聊或群里回复。这一来一回正好匹配“接收话题、回复查询结果”的需求。应用机器人还有个好处它带完整的权限体系。想让机器人读消息、发消息、访问通讯录还是读写多维表格都是按权限申请和授权的可控性比 Webhook 好很多。2.3 为什么我最终选了应用机器人表格里一对比就很清楚对比项自定义群机器人Webhook应用机器人接收消息不支持支持通过事件订阅主动回复仅推送群消息单聊/群聊均可回复用户身份识别不支持可以拿到发送者 open_id权限管理无完整的 OAuth 与权限体系适用场景报警、通知、日志推送对话机器人、客服、查询助手所以结论很直接要做“客户发话题、Demo 回结果”这种双向交互只能选应用机器人。后面所有步骤都基于这个形态展开。3. 应用创建的完整操作从开放后台到拿到凭证3.1 创建企业自建应用打开飞书开放平台登录后进入开发者后台点击“创建企业自建应用”。这里有个容易忽视的点你需要有对应飞书租户的开发者权限。如果你是管理员那就没问题如果不是找管理员给你开个开发者角色否则创建入口都看不到。应用名称我建议起得直白一点比如“订单查询 Demo”。名字会在飞书客户端里显示给用户最好让人一眼知道这个机器人是干什么的。描述和图标可以后面再补不会影响功能接入。创建完成后进入应用管理页左侧菜单能看到“应用能力”“权限管理”“事件订阅”“版本管理与发布”等模块后续配置都在这里。3.2 开启机器人能力应用建好之后默认它只是一个空壳没有机器人身份。需要进入“应用能力”找到“机器人”点击开启。这一步做完应用等于多了一个机器人身份。此时飞书客户端里已经可以通过搜索应用名称找到这个机器人但先别急着去测试因为后面还缺权限和事件配置现在发消息它也不会理你。开启机器人之后我建议顺手把机器人的头像和名称配置一下虽然是小事但在测试阶段能帮你快速区分自己和同事的机器人。3.3 三个凭证值App ID、App Secret、验证令牌进入“凭证与基础信息”能看到几个关键凭证。App ID 是应用的唯一标识类似用户名传输中不算特别敏感但最好也别到处贴。App Secret 是密钥调用 API 时用它换 Tenant Access Token权限极大千万别提交到 Git 仓库。验证令牌Verification Token在 Webhook 模式下用来校验请求来源长连接模式用不到但可以记下来备用。我当时在代码里直接用硬编码测试后面立刻被教训了。建议从一开始就放环境变量export FEISHU_APP_IDcli_xxxxxxxx export FEISHU_APP_SECRETxxxxxxxx代码里读取环境变量避免密钥泄露风险。这一步花两分钟后面少很多麻烦。3.4 权限申请这一步卡住了不少人应用机器人要接收用户消息再回复需要两类权限。第一类是读取消息内容。在“权限管理”里搜索im:message或im:message:readonly不同版本权限粒度不一样新版本被拆成了单聊读、群聊读、全量读等细分权限。Demo 阶段申请im:message或者im:message.p2p_msg:read、im:message.group_msg:read就够了按需选择。第二类是发送消息。搜索im:message:send_as_bot这个权限允许机器人以自身身份发送和回复消息。权限申请时有一个细节权限名称旁边会标注具体作用范围有些是“仅限应用所在租户”有些是“跨租户”。本地 Demo 选租户内即可。我当时在这块踩了个误会以为申请了权限马上就能测结果消息事件一直收不到。后来才明白开放平台的权限并不是申请了就生效它要随应用版本一起发布过审之后才真正启用。这个问题我放在第 6 部分展开讲。4. 事件订阅的两条路径Webhook 回调与长连接4.1 Webhook 回调对 Demo 阶段的不友好飞书给应用推送消息事件有两种方式第一种是 Webhook 回调方式。需要在“事件订阅”里填一个公网可达的“请求地址 URL”。飞书服务器收到用户消息后会往这个 URL 发一个 POST 请求你的服务收到请求再返回响应。听起来不难但对本地开发很不友好你的电脑没有公网域名飞书根本连不上你。网上有一些把本地服务暴露到公网的方案但不是要下载额外工具就是需要注册域名和 HTTPS 证书而且稳定性看运气。更麻烦的是Webhook 回调还涉及 URL 验证、签名校验、解密等一堆事。为了一个 Demo 搞这些性价比太低。4.2 长连接模式本地开发者的救星第二种方式是长连接模式也叫 WebSocket 模式。飞书官方 SDK 会在本地建立一条到飞书服务器的 WebSocket 长连接。飞书收到用户消息后通过这条连接把事件推给你。你不需要公网 URL不需要配置域名和回调只需要程序能访问外网即可。这个模式对 Demo 阶段太合适了。我在笔记本上启动程序飞书上聊一句终端立刻能打日志消息实时推过来调试体验非常顺。长连接模式还自带重连机制网络抖断后 SDK 会尝试恢复稳定性够用。4.3 如何配置长连接模式并添加消息事件在应用“事件订阅”页面选择“使用长连接接收事件”。然后点击“添加事件”在事件列表里搜索im.message.receive_v1接收消息添加到订阅列表。这里注意一个细节事件名称要选receive_v1这是消息接收事件的当前版本。有些文档里还写着旧的im.message.receive那是老版本新应用不建议用。添加完成后先不用做别的。长连接模式下不需要填 URL也不用配加密策略。如果你后续想开 Encrypt Key 加密那是生产环境该考虑的事Demo 阶段先把链路跑通最重要。配置保存后应用后台会要求“发布版本”才能生效先别急把代码那边也准备好一起发布测试更高效。5. 消息流转与代码接入从用户 机器人到回复发出5.1 一条客户消息在飞书里的完整旅程把整个链路拆开看一条消息从客户发出到收到回复要经历四步。第一步客户在飞书里给机器人发单聊或者在群里 机器人。第二步飞书服务器接收到消息后通过长连接把“消息接收事件”推送到你的程序。第三步程序解析消息内容调用 Demo 的查询函数得到结果。第四步调用回复消息 API把结果以机器人身份发回原会话。理解这个链路的价值在于排错消息没到问题多半在事件订阅或长连接消息到了但没回复问题多半在权限或代码解析。知道每一段的边界排查时就不至于瞎猜。5.2 准备 Python 工程与 SDK我这边用的是 Python飞书官方提供了lark-oapiSDK安装一行搞定pip install lark-oapiSDK 版本建议用最新的 2.x。2.x 的长连接支持更完善事件注册的写法也比旧版简洁。如果你之前看过一些老教程里from lark_oapi import Client的写法那是旧版本建议直接看官方 README 的最新示例。5.3 核心代码注册事件、解析消息、回复结果写一段可直接运行的接入代码。假设业务查询函数叫search你只需替换它其他部分是通用的。import json import re import os import logging import lark_oapi as lark from lark_oapi.api.im.v1 import ( P2ImMessageReceiveV1, ReplyMessageRequest, ReplyMessageRequestBody, ) logger logging.getLogger(feishu-demo) logging.basicConfig(levellogging.INFO) def search(query_text: str) - str: 业务查询模块按实际 Demo 逻辑替换即可 if 订单 in query_text: return 查询结果订单 SB-20250101-001状态【已发货】预计明天到达。 if 库存 in query_text: return 查询结果SKU-10086 当前库存 24 件。 return f已收到话题{query_text}。当前 Demo 暂无匹配结果可以试试问“订单”或“库存”。 def handle_message(data: P2ImMessageReceiveV1) - None: message_id data.event.message.message_id content json.loads(data.event.message.content or {}) raw_text content.get(text, ) # 群聊场景只处理被 的消息单聊场景直接处理 if data.event.message.chat_type group: mentions data.event.message.mentions or [] if not mentions: return # 去掉消息里的 占位符把纯文本交给业务函数 clean_text re.sub(rat[^]*[^]*/at, , raw_text).strip() if not clean_text: return result search(clean_text) reply(message_id, result) def reply(message_id: str, text: str) - None: req ( ReplyMessageRequest.builder() .message_id(message_id) .request_body( ReplyMessageRequestBody.builder() .msg_type(text) .content(json.dumps({text: text})) .build() ) .build() ) resp client.im.v1.message.reply(req) if not resp.success(): logger.error(reply failed: code%s msg%s, resp.code, resp.msg) handler ( lark.EventDispatcherHandler.builder(, ) .register_p2_im_message_receive_v1(handle_message) .build() ) client lark.ws.Client( app_idos.getenv(FEISHU_APP_ID, ), app_secretos.getenv(FEISHU_APP_SECRET, ), event_handlerhandler, log_levellark.LogLevel.INFO, ) if __name__ __main__: client.start()这段代码的核心就三块。EventDispatcherHandler注册消息事件处理函数handle_message负责解析和调用业务函数reply负责把结果回复到原消息。content字段是 JSON 字符串不是普通对象所以要先用json.loads解析再取text字段。这是新手最容易看懵的地方。5.4 单聊与群聊的处理差异我在代码里加了chat_type判断。单聊场景下用户给机器人发的每条消息机器人都会收到群聊场景下飞书默认只推送给“被”的消息事件。为什么要主动判断群聊的mentions因为如果后续你给机器人开了“接收群内所有消息”的权限不加判断的话群里任何人的闲聊都会触发你的查询逻辑既浪费调用又容易误回复。判断方式是通过mentions字段。如果群消息里没有任何 mention说明这条消息没在叫机器人直接丢弃。单聊则无需判断因为消息本来就是发给机器人的。这里有一个容易被忽略的点群聊里被的消息文本内容里会带at user_idxxx/at占位符。如果不做清理你的search函数收到的字符串就会夹着这些标记匹配关键词时很容易失败。所以我用正则把at.../at去掉再交给业务层。6. 我踩过的坑与排查思路6.1 机器人完全没反应先看长连接通没通我第一版代码跑起来后在飞书里给机器人发消息等了几秒毫无动静。当时第一反应是权限没配好结果翻了一圈权限之后发现长连接可能根本没建立。排查方式其实很直观看程序日志。lark.ws.Client如果成功建立长连接SDK 会输出连接成功的日志如果连接失败通常伴随认证错误。我那次就是App Secret填错了SDK 启动时没有报错但连接一直卡在认证阶段。把日志级别开到DEBUG之后原因一目了然。所以遇到机器人完全没反应第一件事不是改权限而是确认长连接有没有连上。6.2 消息收到了但回复失败八成是权限问题连接通了之后日志里能看到事件进来但回复就是不成功API 返回一个权限相关错误码。这个阶段的典型原因是im:message:send_as_bot没生效。注意申请权限之后还要发布版本。我当时申请了一堆权限代码也写好可权限没有随应用版本发布出去回复接口自然被拒。正确顺序是权限申请完成 - 创建版本 - 申请发布 - 审核通过 - 重新测试。有些租户的审核在自己手里如果你有管理员权限可以直接通过如果走企业流程就得等审批。6.3 权限开了还是不生效应用必须发版发布版本这个事情我在第一个坑里已经遇到一次后来同事接入时又遇到一次。他权限申请了后台也显示“已开通”但机器人就是读不到消息。原因是权限管理页显示的“已开通”只代表这个权限已经申请成功而事件订阅和权限的实际启用要以“版本发布”为节点。没有发布新版本一切配置都是纸面上的。所以版本发布是一个很容易被漏掉的隐藏步骤。每次改了权限或者事件订阅记得重新发一次版本再等审核通过。这个教训我刻在脑门上了。6.4 同一条消息被回复了两次事件重试与幂等长连接偶尔会断线重连。如果断线期间飞书没推完事件重连后它会重推。如果消息事件被重复推送而你没有任何去重逻辑用户就会看到机器人对同一条消息回复两次。我在测试时就遇到过一开始以为是程序卡顿导致重复调用后来看日志才发现是 SDK 重连后的重推。解法也不复杂在内存里维护一个最近处理过的message_id集合重复消息直接忽略。from collections import deque recent_messages deque(maxlen200) seen_ids set() def handle_message(data: P2ImMessageReceiveV1) - None: message_id data.event.message.message_id if message_id in seen_ids: return seen_ids.add(message_id) recent_messages.append(message_id) # 后续处理逻辑...用deque(maxlen200)做滑动窗口既避免了无限膨胀又能覆盖绝大多数重推场景。生产环境可以用 Redis 做同样的去重思路一样。6.5 群里偶尔收到所有消息注意群消息可见性还有一个场景让我花了一点时间才弄清机器人明明只应该处理它的话但日志里偶尔出现群内普通消息。原因是权限配置中开启了“获取群组中所有消息”之类的权限或者所在租管后台把机器人的群消息可见范围做了调整。这个不是默认行为但如果你的应用为了其他功能申请过群消息相关的高级权限就可能出现这种附带效果。解决方式有两个第一检查权限管理里是否有额外申请的群消息读权限非必要就删除第二在代码里坚持做mentions判断这是最后的防线。7. 让回复更专业从纯文本到富文本、表格与卡片7.1 用 Post 富文本组织多条结果纯文本回复能解决问题但观感一般。如果查询结果包含多段信息用msg_typepost富文本会更清晰。富文本消息可以设置标题、加粗字段、换行适用于状态报告类的回复。它在 API 里本质是 POST /im/v1/messages 时把msg_type改为postcontent改传富文本结构。一个简单例子{ zh_cn: { title: 订单查询结果, content: [ [ {tag: text, text: 订单号SB-20250101-001}, {tag: text, text: 状态, style: {bold: true}}, {tag: text, text: 已发货} ], [ {tag: text, text: 物流单号SF1388888888} ] ] } }飞书客户端渲染时会对齐展示阅读压力比一大段纯文本小很多。7.2 查询结果表格怎么发给客户“机器人发送表格”这个需求我在做查询结果展示时也遇到过。飞书消息本身没有直接发 Markdown 表格的类型但有几种可行路径。第一种如果结果是结构化数据可以生成一张图片以image消息类型发送。图片对格式控制最稳缺点是需要额外处理渲染。第二种用消息卡片里的“表格”组件。飞书消息卡片支持更丰富的布局可以在卡片里塞入表格数据客户在飞书里直接看到行列对齐的查询结果。第三种直接把结果包装成 CSV 文件以file类型发送。飞书客户端会显示成文件气泡用户点开就能下载。这种方式最适合导出型场景。我自己的做法是结果行数少就发文本或富文本结果多就生成图片或者文件。具体看业务需要。7.3 消息卡片把结果做成可交互的界面如果想把机器人做完整一点消息卡片值得研究。卡片里可以放按钮、文本、图片、表格甚至支持按钮交互回调。比如查询订单后卡片里展示订单信息底部放一个“查看详情”按钮用户点击后触发新的回调事件机器人再回复更详细的内容。这就从“问答机器人”升级成了“可交互业务工具”。不过卡片 JSON 结构相对复杂建议先跑通文本消息再把核心回复逐步改成卡片。一次不要贪多先把链路跑通再优化交互体验。我在实际接入中最大的体会是飞书机器人的接入门槛其实是“配置链路长代码不复杂”。只要把应用创建、权限发布、事件订阅这三件事理顺后面的事都是业务函数怎么查结果、结果怎么排版的问题。希望这篇能帮你绕开我撞过的那些坑。