首页
/ CowAgent 飞书 Channel 实战:Webhook 与 WebSocket 双模式事件接入、消息去重与故障排查

CowAgent 飞书 Channel 实战:Webhook 与 WebSocket 双模式事件接入、消息去重与故障排查

2026-09-05 23:46:06作者:董灵辛Dennis

CowAgent 的飞书 Channel 支持 Webhook(HTTP 回调)WebSocket(长连接) 两种事件接收模式,可在同一份配置中通过一个开关自由切换。本文基于仓库中的 channel/feishu/README.md 完整继承其配置项说明、两种模式的接入步骤、平滑迁移与故障排查方法,并结合 channel/feishu/feishu_channel.py 的源码实现,进一步讲清事件分发、URL 验证、Token 校验、消息去重与离线消息过滤的底层机制。读完你可以独立完成飞书机器人的部署与排障,并理解每条配置项在源码中的真实作用。

两种事件接收模式的选择

飞书开放平台提供两种事件推送方式,CowAgent 对两者都做了封装,并通过 feishu_event_mode 配置项选择:

模式 适用场景 优点 缺点
webhook 生产环境 稳定可靠,官方推荐 需要公网IP或域名
websocket 本地开发 无需公网IP,开发便捷 需要额外依赖(lark-oapi

从源码结构看,两种模式在启动阶段分叉,之后汇入同一套消息处理逻辑:startup() 中根据 feishu_event_mode 决定调用 _startup_websocket() 还是 _startup_webhook()(见 channel/feishu/feishu_channel.py),两者的消息最终都进入共用的 _handle_message_event() 方法(源码注释明确写着“webhook和websocket模式共用此方法”)。这意味着模式切换只影响“事件怎么进来”,不影响“事件怎么被处理”。

基础配置与配置项详解

config.json 中添加以下基础配置:

{
  "channel_type": "feishu",
  "feishu_app_id": "cli_xxxxx",
  "feishu_app_secret": "your_app_secret",
  "feishu_token": "your_verification_token",
  "feishu_bot_name": "你的机器人名称",
  "feishu_event_mode": "webhook",
  "feishu_port": 9891
}

配置项说明(默认值已与源码核对):

配置项 说明 默认值
feishu_app_id 飞书应用的 App ID(一般形如 cli_xxxxx 无,必填
feishu_app_secret 飞书应用的 App Secret 无,必填
feishu_token 事件订阅的 Verification Token,用于校验 webhook 回调来源
feishu_bot_name 机器人名称,用于群聊 @ 判断的兜底匹配
feishu_event_mode 事件接收模式:"websocket"(长连接)或 "webhook"(HTTP 服务器) "websocket"
feishu_port webhook 模式下的 HTTP 服务端口 9891

源码中这几项的读取方式值得注意(channel/feishu/feishu_channel.py):

feishu_event_mode = conf().get('feishu_event_mode', 'websocket')  # webhook 或 websocket

不配置 feishu_event_mode 时默认走 WebSocket 长连接模式,这也是 README 中“websocket:长连接模式(默认)”的出处。

另外两点源码层面的补充:

  1. 群聊 @ 判断优先使用 open_id,feishu_bot_name 只是兜底。Channel 启动时会调用飞书 /open-apis/bot/v3/info/ 接口拉取机器人自身的 open_id 并缓存(_fetch_bot_open_id());_is_mention_bot() 依次按 open_id 精确匹配 → feishu_bot_name 名称匹配 → 默认认为被 @ 的三级策略判断(见 channel/feishu/feishu_channel.py)。因此即使不配置 feishu_bot_name,群聊 @ 识别也能正常工作。
  2. 凭据可自动写回配置文件。若启动时发现 feishu_app_id / feishu_app_secret 缺失,命令行场景下会尝试通过 lark_oapiregister_app 能力在终端打印 ASCII 二维码,用飞书 App 扫码一键创建应用并自动把凭据持久化到 config.json(要求 lark-oapi >= 1.5.5);Desktop 模式下 SDK 缺失时还会按需拉取裁剪版 SDK 包(见 channel/feishu/lark_install.pychannel/feishu/feishu_channel.py 的懒加载注释)。

模式一:Webhook 模式(推荐生产环境)

1. 配置

{
  "feishu_event_mode": "webhook",
  "feishu_port": 9891
}

2. 启动服务

python3 app.py

服务将在 http://0.0.0.0:9891 启动。从源码看,webhook 模式由 _startup_webhook() 基于 web.py 的 WSGIServer 实现(channel/feishu/feishu_channel.py):

def _startup_webhook(self):
    """启动HTTP服务器接收事件(webhook模式)"""
    urls = ('/', 'channel.feishu.feishu_channel.FeishuController')
    app = web.application(urls, globals(), autoreload=False)
    port = conf().get("feishu_port", 9891)
    server = web.httpserver.WSGIServer(("0.0.0.0", port), ...)

也就是说:监听地址固定为 0.0.0.0(对外网卡全部可访问),端口从 feishu_port 读取且默认 9891,所有回调都路由到 FeishuController

3. 配置飞书应用

  1. 登录飞书开放平台;
  2. 进入应用详情 -> 事件订阅;
  3. 选择 将事件发送至开发者服务器
  4. 填写请求地址:http://your-domain:9891/
  5. 添加事件:im.message.receive_v1(接收消息 v2.0)和 im.message.recalled_v1(消息撤回);
  6. 保存配置。

4. 回调端点内部做了什么

FeishuControllerchannel/feishu/feishu_channel.py)实现了完整的回调协议,理解它对排障很有帮助:

  • GET 请求:直接返回 "Feishu service start success!",可用它做服务存活检查;
  • url_verification 挑战:飞书首次保存请求地址时会发一次验证请求,Controller 原样回显 {"challenge": ...} 完成握手;
  • Token 校验:对消息事件校验 header.token、卡片事件兼容 event.token / 顶层 token,与配置中的 feishu_token 不一致时返回 {"success": false} 直接拒绝。这就是 feishu_token 必须与开放平台“事件订阅”中的 Verification Token 保持一致的原因;
  • 事件分发:识别三种事件类型——im.message.receive_v1(收消息)、im.message.recalled_v1(消息撤回)、card.action.trigger(卡片按钮回调,用于定时任务卡片交互),分别转发给对应处理函数,成功后返回 {"success": true}

5. 注意事项

  • 需要有公网 IP 或域名;
  • 确保防火墙开放对应端口;
  • 建议使用 HTTPS(需要配置反向代理)。

模式二:WebSocket 模式(推荐本地开发)

1. 安装依赖

pip install lark-oapi

源码对该依赖做了懒加载:import lark_oapi 会拉入上万源文件、耗时 4~10 秒,因此被推迟到首次真正需要时才导入(见 channel/feishu/feishu_channel.py)。__init__ 中若模式为 websocket 且依赖缺失,会抛出明确的 lark_oapi not installed / could not be installed 错误,对应故障排查中的第一条日志。

2. 配置

{
  "feishu_event_mode": "websocket"
}

3. 启动服务

python3 app.py

程序将自动建立与飞书开放平台的长连接。源码层面(_startup_websocket()channel/feishu/feishu_channel.py)的流程是:

  1. 构建 lark.EventDispatcherHandler,注册三类回调:register_p2_im_message_receive_v1(收消息)、register_p2_im_message_recalled_v1(撤回)、register_p2_card_action_trigger(卡片按钮)——与 webhook 模式的三种事件一一对应;
  2. 在独立守护线程中创建 lark.ws.Client(app_id, app_secret, event_handler=...) 并调用 start() 维持长连接;
  3. 收消息回调中会先做一层降噪:群聊中未被 @ 且为文本类型的事件直接跳过,减少日志噪音。

4. 配置飞书应用

  1. 登录飞书开放平台;
  2. 进入应用详情 -> 事件订阅;
  3. 选择 使用长连接接收事件
  4. 添加事件:im.message.receive_v1(接收消息 v2.0)和 im.message.recalled_v1(消息撤回);
  5. 保存配置。

5. 注意事项

  • 无需公网 IP;
  • 需要能访问公网(建立 WebSocket 连接);
  • 每个应用最多 50 个连接;
  • 集群模式下消息随机分发到一个客户端——多实例部署时不要期望每条消息都被每个实例收到。

消息去重与离线消息过滤

两种模式共用同一套去重与过滤机制,这是消息“不重复、不补跑”的关键:

  1. 幂等去重:Channel 用一个 ExpiredDict(60 * 60 * 7.1) 存储已处理的消息 ID,过期时间正是 README 所说的 7.1 小时channel/feishu/feishu_channel.py)。每条消息进入 _handle_message_event() 后先查表,命中即记录 repeat msg filtered 并丢弃。ExpiredDict 是通用组件(common/expired_dict.py),条目在读取时惰性判过期,无需后台清理线程。测试 tests/test_feishu_message_recall.py 中同样以 receivedMsgs = ExpiredDict(60) 的方式注入短期字典来验证撤回消息的去重行为。
  2. 离线积压过滤(源码比 README 多讲的一层):飞书在长连接重连后会补发离线期间的积压消息。为避免机器人“复活”后批量回复旧消息,_handle_message_event() 会用消息的 create_time本通道的启动时间戳 _startup_ts 比较:启动之前发送的消息直接丢弃,仅保留一个 600 秒(STALE_MSG_MAX_AGE_S)的兜底上限来丢弃异常陈旧的补发(见 channel/feishu/feishu_channel.pychannel/feishu/feishu_channel.py 的注释)。注释中解释了为何用“相对启动时间”而非“绝对消息年龄”:单纯按消息年龄截断会误杀“只是投递慢的新消息”,也会给时钟偏移留不出余量。
  3. 撤回事件的会话路由:收到 im.message.recalled_v1 时,用另一个 7.1 小时过期的 ExpiredDict_message_sessions)把 message_id 映射回当初处理该消息的会话,只取消这条消息自己派生的任务,不影响队列中排在后面的新消息。

