聊点实在的。最近忙完的一件事,是把我自己维护的那套 OpenClaw AI 助手网关同时接进了 QQ-Bot 和飞书。原本 QQ 助手和飞书助手是两套互相独立的代码,一个跑在定时脚本里,一个挂在群机器人上,技能逻辑七零八落,改个提示词都要动两处。折腾完这次接入之后,同一个大脑、同一套工具函数,两个入口直接共用,省了差不多一半的维护量。
OpenClaw 本身做的事情很简单:它不负责写对话,也不负责跑模型,而是一个消息网关。QQ 来一个事件,飞书来一个事件,它统一翻译成内部的会话消息,再交给你的智能体逻辑去处理。对不熟悉这套结构的朋友来说,你完全可以把它想象成一块转接板,左边插着各种聊天平台,右边插着你的 Agent 核心。这篇文章就围绕我从零把 OpenClaw 接入 QQ 和飞书的完整过程展开,重点不是复读文档,而是把我在配置、调试、处理回调、踩坑过程中觉得最值得记录的细节写出来。如果你正准备给团队或自己搭一个多平台可用的机器人,这篇应该能帮你少走不少弯路。
1. OpenClaw 是什么,以及我为什么非要同时接 QQ 和飞书
1.1 从"一个助手,两个入口"说起
很多人的 AI 机器人都是按平台分开养的:QQ 上一个脚本,飞书上一个机器人,两边各自维护一套提示词和工具调用。我最初也是这个状态,因为 QQ 和飞书的开放接口差异很大,事件结构不一样,消息类型不一样,就连最简单的"发送一条文本消息"调用的接口都完全不同。结果是每次想加点新能力,比如查天气、查数据库、生成周报,都得在两边各写一遍,甚至因为两边工具封装不一致,跑出来的结果格式都对不上。
后来我意识到一个很朴素的道理:智能体本身和消息平台应该是解耦的。用户从哪个聊天窗口发消息,不该影响我如何处理这条消息;机器人要具备哪些工具,也不该因为平台不同就重写一套。所以我需要一个中间层,把平台差异挡在外面,让核心逻辑只关心"收到了一段文本,需要返回一段文本或者触发一个动作"。
OpenClaw 就是这个中间层的落地实现。它安装好之后,每个平台对应一个 adapter,比如qq适配器和feishu适配器。这些 adapter 负责监听平台的回调、解析消息、调用鉴权、发送回复。我的业务代码只需要注册一个handle_message函数,OpenClaw 会自己判断消息从哪来、应该怎么回。这样我就有了一个只在核心层写一遍、同时服务于两个平台的统一入口。
1.2 OpenClaw 在消息平台接入中的角色
如果你之前用过类似的消息网关,可能对"Webhook + 事件上报"这套模型不陌生。OpenClaw 在每个平台侧都扮演两个角色:对外是回调接收端,对内是事件分发器。以 QQ-Bot 为例,QQ 平台把群消息、私聊消息、好友添加等事件通过 HTTP 回调推送到你配置的 URL,OpenClaw 在收到后先做签名校验,再转成标准化的内部事件对象。飞书一侧也是同样的思路,只不过飞书的回调机制更繁琐一点,需要先通过 URL 验证,再按事件类型订阅。
直接说结论:用 OpenClaw 接两个平台,你的业务代码只需要面对两种输入。一种是InternalMessage,包含platform、chat_id、user_id、text等统一字段;另一种是InternalActionResult,代表工具执行之后的反馈。你不需要关心 QQ 的sender.openid和飞书的sender.sender_id.open_id到底有什么不同,因为 OpenClaw 在 adapter 层已经把它们映射成了同样的结构。
这样设计还有一个隐藏好处:智能体的上下文管理可以和平台无关。我在 OpenClaw 上配置了基于 Redis 的会话存储,session key 是平台前缀 + 用户 ID + 会话场景。QQ 私聊和飞书私聊各自独立,但底层用的是同一套上下文管理代码。也就是说,模型该记住什么、该遗忘什么,完全由核心逻辑决定,不会因为今天从 QQ 进来还是从飞书进来就有差异。
2. 接入前的准备:账号、协议、环境三件套
2.1 QQ 侧的两种接入路线怎么选
QQ 的机器人接入路径比较特殊,因为市面上实际有两类方案在并行使用。一类是 QQ 官方机器人开放平台,走的是正规的开发者审核流程,申请应用拿到app_id和app_secret,然后配置回调地址。这类方案接起来干净,接口稳定,但限制也比较多,比如私聊消息的获取需要特定权限,部分类目需要企业主体认证,个人开发者能申请到的接口范围有限。
另一类是社区协议实现,比如常见的 go-cqhttp 以及它的各种现代分支。它们的原理是模拟一个客户端登录,然后把消息事件通过正向或反向 WebSocket 转发给你。这套方案功能更全,能发文件、能主动拉取历史消息、能执行更多群管理操作,但对账号来说存在一定风险,而且紧跟协议变化需要经常更新。
我这次的选择比较务实:主用社区协议做内部群的自用机器人,同时保留了官方通道的适配器配置备选。如果你是为了给外部用户提供正式服务,我不建议走社区协议,账号安全和被封禁风险都是实打实的;但如果你只是给自己所在的小团队用,社区协议的便捷性非常明显。OpenClaw 里这两种可以通过type: qq-official和type: qq-community切换,底层 adapter 不同,但上行业务逻辑完全不用改。
2.2 飞书自建应用的创建与权限预分配
飞书这边的准备工作相对标准。在飞书开放平台里创建一个企业自建应用,然后在"应用能力"里开通机器人能力。你需要记下App ID和App Secret,这两个值在后续配置里要填到 OpenClaw 的feishu适配器里。
权限这块我要单独提醒一句,尽量遵循最小权限原则。我最初图省事,一次性申请了十几个权限,结果后台审核直接把我拦住,要求说明用途。后来收敛到三个核心权限:接收消息、发送消息、读取用户基本信息。注意飞书不同的权限范围对应不同的事件订阅,比如想接收群消息就得订阅im.message.receive_v1,想读取用户组织信息就得再加contact:user.base:readonly。OpenClaw 的飞书 adapter 会帮你检查事件订阅是否匹配,但权限申请还是要自己动手。
还有一个小细节:飞书应用创建之后默认是测试版,需要先发布版本才能被非开发者成员使用。如果你只是自己调试,可在"版本管理与发布"里创建一个测试版本,然后在"可用成员"里添加测试成员即可。否则你拿小号发消息给机器人,机器人根本收不到。
2.3 本机环境的依赖与回环地址准备
OpenClaw 本身是用 Node.js 写的运行时,我在部署前先确认了两件事:Node.js 版本高于 18,Redis 已经可用。Redis 在这里不是可选项,因为 OpenClaw 的会话锁、事件去重都用到了它。我用的是系统自带的redis-server,配置文件里指向localhost:6379就够用。
最麻烦的是回调地址的连通性。QQ 和飞书都要求回调地址是公网可访问的 HTTPS 地址,开发阶段我没法直接上生产环境,就用内网穿透工具把本机的7000端口映射到了一个临时域名。这个环节有个很容易踩的坑:QQ 后台和飞书后台在保存回调地址时,都会主动发起一次请求验证,如果你的本地服务还没启动,后台会提示"URL 验证失败"。所以正确顺序是先把 OpenClaw 拉起来,确认日志里显示 adapter 在监听,再去后台填 URL。
生产环境我直接用 Nginx 反代到本机端口,同时把 TLS 证书配好。OpenClaw 本身也支持直接配置证书,但我不太建议让应用进程直接持有证书文件,交给 Nginx 统一管理,重启服务时不用操心 443 端口的权限问题。
3. QQ-Bot 接入实操:Webhook 与事件上报的完整走读
3.1 生成密钥与配置回调地址
QQ 这头的配置写起来比较直白。我的claw.yaml里大概是这样的:
platforms: qq: type: qq-community nickname: "helper-bot" reverse_ws_url: "ws://127.0.0.1:8080/ws" access_token: "your-access-token" message_timeout: 30如果你用的是官方通道,配置略有不同:
platforms: qq: type: qq-official app_id: "1023xxxx" app_secret: "xxxxxxxx" token: "callback-sign-token" callback_url: "https://bot.example.com/openclaw/qq/callback"重点解释下token和app_secret的区别。token是回调签名用的,QQ 服务器在推送事件时,会把token、timestamp、nonce三个值拼接后算 SHA1,放在X-Tencent-Signature请求头里。OpenClaw 收到请求后会自己复算一遍,用来确认事件确实来自 QQ。而app_secret是业务接口签名用的,比如主动调 QQ 的接口发消息时,需要用它生成 JSAPISignature。这两个值在概念上不能混,我在刚开始配置时一度把token填成了app_secret,结果回调校验一直失败,日志里全是signature mismatch。
社区协议通道相对省事一点。OpenClaw 作为客户端去连 go-cqhttp 的 WebSocket 服务,不需要关心 HTTP 回调的签名,只需要在access_token里配置一个双方约定好的口令。这个口令同样不能随意填,必须在 go-cqhttp 的配置文件中保持一致,不然握手阶段就会被拒绝。
3.2 把 OpenClaw 的 QQ 适配器跑起来
启动命令比我想象中简单:
openclaw start --config claw.yaml看到日志输出类似下面几行,基本说明 QQ 适配器已经就绪:
[openclaw] starting adapter: qq [openclaw] qq adapter connected to ws://127.0.0.1:8080/ws [openclaw] session manager connected to redis [openclaw] agent core loaded: my-assistant这里我额外强调一个容易被忽略的点:如果连的是社区协议的 WebSocket,OpenClaw 默认是客户端模式,也就是 OpenClaw 主动去连 go-cqhttp。此时 go-cqhttp 的post-message-format最好设置成array,因为string格式下 CQ 码解析容易出错,而array格式会把消息拆成一段段结构化的消息段,OpenClaw 解析起来更准确。改完这个配置记得重启 go-cqhttp,否则你发的图片、@消息都可能在解析层丢失。
3.3 本地联调:私聊、群聊、@触发一次打通
适配器起来之后,我最先做的是拿小号给机器人发一条私聊消息。这里有一个很实用的调试习惯:我一开始不会直接让 OpenClaw 调用任何外部工具,而是先注册一个简单的echo命令,让它原样返回消息内容。这样可以快速验证消息链路是通的,而不是一上来就排查模型调用的报错。
echo通了之后,再去群里测试 @ 触发。QQ 群的机器人只有在被 @ 或者设置了关键词触发的情况下才会收到事件,这是我最初没预料到的。OpenClaw 内部有个配置项叫mention_only,默认是true。如果你希望机器人在群里主动响应所有消息,得显式把这个值改成false。但我不建议这么做,群消息的噪声远大于有效指令,保持只响应 @ 或前缀指令,对用户体验更友好。
联调通过后,还有一件容易被忽略的事:记录群聊和用户的 OpenID。OpenClaw 日志里会把每条消息对应成一个 session key,比如qq:group_xxxx:user_yyyy。后面要让机器人定时往特定群推消息,需要用到这个 key,建议提前在配置里做好映射,别等要用的时候满日志找。
4. 飞书接入实操:事件订阅、卡片消息与租户隔离
4.1 应用凭证与事件订阅 URL 配置
飞书这边的配置结构跟 QQ 有些差异,但理念一致。在飞书开放平台后台的"事件订阅"页面,先添加订阅事件,比如im.message.receive_v1,然后把请求地址 URL 填成https://bot.example.com/openclaw/feishu/callback。
对应的claw.yaml配置是这样:
platforms: feishu: type: lark app_id: "cli_xxxxxxxxx" app_secret: "xxxxxxxxx" verification_token: "xxxxxxxxx" encrypt_key: "" callback_url: "https://bot.example.com/openclaw/feishu/callback"这里有个很容易误解的地方:飞书的事件订阅配置里有Verification Token和Encrypt Key两个概念。Verification Token是明文请求用来验证身份的,OpenClaw 收到请求后会比对它和后台配置是否一致,不一致直接拒绝。Encrypt Key只有在你开启了加密回调才需要填。如果你在后台开启了"加密"选项,但 OpenClaw 配置里encrypt_key留空,那回调请求会解不开,日志里会一直报decrypt failed。我的建议是开发阶段先不开加密,通了以后再加上,这样可以少一个变量。
4.2 处理飞书特有的 challenge 校验
飞书最大的特点,或者说考验,是在你保存事件订阅 URL 时会先执行一次 URL 验证。后台会随机生成一个challenge参数,通过 HTTP 请求发给你的回调地址。你的服务必须原样返回{"challenge": "xxx"},并且状态码为 200,后台才认为 URL 可用。
第一次配置的时候我犯了个错:我把 challenge 当成一个固定值写死在代码里了。结果后台保存时返回"URL 验证失败"。原因是飞书的每个 challenge 都是临时生成的,后一次永远不等于前一次,必须动态读取请求 body 里的值然后返回。
OpenClaw 的适配器已经内置了这个处理逻辑,所以我最终没有手动实现。但如果你哪天需要自己写入口,请一定记住这点。有一个相关的细节是,飞书验证 URL 时往往同时发送 GET 和 POST 两种请求,虽然标准文档里写的是 POST,但我在实测中遇到过 GET 类型的挑战请求,所以你的路由需要同时接受两种方法,或者至少不要对 GET 直接返回 404。
4.3 消息卡片与文本消息的格式差异
飞书的富文本能力强于 QQ,代价是格式转换更麻烦。QQ 那边基本发纯文本和图片就够了,而飞书群里大家习惯看卡片消息,带标题、带分割线、带按钮那种。OpenClaw 内部维护了一个"消息输出"的抽象层,插件返回一个普通字符串时,飞书 adapter 会包成简单的text消息发出去;如果插件返回的是结构化 JSON,适配器可以按模板转成interactive卡片。
这是功能上的自由度,但也带来了麻烦。同样的消息内容,在 QQ 里是一行朴素的文本,在飞书里如果你不做处理,就只是一行更朴素的文本,看起来不够精致。我后来写了一个小函数,专门判断当前平台是不是飞书,是的话就把 Markdown 风格的段落转成飞书卡片结构,在关键信息上用lark_md标签修饰。
需要注意飞书卡片消息的发送接口和普通文本消息不一样,OpenClaw 会自动选择,但你如果自己在飞书后台测试时需要明确:POST 到/im/v1/messages时,msg_type为text或interactive,这个字段决定了消息渲染方式,填错了会报参数错误。
5. 统一大脑分散入口:多平台消息路由与状态同步
5.1 平台 ID 到 OpenClaw 会话的映射
两块平台接入都跑通后,我做的第一件事不是加新功能,而是检查会话映射是否正确。OpenClaw 的会话 key 规则在日志里很容易看清:
- QQ 私聊:
qq:user_openid:private - QQ 群聊:
qq:group_id:user_openid - 飞书私聊:
feishu:ou_xxx:private - 飞书群聊:
feishu:oc_xxx:ou_xxx
默认情况下,每个平台、每个用户、每个会话场景都是独立的 session。就是说,同一个用户先去 QQ 私聊机器人,又跑去飞书私聊机器人,OpenClaw 默认不会把这两段对话混在一起。这个设计很合理,因为两个平台的使用场景不同,历史上下文混在一起容易造成误导。
但是如果你确实希望一个人在两个平台共享同一份记忆,OpenClaw 的配置里有个session_merge_rules可以控制。我试过这个功能,最后没有生产使用,因为跨平台合并要求两边用户 ID 有对应关系,实际很难维护。我给自己的结论是:不做合并,但让用户主动用/remind这类指令把关键信息存进长期记忆,需要的时候再调出来。
5.2 消息去重与多播问题
多平台接入之后,有一个问题会变得非常突出:消息重复触发。这里有两个来源。第一个来源是平台本身的重发机制,比如飞书在没收到 200 响应时会重试推送事件;第二个来源是业务侧的多播,比如你在 QQ 群里让机器人"同步发送到飞书",那么同一条指令可能会在两个平台各执行一次。
我的处理办法是引入一个去重 ID。OpenClaw 的 adapter 在收到事件时会为每个事件生成一个event_id,QQ 社区协议通常用message_id,飞书用message_id也可以。我在核心逻辑里加了一层 Redis 锁:
import redis r = redis.Redis(host="localhost", port=6379, db=0) def acquire_event_lock(event_id, ttl=5): return r.set(f"event_lock:{event_id}", "1", nx=True, ex=ttl)只有拿到锁的那个分支才继续执行有副作用的操作,比如调数据库、发 API 请求,其余分支直接丢弃。这样做以后,飞书重试导致的重复消息明显减少。有一点要特别注意,锁的 TTL 不能设太短,否则一个真正耗时超过 5 秒的工具调用执行到一半,锁就过期了,平台的重试又进来,还是会造成重复。我把 TTL 设成 30 秒,足够了。
5.3 机器人主动推送的权限差异
接入之后,我还需要让机器人具备主动推送能力,比如每天早上向指定群里汇报一次线上状态。这里就发现了多平台之间很大的不对等。
QQ 社区协议可以比较自由地主动发私聊和群消息,但官方 QQ 开放平台对主动消息有严格的频次限制,单日单用户最多几次,超出会被处罚。飞书这边相对宽松,只要机器人有发文权限,主动发消息给用户是常见操作。OpenClaw 里配置一个定时触发的工具后,我额外加了一个platform_quota字段,用它来限制主动推送的频率。
举个具体的例子:我的定时周报任务同时配置了 QQ 群和飞书群,但飞书群里可以完整发卡片周报,QQ 群只能发文本摘要,且每天最多推两次。这个限制写在一个很薄的配置层里:
notify: qq: max_per_day: 2 format: text feishu: max_per_day: 10 format: card别觉得这是过度设计,实际跑起来你会发现,平台规则不是写在文档里给你看看的,而是你推送多了真会受到限制,到时候排查问题的时间远超一开始写好配置的时间。
6. 踩坑清单:事件回调重复、超时换签、卡片权限不足
6.1 腾讯回调的签名头到底校验哪几个
第一轮排查耗时最久的坑,是 QQ 官方回调的签名校验。我用社区协议时没有这个问题,一切正常;切到qq-official后,回调 URL 后台保存成功,但事件推送过来,OpenClaw 日志一直报签名失败。
后来我抓了完整的请求头,发现 QQ 回调带了三个关键 Header:X-Tencent-Timestamp、X-Tencent-Nonce、X-Tencent-Signature。签名计算的规则是把token、timestamp、nonce三个字符串拼接后做 SHA1。我一开始漏掉了token在拼接中的位置,只用了timestamp + nonce,自然对不上。
还有一个隐患是重放攻击。就算签名是对的,如果不去校验timestamp的新鲜度,攻击者可以把很久以前的回调原样重发,OpenClaw 仍然会当作新事件处理。我在自己的中间件里加了一个五分钟的超时窗口,超过当前时间五分钟的一律丢弃。OpenClaw 自带配置项里也有max_ts_skew,建议把它设成 300。
6.2 飞书 URL 验证与事件重放
飞书这边最折磨人的是事件重放机制。我在调试一个耗时较长的 Agent 任务时,飞书后台因为没在 1.5 秒内收到 200,就把同一个事件重发了三次,结果任务被重复执行了三次。
这个坑的直接解法是"先收下,再异步处理"。OpenClaw 的 adapter 其实已经做了这件事,它在收到回调后会立刻返回 200,然后把事件放入内部队列,让 Worker 慢慢处理。但如果你绕过 OpenClaw,自己写了一个同步调用模型的接口,就很容易踩这个超时红线。
另外,飞书的重试事件带有相同的event_id,业务代码里必须做幂等判断。我刚才提到的 Redis 锁在这里再次派上用场。除此之外,我建议把飞书的日志打开,记录每条事件的event_id和处理状态,否则一旦出现重复,很难定位是哪一次请求造成的副作用。
6.3 我最后保留的生产配置模板
折腾完一圈,我最想给后来者看的,是一份我目前还在用的精简配置。它不代表所有场景最优,但至少避开了大多数坑:
server: port: 7000 auth_token: "openclaw-server-token" session: storage: redis redis_host: "localhost" redis_port: 6379 ttl_seconds: 3600 platforms: qq: type: qq-community nickname: "helper-bot" reverse_ws_url: "ws://127.0.0.1:8080/ws" access_token: "your-access-token" mention_only: true feishu: type: lark app_id: "cli_xxxxxxxx" app_secret: "xxxxxxxx" verification_token: "xxxxxxxx" encrypt_key: "" callback_url: "https://bot.example.com/openclaw/feishu/callback" dedupe: enabled: true redis_prefix: "dedupe:event" ttl_seconds: 30 notify: qq: max_per_day: 2 format: text feishu: max_per_day: 10 format: card这份配置跑了好几个星期,稳定性还可以。中间只因为 go-cqhttp 的 WebSocket 连接断开重启过一次,OpenClaw 会自动重连,但重启间隔内收到的消息会丢,所以我在部署脚本里加了守护进程,保证服务异常退出时能快速拉起。
如果再让我重新选一次,我仍然会优先把核心逻辑放在 OpenClaw 的"平台无关层",而不是直接调两个平台的 SDK。原因很简单:平台 SDK 是在变的,回调协议偶尔微调,接口字段也可能升级,而 OpenClaw 把这些变化挡在了 adapter 后面。我只要守住claw.yaml和业务层,大多数平台变动发生时,我只需要更新一下 OpenClaw 版本,业务代码几乎不用动。
最后分享一个实际操作中的小建议。在跑通第一个 Echo 指令之前,别急着接模型、别急着配工具,先把"收到消息 -> 返回固定文本 -> 两边平台都能看到"这条链路稳定跑起来。因为多平台调试的复杂度是叠加的,QQ 和飞书任何一个环节出问题,都会让你的排查范围翻倍。把链路切成一小段一小段地验证,比一次性全部接通要可靠得多。这套工程习惯,值得在你自己接入新平台时同样坚持。