把 OpenClaw 接到飞书之后,最直观的变化就是:你在群里发一句“帮我统计一下今天的接口异常数量”,几秒钟后机器人就会返回一张结构化的结果卡片,旁边同事甚至没意识到这背后其实跑了一个完整的工具调用链。这个场景已经不只是“给机器人做个聊天入口”,而是把 OpenClaw 变成团队协作流里的一部分。这篇内容我会把整个接入过程拆开讲清楚,从飞书开放平台的配置、OpenClaw 端的接口准备,到消息收发闭环的代码实现,再到定时推送、多群隔离、敏感指令拦截这些进阶玩法,最后补一份我在实际操作中踩过的问题清单。不管你是第一次把 AI 助手接进办公协作工具,还是已经在其他 IM 上做过类似集成,都可以参考这套思路直接落地。
1. 接入前先想清楚:OpenClaw 与飞书的连接架构怎么搭
1.1 为什么要用飞书机器人做入口
很多团队最初使用 OpenClaw 的方式是命令行、局域网网页或者本地调试工具,功能没问题,但推广起来很费劲。团队成员不可能每个人都去学命令行参数,也不会特意打开一个内部网页去看看“今天有没有需要处理的任务”。而飞书是大部分同事每天都要打开的工具,把 OpenClaw 的入口做成飞书机器人后,使用成本直接降到最低:不需要额外安装客户端,不需要记住端口号,只要在聊天框里 @ 一下机器人,剩下的交给适配层处理就行。
这背后的逻辑和“把运维后台做成 IM 告警群”是同一个道理,好的工具入口应该长在用户已经习惯的界面里。而且飞书机器人并不是只能收发消息,它支持事件订阅、消息卡片回调、自定义按键交互,这就让 OpenClaw 有条件从“被动回答问题”升级到“主动推送结果”和“二次确认后执行”这几种更复杂的协作形态。
1.2 三种接入方案的选型对比
在动手写代码之前,先确定用哪种方式接入。常见的做法有三类,我列了一张对比表,核心区别在于通信方向、交互深度和实现成本:
| 方案 | 通信方向 | 适用场景 | 复杂度 | 建议 |
|---|---|---|---|---|
| 自建应用机器人 + 事件订阅 | 双向 | 群聊对话、指令触发、卡片交互 | 中 | 推荐,满足绝大多数需求 |
| 自定义机器人 Webhook | 单向(入站) | 告警推送、通知播报、定时消息 | 低 | 只适合“只发不收”的场景 |
| 小程序或网页集成 | 双向 | 复杂表格表单、高度定制 UI | 高 | 除非有强交互需求,否则不建议 |
我最终选择的是“自建应用机器人 + 事件订阅”路线。原因很实际:OpenClaw 的价值在于能理解指令并执行任务,这要求机器人能接收用户消息,而不仅仅是单方向推送。Webhook 方案虽然简单,但用户不能通过回复来追问、纠错、触发任务,交互体验大打折扣。
选型时还有一个容易被忽略的点:如果团队已经用飞书管理权限体系,那么自建应用机器人可以直接复用组织架构和成员身份,适配层能拿到真实的发送人 ID。这样一来,后面做“按人限流”“按群白名单”“操作审计”都会顺很多。
1.3 从 @ 机器人到收到回复,完整链路长什么样
接入完成后,一次完整的请求链路是这样走的:
- 用户在飞书群里 @ 机器人并发送一条消息。
- 飞书服务端识别到消息事件后,向你在后台配置的回调地址推送一个事件请求。
- 适配层收到事件,先做合法性校验,然后解析出消息文本、发送者 ID、群聊 ID。
- 适配层把这段文本包装成 OpenClaw 能识别的指令格式,调用 OpenClaw 的 HTTP API。
- OpenClaw 执行内部任务,可能是调用模型、执行脚本、查询数据,也可能是组合多个工具完成一件事。
- OpenClaw 返回结果后,适配层再调用飞书消息 API,把结果以文本、富文本或卡片形式发回原群。
这个链路里,飞书负责“入口和出口”,OpenClaw 负责“理解和执行”,适配层则像一个翻译官,把两边数据格式转来转去。很多集成的难点不在 OpenClaw 的调用,而在于中间的适配层能不能把飞书事件里那些字段准确映射成 OpenClaw 需要的内容,再把输出合适地格式化回飞书。
2. 飞书后台与 OpenClaw 环境准备
2.1 创建企业自建应用并开启机器人能力
进入飞书开放平台后,第一步是创建一个企业自建应用。这里要强调一下,不用选“商店应用”,因为内部集成只需要企业自己可见。创建好应用之后,在“应用能力”里找到“机器人”,把它启用。这一步并不复杂,但后面所有消息收发都依赖这个能力,漏了的话即使回调配置正确也收不到消息事件。
创建完成后,在“凭证与基础信息”页面能看到两个关键参数:App ID 和 App Secret。App ID 是应用的唯一标识,App Secret 相当于密码,调用飞书 API 获取 tenant_access_token 时必须用到。这两个值后面要保存在适配层的环境变量或配置中心里,不要写死在代码仓库中。另外飞书的 token 默认有效期是 2 小时,适配层需要做缓存,没必要每次发消息都重新请求一次。
2.2 事件订阅与权限清单
回调是整条链路里最核心的部分。在开放平台左侧菜单找到“事件订阅”,你需要做三件事:设置请求地址、选择订阅事件、拿到 Verification Token 和 Encrypt Key。
请求地址必须是公网可访问的 HTTPS 地址,飞书服务器会把事件 POST 到这个地址上。本地开发阶段可以用内网穿透工具把本地端口映射成公网地址,但正式环境建议直接部署到有固定域名的服务器上,不然每次重启穿透服务都要去后台改地址。
订阅事件选择im.message.receive_v1,这个事件表示收到新消息,它覆盖单聊、群聊中 @ 机器人的场景。如果你后续要统计消息已读状态,可以再订阅im.message.message_read_v1,但第一阶段不需要。
订阅完成后,开放平台会要求你配置权限。不同版本的后台对权限命名可能略有差别,但大致需要这些:
| 权限标识 | 作用 |
|---|---|
im:message | 读取与发送消息 |
im:message:send_as_bot | 以机器人身份发送消息 |
im:message.group_at_msg | 获取群聊中 @ 机器人的消息内容 |
权限申请后需要管理员审核,内部应用一般很快。权限配不全导致的典型问题就是“事件能收到但发送接口返回 forbidden”,所以这里别急着跳过去,先核对一下清单再继续开发。
2.3 OpenClaw 服务端配置:开启 HTTP 接口与鉴权
OpenClaw 这边要做的事相对简单。通常它的部署方式是 Docker 或二进制运行,启动后会默认提供一个管理用的 HTTP 接口。不同版本的具体配置项会有差异,但一般都要确认三点:API 端口可以被适配层访问到、API Key 鉴权已开启、跨域或网络策略不会挡住调用。
我习惯用一个最小化的配置文件来管理 OpenClaw 侧参数,大致长这样:
server: host: 0.0.0.0 port: 3768 api: enabled: true auth: enabled: true api_key: ${OPENCLAW_API_KEY}这里的端口和 API Key 在适配层写代码时要用到。启动之后,可以用 curl 快速验证一下服务是否正常,而不需要先写飞书那边的代码:
curl -X POST http://127.0.0.1:3768/v1/chat/completions \ -H "Authorization: Bearer ${OPENCLAW_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"model":"default","messages":[{"role":"user","content":"ping"}],"stream":false}'如果返回了正常 JSON,说明 OpenClaw 侧已经准备好接收请求。这一步能提前把问题边界划清,后面如果消息链路不通,至少可以排除“OpenClaw 本身是否可用”这个变量。
3. 核心代码实现:跑通消息收发闭环
3.1 回调验证:先解决飞书的 URL 校验
飞书在后台保存回调地址时会主动发起一次验证请求,请求里的type是url_verification,并携带一个challenge字段。适配层需要把challenge原样返回,并且如果配置了加密,还要先对请求体做解密。我用 Flask 写了一个最简版本:
from flask import Flask, request, jsonify app = Flask(__name__) VERIFICATION_TOKEN = "粘贴Verification Token" ENCRYPT_KEY = "粘贴Encrypt Key" @app.route("/feishu/callback", methods=["POST"]) def feishu_callback(): data = request.get_json() if data.get("token") != VERIFICATION_TOKEN: return jsonify({"code": 1, "msg": "invalid token"}), 403 if data.get("type") == "url_verification": return jsonify({"challenge": data.get("challenge")}) # 事件回调继续走下面的处理 event = data.get("event", {}) handle_event(event) return jsonify({"code": 0, "msg": "ok"})如果开启了 Encrypt Key,飞书会把整个事件体加密后放在encrypt字段里,需要先解密再走后续逻辑。解密方式是 AES-256-CBC,密钥基于 Encrypt Key 做 SHA256 和 Base64 处理后截取,初始化向量取密文前 16 字节。示例逻辑如下:
import base64 import hashlib from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding def decrypt_event(encrypt_key, encrypt_data): key = hashlib.sha256(encrypt_key.encode("utf-8")).digest() encrypted = base64.b64decode(encrypt_data) iv = encrypted[:16] ciphertext = encrypted[16:] cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) decryptor = cipher.decryptor() padded = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(128).unpadder() plaintext = unpadder.update(padded) + unpadder.finalize() return plaintext.decode("utf-8")加密模式的好处是请求体不以明文形式暴露,适合涉及内部敏感信息的场景。但也要注意解密代码在不同语言里的实现细节,Python 的cryptography库处理 PKCS7 时默认要求数据是 Block 对齐的,如果解密报错先检查是否漏了 unpad。
3.2 事件解析:从消息事件中提取有效内容
飞书的事件回调里,消息内容在event.message对象中。它不是直接给一个明文字符串,而是 JSON 字符串,需要二次解析。比如文本消息的content字段是{"text": "..."},所以要json.loads后再取text字段。
其中几个关键字段我在开发第一版时踩过坑,先列出来:
| 字段 | 路径 | 说明 |
|---|---|---|
| 消息内容 | event.message.content | 需要二次 JSON 解析 |
| 发送者 ID | event.sender.sender_id.open_id | 用户唯一标识 |
| 群组 ID | event.message.chat_id | 消息所属会话,单聊和群聊都适用 |
| 消息类型 | event.message.message_type | 常见text、post、image |
| 消息 ID | event.message.message_id | 用于幂等去重 |
一个完整的解析函数可以写成这样:
def extract_message(event): message = event.get("message", {}) message_type = message.get("message_type") if message_type != "text": return None content = json.loads(message.get("content", "{}")) text = content.get("text", "").strip() sender = event.get("sender", {}).get("sender_id", {}) chat_id = message.get("chat_id") message_id = message.get("message_id") return { "text": text, "open_id": sender.get("open_id"), "chat_id": chat_id, "message_id": message_id, }这里有个容易忽略的细节:群聊中 @ 机器人时,飞书推送的文本里会把机器人本身的 ID 也拼进去,比如 “你好 @_user_123”。如果不做清洗,OpenClaw 收到指令时可能把无意义的 @ 符号也当成内容。建议在解析后把@_user_xxx或OpenClaw这些字样的片段去掉,再送入处理。
3.3 调用 OpenClaw 并发送结果回群
拿到用户文本后,下一步是构造 OpenClaw 的请求。OpenClaw 的接口格式通常会兼容 OpenAI 风格的 Chat Completions,因此适配层可以直接用requests库去调用:
def ask_openclaw(session_id, user_message): url = "http://127.0.0.1:3768/v1/chat/completions" headers = { "Authorization": "Bearer " + OPENCLAW_API_KEY, "Content-Type": "application/json", } payload = { "model": "default", "messages": [ {"role": "system", "content": "你是团队内助手,回答要求简洁、可直接执行。"}, {"role": "user", "content": user_message}, ], "stream": False, } resp = requests.post(url, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]拿到结果之后,再通过飞书的发送消息接口把文本发回给用户。发送前需要获取 tenant_access_token:
import requests def get_tenant_access_token(): url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" resp = requests.post(url, json={ "app_id": APP_ID, "app_secret": APP_SECRET, }) data = resp.json() return data["tenant_access_token"] def send_text_message(chat_id, text): token = get_tenant_access_token() url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id" headers = { "Authorization": "Bearer " + token, "Content-Type": "application/json", } body = { "receive_id": chat_id, "msg_type": "text", "content": json.dumps({"text": text}), } resp = requests.post(url, headers=headers, json=body) return resp.json()这个阶段最理想的做法是先用requests手动调通一遍,看看返回结构里是否有code != 0的情况,比如提示 token 无效、权限不足、chat_id 不存在。不要急着加缓存和优化,先把裸链路打通,后面所有高级功能都建立在这条闭环上。
3.4 异步处理:回调 3 秒超时的破解思路
飞书对回调有一个硬性要求:请求必须在 3 秒内返回 HTTP 200,否则会判定超时并自动重试。如果适配层同步去调用 OpenClaw,而 OpenClaw 内部又要执行一段较长的工具链,很容易超过 3 秒。
我的做法是把事件处理丢进线程池,让回调接口立刻返回 200,后台线程再慢慢处理并发送结果:
from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=8) @app.route("/feishu/callback", methods=["POST"]) def feishu_callback(): data = request.get_json() if data.get("token") != VERIFICATION_TOKEN: return jsonify({"code": 1, "msg": "invalid token"}), 403 if data.get("type") == "url_verification": return jsonify({"challenge": data.get("challenge")}) event = data.get("event", {}) executor.submit(handle_event, event) return jsonify({"code": 0, "msg": "ok"}) def handle_event(event): msg = extract_message(event) if not msg: return reply = ask_openclaw(msg["text"]) send_text_message(msg["chat_id"], reply)线程池方案虽然简单,但要注意两个问题:一是消息可能要按用户会话区分上下文,单纯开线程不会自动带 session;二是线程池最大数量的设置要结合服务负载来定,别盲目开太大。如果是生产环境且有重试需求,更稳的做法是引入消息队列,先把事件丢进 Redis 或 RabbitMQ,再由消费端去处理。第一版用线程池足够,后续瓶颈出现再换。
4. 进阶玩法:把 OpenClaw 变成团队自动化中枢
4.1 定时任务:每天 9 点自动推送日报
消息收发闭环跑通之后,自然就会想加定时任务。比如每天早上 9 点让 OpenClaw 汇总生成一份简报,发到固定的日报群。这里的要点不是“定时”,而是“把 OpenClaw 的产出推送给飞书”。
实现上可以把定时调度器独立成一个进程,比如使用 APScheduler:
from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() @scheduler.scheduled_job("cron", hour=9, minute=0) def daily_report(): report = ask_openclaw( "daily_report", "请基于最近的系统状态生成一份昨日日报,重点是异常数、请求量和趋势判断。", ) send_text_message("oc_report_group", report) scheduler.start()这里我建议把ask_openclaw的第一个参数session_id传成类似daily_report这种固定标识,让 OpenClaw 侧能区分不同会话。实际部署时,这个任务也可以放在适配层的同一个 Python 进程里,但要确保调度器不会因为适配层重启而漏执行,最好用单独进程或容器跑。
定时场景中更适合用消息卡片而不是纯文本,因为日报通常包含多个模块。卡片可以用interactive类型,里面塞多个section和div组件,视觉上比长文本更容易扫读。
4.2 事件驱动:任务完成即通知到群
除了定时,还有一类很实用的场景是事件驱动通知。OpenClaw 内部可能会执行一些耗时任务,比如跑测试用例、批量数据处理、模型推理任务,执行完成后希望主动通知到群里。
实现方式有两种。第一种是在 OpenClaw 的工具内部写回调逻辑,执行结束后直接调用飞书 Webhook 或 API。第二种是在适配层监听 OpenClaw 返回结果的状态,发现是异步任务完成事件,再转成飞书消息。
我更推荐第二种,因为让工具层保持纯净,不做飞书 SDK 的耦合,所有对外通知统一由适配层完成。比如 OpenClaw 返回一个携带task_status字段的 JSON,适配层判断completed之后,再调用send_interactive_card把任务结果展示出来。
4.3 多群隔离与权限白名单
接入早期可以只在测试群开放,但一旦推广到多个团队,就需要考虑多群隔离。最基本的控制是白名单,适配层维护一个允许响应的 chat_id 列表,不在列表里的群即使 @ 机器人也不做任何响应。
ALLOWED_CHATS = {"oc_test_group", "oc_tech_group"} def is_allowed(chat_id): return chat_id in ALLOWED_CHATS这个列表可以配置成外部 YAML 或数据库表,方便非开发人员调整。比白名单更细粒度的控制还包括按用户角色限制指令,比如普通成员只能触发查询和对话,管理员才能触发执行类任务。实现方式是在解析完open_id后,再查一层用户映射表或调用飞书通讯录 API 判断角色。
多群隔离背后还有一层考虑:OpenClaw 的大模型调用成本不是免费的,白名单能避免有人把机器人当成闲聊对象,导致 token 消耗失控。
4.4 敏感指令拦截与卡片二次确认
OpenClaw 如果接了执行类工具,比如命令行执行、数据库操作、发布脚本,就必须加一道安全闸门。群里一句话直接触发生产环境的操作,风险可想而知。
我的做法是适配层维护一组关键词规则,命中后先不执行,而是给发送人回一条确认卡片。卡片带两个交互按钮,用户点击“确认执行”后才能继续:
SENSITIVE_KEYWORDS = ["rm -rf", "drop table", "shutdown", "restart", "production"] def contains_sensitive(text): lowered = text.lower() return any(kw in lowered for kw in SENSITIVE_KEYWORDS) def handle_event(event): msg = extract_message(event) if not msg: return if contains_sensitive(msg["text"]): send_confirm_card(msg["chat_id"], msg["text"]) return executor.submit(process_and_reply, msg)卡片按钮点击后的回调会以card.action.trigger事件推送到适配层,那时再真正执行 OpenClaw 调用。这个流程本质上是在“对话入口”和“实际操作”之间加了一道显式确认,成本很低但能挡住大部分误操作。
5. 踩坑记录:常见问题与排查思路
5.1 高频问题速查表
我把接入过程中遇到的高频问题整理成了一张表,按症状、原因、解法组织,遇到问题直接对号入座:
| 症状 | 常见原因 | 解决办法 |
|---|---|---|
| 后台保存回调地址一直校验失败 | challenge未返回,或请求体解密失败 | 检查url_verification分支是否先于加密处理 |
| 事件回调收到了但群里收不到回复 | 线程池抛异常,回复逻辑没执行 | 查看适配层日志,确认handle_event是否有异常 |
发消息时报code 99991663 | 机器人没有im:message:send_as_bot权限 | 到开放平台补齐权限并重新发布版本 |
| 用户 @ 机器人后没有任何回调 | 事件订阅未选到im.message.receive_v1 | 确认事件已添加并发布,回调地址公网可访问 |
| 回复内容被截断 | 文本长度超过飞书单条消息上限 | 改为富文本分块,或使用消息卡片多 section |
| 同一条指令处理两次 | 飞书超时重试,自己的逻辑没有幂等 | 用message_id做去重缓存 |
| OpenClaw 调用返回 401 | API Key 配错,或者鉴权没开启 | 先在本地用 curl 验证 OpenClaw 接口可通 |
这里有一个容易被忽视的点:飞书在回调超时或网络异常时会自动重试。如果适配层没有做消息幂等,用户可能看到机器人把同一条回复发了两遍。我通常在内存里维护一个最近一小时的消息 ID 集合,命中直接丢弃,生产环境可以换 Redis 做同样的去重。
5.2 调试建议:先跑通 echo 再叠功能
接入过程中最大的经验就是“别一上来就想做复杂功能”。我的做法是分四步推进:
- 第一步,在本地写一个极其简单的回调服务,只打印收到的原始 JSON,确认飞书事件能推过来。
- 第二步,把
url_verification分支写好,让后台 URL 校验能通过。 - 第三步,写一个 echo 逻辑,不管用户发什么,机器人原样返回一句“收到”。
- 第四步,再把 OpenClaw 的调用接进来。
很多问题如果跳过前两步,直接写 OpenClaw 调用,一旦失败就得同时排查配置、网络、解析、鉴权四个环节,定位成本非常高。先跑通 echo,你就有了一个可控的基线,后面每加一层只排查多出来的那部分。
开发时还要善用飞书开放平台自带的调试台。它可以手动模拟事件推送,直接看请求响应结果和失败日志,能省掉反复用手机发消息测试的时间。本地开发时我还会把所有回调原始数据打一份日志,格式随意,关键是能复现问题现场。
5.3 几条实用心得:从日志到安全
再说几条不带代码但很重要的心得。
第一,日志里不要打印用户的完整消息内容。飞书群里可能包含内部敏感的工作信息,日志只记录message_id、事件类型和字符长度就够了。我见过因为日志打太多导致运维去日志平台翻聊天记录的尴尬场景,这个边界要从开发时就把住。
第二,OpenClaw 的输出不一定都适合直接群发。比如它会输出很多分析过程、思考链、工具返回的原始 JSON,直接转成飞书文本会非常啰嗦。适配层要做一层“回复压缩”,把关键结论和行动项提取出来。我在实际项目中常用一个简单 prompt 让 OpenClaw 把自己的输出按要求格式化为“结论+依据+建议动作”,效果比后端硬截断要好。
第三,别把所有指令都交给大模型判断是否执行。重要操作必须在适配层用规则卡住,前面说的敏感词拦截只是最低要求。真正强管控时,应该给 OpenClaw 配置独立的白名单工具集,让它只能调那些明确允许的操作。
第四,关联了真实业务后,飞书机器人的回复延迟会直接影响团队使用意愿。我建议把 OpenClaw 里耗时过长的工具拆出来做成异步任务,先给用户回一条“任务已启动,预计 xx 秒后完成”,等结果出来再推送到群里,比让用户干等体验好得多。
再补一个细节:飞书的消息卡片设计是有学习成本的,但一旦用熟,效果会优于纯文本。卡片里的头部标题、状态标签、按钮位置都可以复用同一套模板,适配层只要维护一个“卡片模板工具函数”就能统一风格。不要把一张卡片塞太多内容,一条消息有一件事、一个结论、一个行动按钮,体验是最舒服的。
整个接入过程走下来,我的最大体会是:接口对接本身不难,难的是把“人能看懂的对话”和“机器能执行的任务”这两套逻辑在中间层恰当地翻译和隔离。飞书负责入口体验,OpenClaw 负责能力执行,适配层负责规则和安全。先跑通最小闭环,再逐步往里面加权限、定时、卡片和拦截,这条路线适合绝大多数团队。
如果你准备动手做,建议第一周先解决两件事:一是本地把 OpenClaw 的 HTTP 接口调通,二是在飞书后台完成一次真实的消息回声。这两件事一结束,整个接入工作就算完成了最难的一步,后面的扩展都是在这个基线上做增量。