平滑迁移:Webhook 与 WebSocket 互切

从 webhook 模式切换到 websocket 模式(或反向切换):

  1. 修改 config.json 中的 feishu_event_mode
  2. 如果切换到 websocket 模式,安装 lark-oapi 依赖;
  3. 重启服务;
  4. 在飞书开放平台修改事件订阅方式(“将事件发送至开发者服务器” ↔ “使用长连接接收事件”)。

重要:同一时间只能使用一种模式,否则会导致消息重复接收。由于去重字典只保留 7.1 小时,切换后 7.1 小时内的旧消息理论上仍可能被另一条通路重复投递,所以务必先在开放平台改订阅方式、再启动新服务。

故障排查

1. WebSocket 模式连接失败

[FeiShu] lark_oapi not installed

原因:未安装 SDK 依赖。解决pip install lark-oapi。源码中该提示对应 __init__ 里 websocket 模式的依赖预检(channel/feishu/feishu_channel.py)。

2. SSL 证书验证失败

[Lark][ERROR] connect failed, err:[SSL:CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain

原因:网络环境中存在自签名证书或 SSL 中间人代理(如企业代理、VPN 等)。

解决:无需手动配置。从源码看(channel/feishu/feishu_channel.py),长连接启动有一个最多两次的重试循环:第一次失败时若错误信息包含 CERTIFICATE_VERIFY_FAILEDcertificate verify failed,第二次尝试会把 ssl.create_default_context 替换为禁用证书校验的版本后重试,成功后即正常长连。此时日志会显示:

[FeiShu] SSL certificate verification disabled due to certificate error. This may happen when using corporate proxy or self-signed certificates.

这是预期行为,程序会自动处理并继续运行。注意该降级仅针对 SSL 校验这一项,且只在检测到证书类错误时触发。

3. Webhook 模式端口被占用

Address already in use

原因feishu_port(默认 9891)已被其他进程监听。解决:修改 feishu_port 配置,或关闭占用端口的进程。

4. 收不到消息

按以下顺序逐项检查:

  1. 检查飞书应用的事件订阅配置(订阅方式与当前 feishu_event_mode 是否一致);
  2. 确认已添加 im.message.receive_v1im.message.recalled_v1 事件;
  3. 检查应用权限:需要 im:message 权限;
  4. 查看日志中的错误信息——webhook 模式下 Controller 对 Token 不符、事件类型不匹配都有明确的 {"success": false} 返回与 error 日志,可直接定位。

开发与部署建议

环境 推荐做法
本地开发 使用 websocket 模式,无需公网环境,快速迭代
测试环境 可使用 webhook 模式 + 内网穿透工具(如 ngrok)
生产环境 使用 webhook 模式,配置正式域名和 HTTPS

延伸阅读(仓库内相关实现)

总体而言,CowAgent 飞书 Channel 的设计思路是“传输层双模式可插拔、处理层完全统一”:无论你用 webhook 还是 websocket 接入,事件都会汇入同一套带幂等去重、离线积压过滤和会话级撤回路由的处理管线,这使得两种模式之间可以做到真正平滑的切换与迁移。

登录后查看全文
热门项目推荐
相关项目推荐