1. 为什么飞书机器人一定要走 Webhook,而不是轮询
如果你正在把飞书当成团队的情报入口,想让 OpenClaw 这类 Agent 工具自动采集、整理、回推到群里,第一个绕不开的问题就是:飞书怎么把消息实时交到你手里。很多人第一反应是写个定时任务,每隔几秒调一次飞书的消息接口,问一句“有新消息吗”。这个方案能跑,但延迟高、空转多,消息一多就开始堆积,体验很差。
Webhook 换了个思路:你不再主动去问,而是提前在飞书开放平台登记一个公网 URL,当群里有人发消息、机器人被 @、或者卡片被点击时,飞书服务器主动向你登记的地址发一个 HTTP POST。请求的发起权在事件源手里,你的服务只需要被动接收、解析、分发。这就是所谓的 Push 模型,也是工业级 IM 机器人的标准做法。
这篇是系列第一篇,目标很明确:把飞书 Webhook 回调链路和 OpenClaw 情报采集链路第一次打通。具体交付四样东西——一个可复制的 Flask 接收端骨架、OpenClaw 侧 config.toml 的配置片段、TaoToken 统一 Key 在 settings.json 里的填写位置,以及本地反向代理验证 Webhook 可达性的命令和预期返回。适合需要把多源 AI 能力接进飞书群的技术团队,也适合自己搭自动化情报站的同学。
整条链路大致是这样:飞书群消息触发事件,飞书服务器 POST 到你的公网入口,反向代理转发到本机 Flask 的 18789 端口,Flask 做握手校验和指令提取,再把清洗后的文本交给 OpenClaw,OpenClaw 调用模型能力生成结果,最后回推飞书。这一篇先把前半段跑通。
2. TaoToken 前置:统一 Key 与 OpenClaw 的接入位置
在动手写代码之前,先把模型调用的入口定下来。OpenClaw 作为 Agent 工具,本身不绑定某一家模型,它需要一个兼容 OpenAI 协议风格的 API 端点。TaoToken 提供的就是这样一个统一入口,你申请一个 Key,就能在 OpenClaw 里调用多种模型,不用为每个模型单独维护一套鉴权和地址。
官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意这个 API 地址后面不加任何查询参数。你需要先去控制台创建一个 API Key,创建入口在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。Key 只在创建时完整显示一次,复制下来存到安全的地方。
如果你只是想先验证模型能不能通,可以直接用模型对话页面试一句: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果打算长期跑编码类或 Agent 类任务,建议了解下 Coding Plan: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到参数问题优先查这里。
注意:API Key 属于敏感凭证,不要写进会提交到 Git 的配置文件里。建议用环境变量注入,或者放在 .gitignore 覆盖的本地配置中。
OpenClaw 侧的配置分两块:一块是 config.toml,负责声明模型端点和行为;另一块是 settings.json,负责填写实际的 Key 和运行时参数。下面两节分别给出可复制的片段。
3. 可复制配置:Flask 接收端、config.toml 与 settings.json
3.1 Flask 接收端骨架 bridge.py
先建一个最小可运行的接收端。它的职责有三个:响应飞书的 url_verification 握手、解析 im.message.receive_v1 事件、把用户文本打印或转发出去。依赖只有 Flask 和 requests。
# bridge.py import json import os import requests from flask import Flask, request, jsonify app = Flask(__name__) # OpenClaw 本地网关地址,后续章节会用到 OPENCLAW_ENDPOINT = os.environ.get("OPENCLAW_ENDPOINT", "http://127.0.0.1:18790/ingest") @app.route("/webhook", methods=["POST"]) def handle_feishu(): data = request.get_json(silent=True) or {} # 1. 握手校验:飞书首次配置时会发 url_verification if data.get("type") == "url_verification": return jsonify({"challenge": data.get("challenge")}) # 2. 事件分发:只处理消息接收事件 header = data.get("header", {}) if header.get("event_type") == "im.message.receive_v1": event = data.get("event", {}) message = event.get("message", {}) # content 是被二次转义的 JSON 字符串,需要再解析一次 raw_content = message.get("content", "{}") try: msg_content = json.loads(raw_content) except json.JSONDecodeError: msg_content = {} user_text = msg_content.get("text", "").strip() print(f">>> 接收指令: {user_text}") # 3. 转发给 OpenClaw(可选,链路打通后再启用) if user_text: try: requests.post( OPENCLAW_ENDPOINT, json={"text": user_text, "source": "feishu"}, timeout=5, ) except requests.RequestException as exc: print(f"转发 OpenClaw 失败: {exc}") return jsonify({"code": 0, "msg": "ok"}) if __name__ == "__main__": app.run(host="0.0.0.0", port=18789, debug=False)启动命令:
pip install flask requests python bridge.py启动后你会看到 Flask 监听在 0.0.0.0:18789。注意 host 必须是 0.0.0.0,否则反向代理从外部转发进来会连不上。
3.2 OpenClaw 的 config.toml 片段
OpenClaw 的 config.toml 负责声明模型提供方。把 base_url 指向 TaoToken 的 API 地址,模型名按你实际要用的填。
# config.toml [provider.taotoken] type = "openai_compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" default_model = "gpt-4o-mini" timeout_seconds = 60 [agent.default] provider = "taotoken" system_prompt = "你是一个情报整理助手,负责把飞书群里的原始消息归纳成结构化摘要。" max_tokens = 2048 temperature = 0.3 [ingest] listen_host = "127.0.0.1" listen_port = 18790 path = "/ingest"这里 api_key_env 指向环境变量名,而不是把 Key 明文写进 toml。启动 OpenClaw 前先导出:
export TAOTOKEN_API_KEY="你的Key"3.3 settings.json 里 TaoToken Key 的填写位置
有些 OpenClaw 版本或插件用 settings.json 管理运行时配置。Key 字段通常叫 api_key 或 token,放在 provider 对应的节点下。结构大致如下:
{ "providers": { "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "default_model": "gpt-4o-mini" } }, "runtime": { "log_level": "info", "ingest_port": 18790 } }提示:settings.json 和 config.toml 同时存在时,以你所用 OpenClaw 版本的加载顺序为准。不确定就只保留一份,避免 Key 来源混乱。
4. 验证请求:反向代理与 Webhook 可达性测试
飞书服务器在公网,你的 Flask 在本机 18789,中间需要一个公网入口。生产环境用 Nginx 或云负载均衡,本地调试可以用反向代理工具把本机端口映射出去。这里给出 Nginx 的配置和本地验证命令。
4.1 Nginx 反向代理配置
server { listen 443 ssl; server_name your-domain.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; location /webhook { proxy_pass http://127.0.0.1:18789/webhook; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 30s; } }改完执行nginx -t检查语法,再nginx -s reload生效。云服务器还要在安全组放行 443 入站,否则握手请求在网络层就被丢弃,表现为 Connection Timeout。
4.2 本地模拟飞书握手请求
在配置飞书后台之前,先用 curl 模拟一次 url_verification,确认你的接收端能正确回传 challenge:
curl -X POST https://your-domain.example.com/webhook \ -H "Content-Type: application/json" \ -d '{"type":"url_verification","challenge":"test-challenge-123"}'预期返回:
{"challenge":"test-challenge-123"}如果返回的是这个,说明公网入口、反向代理、Flask 三层都通了。如果返回 502,多半是 Flask 没起来或端口不对;返回 404,检查 Nginx 的 location 路径和 proxy_pass 是否一致。
4.3 模拟消息事件
握手通过后,再模拟一条消息事件,确认解析逻辑正常:
curl -X POST https://your-domain.example.com/webhook \ -H "Content-Type: application/json" \ -d '{ "header": {"event_type": "im.message.receive_v1"}, "event": { "message": { "content": "{\"text\":\"帮我整理今天的行业新闻\"}" } } }'预期在 Flask 控制台看到>>> 接收指令: 帮我整理今天的行业新闻,同时接口返回{"code":0,"msg":"ok"}。到这一步,飞书到本地的链路就算打通了。
4.4 验证 OpenClaw 侧模型调用
链路打通后,单独验证 OpenClaw 能不能通过 TaoToken 调通模型。用 curl 直接打 API 基址:
curl https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "用一句话说明 Webhook 和轮询的区别"}] }'返回里能看到 choices 数组和模型输出,就说明 Key 和端点都正确。如果返回 401,检查 Key 是否复制完整;返回 404,检查 base_url 是否误加了路径后缀。
5. 本篇常见错排查
5.1 握手返回 challenge 为空
最常见的原因是 request.get_json() 拿不到数据。飞书发的是 application/json,但如果你在 Nginx 里改了 Content-Type,或者 Flask 前面还有一层没透传 body,就会解析失败。用request.get_data(as_text=True)打印原始 body 确认一下。另外注意 challenge 字段在顶层,不在 header 里。
5.2 端口占用导致 Flask 起不来
报错Address already in use,说明 18789 被占了。查一下:
lsof -i :18789如果是上次没退干净的进程,kill 掉再启动。OpenClaw 的 18790 同理,两个端口别配重了。
5.3 反向代理 502 Bad Gateway
三个检查点:Flask 是否监听 0.0.0.0 而不是 127.0.0.1;Nginx 的 proxy_pass 地址端口是否和 Flask 一致;SELinux 或防火墙是否拦截了本机回环转发。云服务器上curl http://127.0.0.1:18789/webhook能通,但外网不通,基本就是安全组没放行。
5.4 content 字段解析报错
飞书消息的 content 是被转义过的 JSON 字符串,形如"{\"text\":\"hello\"}"。直接当字典用会报错,必须先 json.loads 一次。如果消息类型是图片或文件,content 结构不同,text 字段可能不存在,记得用 get 兜底。
5.5 TaoToken 调用返回 401 或 403
先确认环境变量是否真的导出成功:echo $TAOTOKEN_API_KEY。如果是在 systemd 或 Docker 里跑 OpenClaw,环境变量不会自动继承,需要在 service 文件或 compose 里显式声明。另外检查 Key 有没有多余空格,复制时容易带上换行。
5.6 事件重复推送
飞书在没收到你的成功响应前会重试。如果你的处理逻辑耗时较长,先返回 200 再异步处理,避免飞书判定超时重复推送。生产环境建议加一个基于 event_id 的去重表。
6. 下一步:把链路接进真实情报流
到这里,飞书 Webhook 到本地 Flask 的链路已经能跑,OpenClaw 的模型端点也验证通过。接下来要做的,是把 bridge.py 里那段转发逻辑真正接到 OpenClaw 的 ingest 接口上,让群消息触发 Agent 采集和整理,再把结果以卡片形式回推飞书群。
如果你在接入过程中卡在 Key 配置或端点地址上,直接去 API Keys 页面重新生成一个再试: https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。参数细节以接入文档为准: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先确认模型输出风格,用模型对话页试一句最快: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。长期跑 Agent 任务的话,Coding Plan 的额度模型更划算: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
下一篇会处理端口主权冲突和 OpenClaw 网关的并发启动问题,那是链路打通后第一个真正会咬人的坑。