OpenClaw 接入飞书 / 钉钉 / 企业微信:从 HTTP Webhook 到 WebSocket 长连接
关键词:OpenClaw、Feishu/Lark、DingTalk Stream、企业微信、HTTP Webhook、WebSocket、长连接、心跳、断线重连
适用人群:在做企业 IM 机器人接入、对比"回调 vs 长连接"该选哪个、想理解 OpenClaw
channels.feishu为什么不需要公网域名的开发者。
0. 一图看懂演进
企业 IM 的机器人接入方式,最近两三年正在经历一次明显的范式切换:从「平台主动推 → 开发者公网服务器接」的 HTTP Webhook,演进到「客户端主动连 → 平台反向推」的 WebSocket / Stream 长连接。具体节奏上,国内三家平台并不同步——钉钉 Stream 模式2023 年GA,飞书事件订阅 WebSocket SDK 在2023~2024 年陆续放出,企业微信截至 2026 年仍只提供 HTTP 回调;海外则早走了好几年,Discord 自2015 年起原生就是 WebSocket Gateway,Slack 在2021 年推出 Socket Mode,Telegram 至今还是 long-poll 为主。所以这并不是什么"十年长跑",而是2023 年起国内 IM 集体补课、到 2025 年趋于稳定的一次集中迁移,背后是「NAT 普及 + 内网部署需求 + 实时性要求 + 安全合规」几股力量同时推着走。
OpenClaw 作为多渠道 AI Agent 网关,在飞书插件@openclaw/feishu里直接放弃了传统 Webhook 模式,强制使用飞书官方的 WebSocket 事件订阅——这不是偷懒,而是这种"反向连接"模式恰好把 OpenClaw 大量部署在用户笔记本、家用服务器、公司内网这种"没有公网 IP"环境的痛点彻底解掉了。
下图把两种模式的核心差异并排放在一张图里:左边是传统 Webhook 必须翻越的"公网 IP / HTTPS 证书 / IP 白名单"三座山,右边是长连接"客户端主动建连 + 心跳保活 + 自动重连"的简洁结构。
关键要点(结合上图逐项对照):
- 方向相反:Webhook 是Platform → Server(平台主动推到你公网 URL),长连接是Client → Platform(你主动连到平台 WSS 端点),方向反转是一切差异的起点
- 可达性需求:Webhook 模式下你必须公网可达——独立 IP 或域名 + HTTPS 证书 + 备案(国内);长连接模式下你完全可以在 NAT 后面、家庭网络、Docker 容器里跑
- 失败回退:Webhook 推送失败时平台会重试,但重试有上限(飞书 3 次、钉钉 5 次),超时整条消息丢失;长连接断开后重连就行,不丢事件(平台侧有 broker 缓存)
- OpenClaw 选型:飞书插件直接锁死 WebSocket,文档明确写“connects OpenClaw to a Feishu/Lark bot using the platform’s WebSocket event subscription so messages can be received without exposing a public webhook URL”
设计取舍:长连接不是没有代价——它要求客户端持续维护 socket、跑心跳线程、处理断线状态机;但比起"必须有公网域名"这种部署门槛,前者是工程问题,后者是用户门槛问题。OpenClaw 选择把工程复杂度吃在自己代码里,把"零部署门槛"留给用户。
1. 早期:HTTP Webhook 模式(飞书旧版 / 钉钉旧版 / 企业微信至今)
1.1 它是怎么工作的
HTTP Webhook 是几乎所有 IM 开放平台第一代的设计——简单、直接、符合 HTTP 直觉:开发者在平台后台填一个Event Callback URL,平台把所有用户消息事件 HTTP POST 到这个 URL,开发者收到后处理并返回 200。钉钉机器人、企业微信在 2017 年、飞书机器人在 2018 年推出时都是这套范式,因为当时大家做的都是"部署在云 ECS 上的 PHP/Java 后台",公网 IP 唤手即来。
但实际接过的人都知道,Webhook 远不止"暴露一个 URL"这么简单。下图把一次正常 Webhook 投递的全链路拆开:
关键要点(结合上图逐步拆解):
- Step 0:URL 校验(图中未画):平台后台第一次保存 callback URL 时,会发一个
challenge请求,开发者必须原样返回challenge字段——证明你确实控制这个 URL,防止被人乱填 - Step 1:用户在 IM 客户端发消息 → 平台 IM 服务接收
- Step 2:平台事件中心把消息打包成 JSON,带上 HMAC 签名头(飞书
X-Lark-Signature/ 钉钉timestamp+sign/ 企微msg_signature)POST 到开发者 URL;payload 通常还会做一次AES 加密(企微EncodingAESKey/ 飞书encrypt_key) - Step 3:开发者服务器三件事必须做:① 校验 IP 是否在平台白名单 ② 用 secret 重算签名比对 ③ 用 AES key 解密 body 拿到明文事件
- Step 4:3 秒内必须返回 HTTP 200——否则平台认为你超时,会触发重试(最多 N 次后丢消息)
- Step 5:开发者用平台 Send API(HTTP)反向调用,把 LLM 生成的回复发回去——这一步和 Webhook 无关,永远是 HTTP
典型 URL 校验代码(飞书 v2 协议示例,所有平台大同小异):
deffeishu_callback(request):body=request.json()# URL 校验请求ifbody.get("type")=="url_verification":return{"challenge":body["challenge"]}# 签名校验timestamp=request.headers["X-Lark-Request-Timestamp"]nonce=request.headers["X-Lark-Request-Nonce"]signature=request.headers["X-Lark-Signature"]raw=(timestamp+nonce+ENCRYPT_KEY+request.body).encode()expect=base64.b64encode(hashlib.sha256(raw).digest()).decode()ifsignature!=expect:returnResponse(401,"bad signature")# AES 解密 + 业务处理event=aes_decrypt(body["encrypt"],ENCRYPT_KEY)handle_message(event)return{"code":0}设计取舍:Webhook 把"网络可达性"完全外包给开发者。平台只负责推,能不能收到是你自己的事——这对于"有专门后端团队 + 独立服务器"的场景没问题;但对"想把 AI 助手装在自己电脑上"的现代用法是灾难。
1.2 Webhook 的 6 大痛点
如果你只是接一个公司内部的考勤机器人,Webhook 完全够用。但当你想做一个像 OpenClaw 这样"装在用户笔记本上"的 Agent,下面这 6 个痛点会一个不漏地全部撞上:
关键要点(结合上图,每个痛点的真实场景):
- ① 公网域名 / IP:你必须有一个
bot.example.com这种可解析域名。家庭宽带的动态 IP?路由器后面的内网?Docker 容器?全都跑不了 Webhook - ② HTTPS 证书:所有主流 IM 平台强制要求 HTTPS(飞书甚至强制 TLS 1.2+),自签证书不收,必须 Let’s Encrypt 或 CA 签发证书 + 续期
- ③ IP 白名单:钉钉、企业微信会要求开发者把"自己服务器的出口 IP"加到平台白名单——但平台推过来的 IP 池也时常变动,你还得反查
event:ip_list这种文档 - ④ NAT 不友好:99% 的家庭/企业内网都在 NAT 后面,没有公网入口;只能上"frp / ngrok / cloudflare tunnel"这种隧道方案,又引入新的故障点
- ⑤ 重放攻击风险:Webhook URL 一旦泄露,攻击者可以截获 + 重放 payload;你必须自己实现
timestamp + nonce防重放(5 分钟窗口 + nonce 集合去重),漏一个就出事 - ⑥ 服务宕机 = 消息丢失:你机器关机 5 分钟、SSL 证书过期没续、Nginx 配置写错——这 5 分钟的所有用户消息永久丢失。平台侧不缓存,也不会"等你恢复了重发"
# 真实世界里 Webhook 部署常见的 4 件套1. 域名(~50 元/年)+ 备案(国内)+ 解析2. 服务器(~100 元/月)或 frp 内网穿透3. Let's Encrypt 证书自动续期(certbot)4. 防重放中间件(Redis 存 nonce, TTL 5min)设计取舍:上面这 4 件套对一个给最终用户用的开源工具而言完全是反人类的——OpenClaw 的目标用户是"想在自己笔记本上跑 Claude/GPT"的开发者,不是"运维"。这就是为什么飞书插件没有提供 Webhook 选项,只走长连接。
2. 现在:WebSocket / Stream 长连接(飞书 SDK / 钉钉 Stream)
2.1 长连接是怎么工作的
长连接模式把"消息推送"这件事的发起方掉了个个:不再是平台 → 服务器,而是客户端 → 平台。客户端用 App ID + App Secret 鉴权后,主动发起一个WSS(WebSocket over TLS)连接到平台的事件订阅端点(飞书wss://...feishu.cn/callback/ws/...,钉钉wss://wss-open-connection.dingtalk.com/connect),握手成功后这条 socket 就一直保持,平台有事件就从这条已经建好的隧道里推下来。
下图展示了这个"反向建连 + 持久隧道 + 双向心跳"的核心结构:
关键要点(结合上图逐层解释):
- WSS 握手:客户端先用
App ID + App Secret调一次 HTTP 接口换tenant_access_token(飞书)或accessToken(钉钉),然后带着 token 发起 WebSocket 升级请求GET /callback/ws/... HTTP/1.1+Upgrade: websocket - 持久隧道:握手成功后 socket 不关闭,平台侧把"原本要 POST 给 Webhook URL 的事件"直接以 WS frame 形式发过来,frame 内容仍然是 JSON(
im.message.receive_v1等) - 心跳保活:客户端每 20~30s 发一个
pingframe,平台回pong;如果连续 N 个心跳周期没收到对端回复,判定连接已死,立即触发重连 - NAT 穿透原理:因为是客户端主动发起的出站连接,NAT 设备会自动建立映射表,平台的回包顺着映射表回来——和 HTTP 出站请求是一个原理,完全不需要在 NAT 上打洞
- token 刷新:长连接本身不需要刷新 token,但断线重连时要拿当时的有效 token 重新握手,所以客户端得有 token 自动续期逻辑
飞书插件实际接收事件的伪代码(基于飞书官方 lark-oapi-py SDK,OpenClaw@openclaw/feishu里 TS 版本逻辑相同):
importlark_oapiaslarkdefon_p2p_message(data:lark.im.v1.P2ImMessageReceiveV1):msg=data.event.message user_id=data.event.sender.sender_id.open_id# 转交 OpenClaw AgentLoopagent_loop.dispatch(channel="feishu",peer_id=user_id,text=msg.content)ws=lark.ws.Client(app_id=APP_ID,app_secret=APP_SECRET,event_handler=lark.EventDispatcherHandler.builder().register_p2_im_message_receive_v1(on_p2p_message).build(),)ws.start()# 内部 = 握手 + 心跳 + 重连,全部托管设计取舍:长连接的复杂度全部沉到 SDK 里——ws.start()一行后面藏着握手 / token 刷新 / ping-pong / 重连 / 事件分发 / 消费确认的整套状态机。OpenClaw 直接复用官方 SDK,不自己实现协议——这是聪明的,别和平台官方 SDK 较劲。
2.2 三平台现状对比:飞书 / 钉钉 / 企业微信
虽然我们一直在说"演进到长连接",但三个平台的进度并不一致。这里是非常重要的事实分歧:
关键要点(结合上图,每平台一句话总结当前最佳实践):
- 飞书 / Lark:✅2023~2024 年陆续切到 WebSocket。开发后台「事件订阅」面板直接提供“使用长连接接收事件”选项,OpenClaw 文档原文“Choose Use long connection to receive events (WebSocket)”;旧的 HTTP 回调仍保留兼容,但官方推荐长连接。截至 2026 年这条路径已稳定运行 2~3 年
- 钉钉:✅2023 年推出 Stream 模式(WebSocket)。SDK 包名
dingtalk-stream,端点wss-open-connection.dingtalk.com/connect;但老应用大量仍在用 outgoing webhook(/v1/robot/send反向 HTTP);钉钉同时支持两种,新接入推荐 Stream - 企业微信(WeCom):⚠️截至 2026 年仍是 HTTP 回调模式。
receive message API必须配置回调 URL + Token + EncodingAESKey,AES 加密 + SHA1 签名;没有公开的长连接订阅 SDK——这是企业微信开放平台节奏偏保守的体现 - Google Chat:⚠️ 仅 HTTP webhook,OpenClaw
googlechat.md明确写“via HTTP webhook” - Telegram:✅ 通过
getUpdateslong-polling(特殊形态的"长连接")或 webhook 二选一;OpenClaw 通过 grammY 默认走 long-poll - Discord / Slack:✅ 原生就是 WebSocket(Discord Gateway / Slack Socket Mode),从来没用过 webhook 接消息
平台 推荐模式 OpenClaw 接入状态 -------------------------------------------------------------------- Feishu/Lark WebSocket (im.message.*) ✅ 官方插件 @openclaw/feishu DingTalk Stream (WSS) ⚠️ wiki 暂未列入官方 channel WeCom HTTP callback (无替代) ⚠️ wiki 暂未列入官方 channel Telegram long-poll / webhook ✅ grammY 走 long-poll Discord Gateway WSS ✅ Discord.js Slack Socket Mode WSS ✅ Bolt SDK设计取舍:企业微信至今没出长连接 SDK,反映了它定位是"严管严控的企业 IT 渠道"——必须有公网服务器、必须备案、必须白名单,反而是它要的安全模型;而飞书定位"现代协作 + 开发者友好",钉钉定位"中小企业自动化",两者都需要降低接入门槛,所以走长连接是必然选择。
3. OpenClaw 网关侧:把"渠道差异"统一抽象掉
不同 IM 平台的接入方式天差地别——飞书用 WSS、企微用 HTTP 回调、Telegram 长轮询、Discord 原生 Gateway——但 OpenClaw 的AgentLoop(即"决定怎么回话"的核心循环)不应该感知这些差异。这就是src/channels/*这一层抽象存在的全部理由。
下图展示了 OpenClaw 的 channel 分层结构:
关键要点(结合上图,自顶向下解读):
- 顶层 AgentLoop:只关心“有人发了一条消息,我用 LLM 生成回复”,不知道也不需要知道消息从哪个平台来
- Channels 抽象层:定义统一的
Message信封——{channel, peer: {kind, id}, text, attachments, ts};每个平台 provider 进来的事件都被翻译成这个信封 - Provider 多态:每个平台一个 provider 模块,内部封装该平台特有的接入方式:
feishuprovider → 飞书官方 WS SDK 启动 + 事件回调telegramprovider → grammY 的Bot.start()(long-poll)或webhookCallback(HTTP)whatsappprovider → Baileys 模拟客户端(QR 配对 + 持久 WS)discordprovider → Discord.js Gatewaygooglechatprovider → 内置 HTTP server 等回调
- bindings 路由:上层用
bindings: [{agentId, match: {channel, peer}}]决定"这条 Feishu 群消息走哪个 agent"——这个匹配只看抽象信封,不看协议细节 - outbound send:发回消息时同样统一接口
provider.send(envelope),每个 provider 内部再调平台 Send API
OpenClaw 飞书插件的 binding 配置真例(来自feishu.md):
{ agents: { list: [{ id: "main" }, { id: "clawd-fan", workspace: "..." }] }, bindings: [ { agentId: "main", match: { channel: "feishu", peer: { kind: "direct", id: "ou_xxx" } } }, { agentId: "clawd-fan", match: { channel: "feishu", peer: { kind: "group", id: "oc_zzz" } } }, ], }设计取舍:OpenClaw 没有发明新的"统一 IM 协议"(这是个反复有人尝试但都失败的方向,例如 Matrix Bridge),而是承认"每个平台 SDK 是事实标准",只在自己代码侧做一层信封翻译。复杂度藏在 provider 内部,不污染核心循环——这是工程上务实的选择。
4. 长连接的硬骨头:心跳 + 断线重连状态机
长连接看起来很美好,但工程实现上最容易翻车的就是心跳 + 重连。一个不注意,要么心跳太频繁烧服务器、要么重连风暴打挂平台、要么"假死连接"导致几小时收不到消息但日志一切正常。
下图是飞书 / 钉钉 / Discord / Slack 等所有长连接渠道通用的状态机模板:
关键要点(结合上图,每个状态的语义和必踩的坑):
Connecting:握手中,已发出 HTTPUpgrade: websocket请求;超时阈值典型 10s,超时直接转入Backoff retryConnected:socket 已建立,进入心跳循环;这时必须立即把订阅事件类型告诉平台(飞书发subscribe帧、钉钉发pingResponse),漏了平台不会推任何消息Heartbeat OK:每 30s 客户端发pingframe(飞书 SDK 默认 25s),平台回pong;关键技巧:要用应用层 ping,不要只依赖 TCP keepalive——TCP keepalive 在 Linux 默认 2 小时才生效,IM 场景不可接受Disconnected:连续 2 个心跳周期没收到pong,或TCP socket 直接 RST/FIN;这时不能立即重连,否则会形成重连风暴Backoff retry:指数退避1s, 2s, 4s, 8s, ...上限 30s,加 ±20% 随机抖动避免羊群效应;重要:达到最大次数(典型 10 次)后要触发告警,不能无限重连掩盖配置错误(比如 secret 错了)
典型实现伪代码(生产级简化版):
asyncfunctionrunWithReconnect(){letattempt=0;while(true){try{constws=awaitconnectWithToken();// Connectingattempt=0;// 成功后重置startHeartbeat(ws,30_000);// Connected -> Heartbeat OKawaitws.waitForClose();// 阻塞直到断开}catch(err){log.warn("ws disconnected",err);}// Backoff retryconstdelay=Math.min(30_000,1000*2**attempt)*(0.8+Math.random()*0.4);attempt=Math.min(attempt+1,10);if(attempt===10)alertOps("ws reconnect storm");awaitsleep(delay);}}functionstartHeartbeat(ws,interval){letmissed=0;setInterval(()=>{ws.ping();setTimeout(()=>{if(!ws.lastPongWithin(interval)){missed++;if(missed>=2)ws.terminate();// 主动断开触发重连}else{missed=0;}},interval-1000);},interval);}设计取舍:心跳间隔不能太短(30s 是工业界共识,太短烧电池/烧服务器)也不能太长(>60s 移动网络 NAT 表项可能已经被运营商回收,回包回不来);指数退避必须加抖动,否则一千个机器人同时断线会同时重连把平台打死——这是分布式系统的基本功,但每年都有团队栽在这。
5. 总结:什么时候用什么
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 公司内部 IT 后台机器人,已有公网服务器 | HTTP Webhook | 简单直接,既有运维能 cover |
| 给最终用户的桌面/笔记本 AI 助手 | 长连接 | 零部署门槛,用户不需要懂运维 |
| 需要超低延迟(流式输出) | 长连接 | 服务端推送无需轮询,延迟 <100ms |
| 平台只支持 Webhook(如企微) | HTTP Webhook + 内网穿透 | 没有别的选择,frp / cloudflare tunnel 兜底 |
| 高并发集群 | 视情况 | Webhook 易水平扩;长连接需要 sticky session |
OpenClaw 的选型逻辑很清晰:它的目标用户是"在自己机器上跑 Agent"的开发者和最终用户,所以只要平台提供长连接选项就坚决用长连接——飞书插件锁死 WebSocket,Telegram 默认 long-poll,WhatsApp 走 Baileys WS。只有像 Google Chat 这种纯 webhook 平台才退而求其次。
理解了这套演进逻辑,再回头看 OpenClaw 文档里那句不起眼的提示——“Use long connection to receive events”——你就会明白:这不是一个无关紧要的勾选项,而是 OpenClaw 整个"零部署门槛"产品定位的技术地基。
