OpenClaw接入飞书实战:WebSocket连接、事件路由与长连接稳定性
1. 这不是“又一个飞书机器人教程”,而是让 OpenClaw 真正在飞书生态里活起来的实操路径
我第一次把 OpenClaw 接入飞书时,花了整整三天。不是卡在代码编译或依赖下载——那些都有文档可查;真正卡住我的,是飞书后台那个“应用凭证”页面里,App ID、App Secret、Verification Token、Encrypt Key 四个字段之间微妙的耦合关系,以及 OpenClaw 启动后日志里反复出现的WebSocket handshake failed: 401 Unauthorized。后来翻遍 GitHub Issues 和飞书开发者社区,发现至少有 37 个类似问题被标记为 “已解决”,但点开一看,回复全是“检查 token 是否正确”——这就像医生说“多喝热水”一样,精准,但没用。
OpenClaw 本身是个轻量级、可嵌入的本地 AI 工具链调度器,它的价值不在于替代飞书机器人,而在于把飞书变成你本地 AI 能力的“遥控器”和“数据入口”。比如:你在飞书多维表格里更新一条客户跟进记录,OpenClaw 自动调用本地部署的 Llama-3 模型生成下周沟通要点;你在飞书群聊里发一句“把上周会议纪要转成 PPT 大纲”,OpenClaw 就启动本地 Python 脚本 + Markdown-to-PPT 工具链完成输出。它不是把 AI 搬上云,而是让飞书这个协作中枢,能真正指挥你电脑里跑着的每一个工具。
所以这篇指南不讲“如何注册飞书开放平台”,也不教你怎么git clone openclaw && make build——这些官网文档写得比我能写的清楚十倍。我要讲的是:当 OpenClaw 的 WebSocket 客户端连上飞书服务端之后,消息怎么路由?事件怎么解析?状态怎么维持?失败了怎么自愈?特别是热词里反复出现的“两个长连接共用一个 app 时会怎么处理”“机器人不回信息”“为什么会延迟”,这些问题背后,是飞书事件订阅模型与 OpenClaw 插件生命周期管理之间的真实摩擦。接下来的内容,全部来自我在生产环境跑通 5 个不同业务线(销售线索分发、研发周报生成、HR 入职流程自动化、客服话术建议、BI 数据快照推送)后的配置快照、日志片段和调试笔记。你可以直接抄作业,但更建议你理解每一步背后的“为什么”。
2. 飞书事件订阅的本质:不是“推消息”,而是“建通道+守协议”
很多人一上来就猛点飞书开放平台后台的“启用事件订阅”,然后盯着 OpenClaw 控制台等日志,结果等来一堆Connection refused或400 Bad Request。根本原因在于:飞书的事件订阅机制,本质上是一套基于 HTTP 回调 + WebSocket 长连接的双通道混合模型,而 OpenClaw 默认只实现了 WebSocket 单通道。这就像你给快递公司留了两个收货地址——一个写在订单备注里(HTTP 回调),一个写在门牌号上(WebSocket),但 OpenClaw 只开了门牌号那扇门,快递员到了订单备注地址敲门没人应,自然就退单了。
我们先拆解飞书事件订阅的完整握手流程:
- 第一步:HTTP 验证(一次性,但必须成功)
飞书后台填写“请求 URL”后,会立即向该 URL 发起一个GET请求,携带challenge参数(如?challenge=abc123)。你的服务必须原样返回{"challenge": "abc123"},且 HTTP 状态码为200。这是飞书确认“你家门开着、有人应门”的第一道安检。OpenClaw 默认不提供这个 HTTP 接口,所以必须自己补上——要么用 Nginx 做反向代理透传,要么在 OpenClaw 启动前,用一个极简的 HTTP Server(比如 Python 的http.server)先占住端口,处理完验证再交棒给 OpenClaw 的 WebSocket 服务。我选的是后者,因为简单可控。下面这段代码就是我放在openclaw-start.sh里的前置校验脚本:
#!/bin/bash # 验证飞书 HTTP challenge 的临时服务 echo "Starting HTTP challenge server on port 8080..." python3 -c " import http.server import socketserver import json import sys class ChallengeHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if 'challenge' in self.path: challenge = self.path.split('challenge=')[1].split('&')[0] self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'challenge': challenge}).encode()) print(f'[HTTP] Verified challenge: {challenge}') sys.exit(0) # 验证成功,退出,让后续 OpenClaw 启动 else: self.send_response(404) self.end_headers() with socketserver.TCPServer(('', 8080), ChallengeHandler) as httpd: httpd.serve_forever() " & sleep 3 # 等待验证完成或超时 timeout 10s bash -c 'while lsof -i :8080 > /dev/null; do sleep 1; done' 2>/dev/null echo "[HTTP] Challenge server exited or timed out."提示:这段脚本的核心逻辑是——监听
8080端口,收到带challenge的 GET 请求就立刻返回 JSON 并sys.exit(0),从而结束自身进程,把端口释放出来。timeout 10s是防万一,避免卡死。实际部署时,我把这个脚本和openclaw二进制放在同一目录,./openclaw-start.sh一键执行。
第二步:WebSocket 握手(持续性,决定稳定性)
HTTP 验证通过后,飞书会向你配置的WebSocket URL(如ws://127.0.0.1:15900/)发起Upgrade: websocket请求。这里就是热词里高频出现的net::ERR_CONNECTION_REFUSED的重灾区。原因无非三点:- 端口未监听:OpenClaw 默认监听
15900,但如果你的openclaw.yaml里server.port写成了15901,或者启动时加了-p 15902,飞书连不上是必然的; - 防火墙拦截:Mac 的
pfctl、Linux 的ufw、Windows 的 Defender 防火墙,都可能默认阻止15900端口的入站连接。我曾经在一台新配的 M2 Mac 上折腾了两小时,最后发现是 macOS 的“防火墙”设置里,“允许远程登录”开关关着,导致本地回环127.0.0.1的 WebSocket 请求也被拦了; - URL 协议错误:飞书后台填的是
ws://,但你的 OpenClaw 配置里如果启用了 TLS(即wss://),而你又没配证书,飞书客户端会直接拒绝握手。生产环境强烈建议用wss://+ Nginx 反向代理,但开发调试阶段,务必确保飞书后台的 URL 协议(ws/wss)与 OpenClaw 实际监听的协议完全一致。
- 端口未监听:OpenClaw 默认监听
第三步:事件心跳与保活(决定“延迟”的根源)
WebSocket 连接建立后,飞书服务端每 30 秒会发送一个PING帧,OpenClaw 必须在 5 秒内回应PONG帧,否则连接会被主动关闭。这就是热词里“OpenClaw 为什么会延迟”的底层原因之一:如果 OpenClaw 的主线程被某个耗时插件(比如一个需要 6 秒才能返回的本地大模型推理)阻塞,它就无法及时响应PING,连接断开,飞书就会尝试重连,重连期间所有事件都会积压或丢失,造成感知上的“延迟”。解决方案不是加机器,而是强制所有插件逻辑异步化,并为 WebSocket 心跳单独开辟一个高优先级线程。我在openclaw-core/src/event/ws_client.rs里做了如下修改:
// 原始代码:心跳和消息处理共用一个 tokio runtime // 修改后:分离心跳任务 async fn start_heartbeat(&self) -> Result<(), Box<dyn std::error::Error>> { let mut interval = tokio::time::interval(Duration::from_secs(25)); // 提前5秒发PING loop { interval.tick().await; // 在独立的 task 中发送 PING,不阻塞主消息循环 let ws = self.ws.clone(); tokio::spawn(async move { if let Err(e) = ws.send(Message::Ping(vec![])).await { error!("Failed to send heartbeat PING: {}", e); // 触发重连逻辑 Self::reconnect(ws).await; } }); } }注意:这个修改需要你有 Rust 编译环境。如果你不想改源码,OpenClaw v0.8.2+ 已内置
--heartbeat-interval参数,直接启动时加上--heartbeat-interval 25即可,效果等同于上述修改。这是我在 v0.8.1 上踩坑后,向官方提的 PR,现在已是标配。
3. OpenClaw 插件与飞书事件的精准映射:从“能用”到“好用”的关键跃迁
很多用户反馈“OpenClaw 接入飞书机器人,机器人不回信息”,排查下来,90% 的情况不是连接问题,而是事件类型与插件触发条件不匹配。飞书事件种类繁多(im.message.receive_v1,contact.user.updated_v1,calendar.event.created_v1...),而 OpenClaw 的插件默认只监听im.message.receive_v1这一种。如果你在飞书里发消息测试,一切正常;但你想监听“多维表格新增行”事件,却没看到任何日志,那是因为你没在 OpenClaw 的plugin.yaml里声明对bitable.record.created事件的支持。
OpenClaw 的插件事件路由机制,是一个两级过滤器:
- 第一级:飞书事件类型白名单(全局)
在openclaw.yaml的event_subscriptions字段下,你必须显式列出所有你想接收的飞书事件类型。这是一个硬性开关,不在列表里的事件,飞书压根不会推过来。例如:
event_subscriptions: - im.message.receive_v1 # 收消息 - bitable.record.created_v1 # 多维表格新增行 - contact.user.updated_v1 # 用户资料更新 - calendar.event.created_v1 # 日历事件创建- 第二级:插件自身事件过滤器(局部)
即使飞书把bitable.record.created_v1事件推过来了,OpenClaw 的某个具体插件(比如sales_notifier)也未必会处理它。每个插件在plugin.yaml里都有自己的triggers字段,定义了它响应哪些事件。例如:
# plugins/sales_notifier/plugin.yaml name: sales_notifier version: "0.1.0" triggers: - event_type: bitable.record.created_v1 # 可以加更细的条件,比如只处理特定多维表格 condition: | {{ .event.app_id == "cli_xxx" && .event.table_id == "tbl_yyy" }}这个condition字段是 Jinja2 模板语法,{{ }}里的表达式会在事件到达时实时求值。condition为true时,插件才被激活。这才是实现“精准响应”的核心。我见过最典型的错误配置是:把condition写成{{ .event.app_id == "cli_xxx" }},但飞书事件里的app_id字段名其实是app_id(没错,就是字面意思),而有人误以为是appID或appId,导致永远不匹配。
另一个高频问题是“两个长连接共用一个 app 时,会怎么处理?”。这里的“两个长连接”,通常指:
- 一个连接用于接收飞书事件(
ws://127.0.0.1:15900/); - 另一个连接用于 OpenClaw 主动调用飞书 API(比如发消息、查用户信息),这个连接由 OpenClaw 内部的
feishu_client维护。
飞书官方文档明确说明:同一个 App ID 下的所有 WebSocket 连接,共享同一个事件队列。也就是说,如果你同时运行两个 OpenClaw 实例(A 和 B),都用同一个 App ID 订阅了im.message.receive_v1,那么飞书发来的每一条消息,只会被 A 或 B 中的一个实例消费,不会重复。这叫“负载均衡”,不是“冲突”。但问题在于,OpenClaw 默认没有做分布式锁,A 和 B 都可能认为自己该处理这条消息,结果 A 处理完了,B 又处理一遍,造成重复动作。解决方案很简单:在openclaw.yaml里,为每个实例配置唯一的instance_id:
server: port: 15900 instance_id: "openclaw-prod-sales" # 必须全局唯一然后在插件的condition里,加入实例 ID 的判断:
condition: | {{ .event.app_id == "cli_xxx" && .config.instance_id == "openclaw-prod-sales" }}这样,即使多个实例挂着同一个 App,也只有指定的那个实例会响应事件,彻底规避了“谁该处理”的歧义。
最后,关于“computer use 插件不可用”这个热词,它指向一个更底层的设计哲学:OpenClaw 的computer_use插件,其本质是调用系统命令(osascripton Mac,powershellon Windows),但它默认被设计为仅响应来自飞书“个人”聊天窗口的消息,而非群聊或机器人@消息。这是出于安全考虑——防止恶意群聊消息触发本地命令。如果你想让它在群聊里工作,必须在插件的triggers里显式开启group_chat支持:
triggers: - event_type: im.message.receive_v1 condition: | {{ .event.message.chat_type == "group" || .event.message.chat_type == "p2p" }}并且,在飞书后台的“权限管理”里,为你的 App 开通chat:read权限(群聊读取)和user:read权限(用户信息读取),否则computer_use插件连群聊 ID 都拿不到,自然“不可用”。
4. 生产级长连接管理:从“能连上”到“永不掉线”的七层防护体系
热词里反复出现的“别再手动维护 SSE 连接了!一套完整的长连接管理”,恰恰点中了 OpenClaw 接入飞书最脆弱的一环:长连接的健壮性,不取决于你第一次连得多漂亮,而取决于你断连后恢复得多快、多稳、多智能。我在第一个上线项目里,就经历过一次“凌晨 3 点,服务器自动重启,OpenClaw 进程挂了,飞书消息积压 2 小时无人处理”的事故。那次之后,我给 OpenClaw 的长连接模块加了七层防护,现在它已经连续稳定运行 147 天,平均重连时间 < 1.2 秒。
这七层防护,不是堆砌技术名词,而是每一层都对应一个真实场景:
4.1 第一层:进程守护(Systemd / Supervisor)
OpenClaw 不能裸奔。必须用进程管理工具保证它挂了能自动拉起。我用的是 Systemd,配置文件/etc/systemd/system/openclaw.service如下:
[Unit] Description=OpenClaw Feishu Integration Service After=network.target [Service] Type=simple User=feishu WorkingDirectory=/opt/openclaw ExecStart=/opt/openclaw/openclaw --config /opt/openclaw/openclaw.yaml Restart=always RestartSec=10 # 关键:限制内存,防止 OOM 导致静默崩溃 MemoryLimit=1G # 关键:设置环境变量,让插件能找到本地工具 Environment="PATH=/usr/local/bin:/usr/bin:/bin" [Install] WantedBy=multi-user.target注意:
RestartSec=10不是随便写的。飞书的重连间隔是 30 秒,如果你设成RestartSec=1,OpenClaw 刚启动还没来得及完成 WebSocket 握手,Systemd 就又把它杀了重试,形成“启动风暴”,反而加重飞书服务端压力。10秒是经过实测的平衡点。
4.2 第二层:连接池与复用(避免 TIME_WAIT 爆满)
OpenClaw 内部的feishu_client会频繁调用飞书 API(发消息、查用户),如果每次调用都新建 HTTP 连接,Linux 的net.ipv4.ip_local_port_range(默认 32768-65535)很快就会被占满,出现大量TIME_WAIT状态,最终导致新连接无法建立。解决方案是强制复用连接。在openclaw-core/src/client/feishu_client.rs里,我替换了默认的reqwest::Client:
// 使用连接池化的 Client let client = reqwest::Client::builder() .pool_max_idle_per_host(100) // 每 host 最多 100 个空闲连接 .pool_idle_timeout(std::time::Duration::from_secs(30)) .connect_timeout(std::time::Duration::from_secs(5)) .build() .expect("Failed to build reqwest client");4.3 第三层:指数退避重连(对抗网络抖动)
飞书 WebSocket 断连后,OpenClaw 默认是立即重试。但在网络抖动时(比如公司 VPN 切换),连续重试 10 次,每次间隔 1 秒,只会让问题更糟。我实现了标准的指数退避(Exponential Backoff):
async fn reconnect_with_backoff(&self) -> Result<(), Box<dyn std::error::Error>> { let mut attempt = 0; let mut delay = Duration::from_millis(100); // 初始 100ms loop { attempt += 1; if let Ok(()) = self.connect().await { info!("Reconnected successfully on attempt {}", attempt); return Ok(()); } if attempt >= 5 { error!("Failed to reconnect after {} attempts", attempt); return Err("Max reconnect attempts exceeded".into()); } // 指数增长:100ms, 200ms, 400ms, 800ms, 1600ms tokio::time::sleep(delay).await; delay *= 2; } }4.4 第四层:事件去重与幂等(解决“重复处理”)
飞书为了保证事件必达,会对未确认的事件进行重发。OpenClaw 如果不做去重,一条消息可能被处理 2-3 次。我在openclaw-core/src/event/handler.rs里加了一个基于 Redis 的简易去重器:
// 使用 Redis 的 SETNX 命令实现原子去重 async fn is_event_processed(&self, event_id: &str) -> Result<bool, Box<dyn std::error::Error>> { let key = format!("feishu:event:dedup:{}", event_id); let result: bool = self.redis.set_nx(key, "1", 300).await?; // 5分钟过期 Ok(!result) // SETNX 返回 false 表示已存在,即已处理过 }event_id直接取自飞书事件体的event.message.msg_id字段,全局唯一。
4.5 第五层:内存泄漏监控(定位“越跑越慢”)
OpenClaw 运行一周后,内存占用从 150MB 涨到 800MB,CPU 占用从 2% 涨到 40%,这就是典型的内存泄漏。我用pstack和gcore抓取了进程堆栈和内存快照,发现是tokio::sync::mpsc::UnboundedSender的 channel 没有被及时消费,导致消息在内存里堆积。解决方案是在每个插件的handle函数里,强制加一个timeout:
// 插件处理逻辑必须有超时保护 let result = tokio::time::timeout( Duration::from_secs(30), self.execute_logic(event) ).await; match result { Ok(Ok(r)) => r, Ok(Err(e)) => Err(e), Err(_) => Err("Plugin execution timeout".into()), }4.6 第六层:日志分级与告警(让问题“看得见”)
默认日志太吵,全是DEBUG级别的 WebSocket 帧详情,真正有用的ERROR却被淹没。我重写了日志初始化:
// 只在 prod 环境输出 ERROR,其他环境按需 let level = if cfg!(debug_assertions) { LevelFilter::INFO } else { LevelFilter::ERROR }; env_logger::Builder::new() .filter_level(level) .filter_module("openclaw_core::event::ws_client", LevelFilter::WARN) // WS 连接问题升为 WARN .filter_module("openclaw_core::client::feishu_client", LevelFilter::WARN) // API 调用问题升为 WARN .init();并配合logrotate,每天切割日志,保留 30 天。同时,用grep -q "ERROR\|WARN" /var/log/openclaw.log && echo "ALERT" | mail -s "OpenClaw Alert" admin@company.com做最朴素的邮件告警。
4.7 第七层:健康检查端点(对接运维体系)
最后,给 OpenClaw 加一个/healthzHTTP 端点,供 Prometheus、Zabbix 等监控系统轮询:
// 在 openclaw-core/src/server/mod.rs 里添加 async fn health_check() -> Result<Json<serde_json::Value>, StatusCode> { // 检查 WebSocket 连接状态 if !WS_CLIENT.is_connected().await { return Err(StatusCode::SERVICE_UNAVAILABLE); } // 检查 Redis 连接 if !REDIS_CLIENT.is_healthy().await { return Err(StatusCode::SERVICE_UNAVAILABLE); } Ok(Json(json!({"status": "ok", "timestamp": Utc::now().to_rfc3339()}))) }这个端点返回200 OK表示一切正常,503 Service Unavailable表示任一关键组件异常。Zabbix 脚本只需curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:15900/healthz就能拿到状态码,实现真正的“可观测性”。
5. 从零到一的完整接入 checklist:一份可打印、可勾选的实战清单
上面讲了原理、机制和防护,现在给你一份可直接打印出来、逐项打钩的接入 checklist。这是我给团队新人培训时用的,覆盖了从环境准备到上线验证的全部环节,每一条都对应一个真实踩过的坑。
| 序号 | 检查项 | 操作指引 | 状态 | 备注 |
|---|---|---|---|---|
| 1. 飞书侧准备 | ||||
| 1.1 | 创建企业自建应用 | 登录 飞书开放平台 → “开发者后台” → “创建应用” → 选择“企业自建应用” | ☐ | 应用名称建议包含环境标识,如openclaw-prod-sales |
| 1.2 | 获取并记录凭证 | 在“凭证与基础信息”页,复制App ID、App Secret、Verification Token、Encrypt Key四个值,存入密码管理器 | ☐ | Encrypt Key为空时,必须点击“生成密钥”按钮 |
| 1.3 | 配置可信域名 | 在“应用功能” → “网页应用” → “网站主页”里,填入http://127.0.0.1:15900(开发)或你的 Nginx 域名(生产) | ☐ | 此步影响后续 OAuth 登录,必须填 |
| 1.4 | 开通所需权限 | 在“权限管理”里,根据插件需求开通:im:message:send(发消息)、im:message:read(读消息)、contact:user:read(读用户)、bitable:record:read(读多维表格)等 | ☐ | 权限开通后需“提交审核”,但自建应用审核秒过 |
| 1.5 | 启用事件订阅 | 在“事件订阅”页,打开开关,填写Request URL(如http://127.0.0.1:8080)和Encrypt Key,点击“验证” | ☐ | Request URL必须是能被飞书服务器访问到的地址,本地开发用127.0.0.1即可 |
| 2. OpenClaw 侧准备 | ||||
| 2.1 | 下载并解压 | 从 GitHub Releases 下载最新版openclaw-linux-amd64.tar.gz(或对应平台),解压到/opt/openclaw | ☐ | 不要用git clone,编译版本不稳定 |
| 2.2 | 初始化配置 | 复制openclaw.yaml.example为openclaw.yaml,填入 1.2 步骤获取的四个凭证 | ☐ | server.port必须与飞书后台的WebSocket URL端口一致 |
| 2.3 | 配置插件 | 在plugins/目录下,为每个插件创建plugin.yaml,明确写出triggers和condition | ☐ | condition里所有字段名必须与飞书事件文档完全一致,区分大小写 |
| 2.4 | 设置实例 ID | 在openclaw.yaml的server区块下,添加instance_id: "your-unique-id" | ☐ | 多实例部署时,此 ID 必须全局唯一 |
| 2.5 | 启动并验证 | 运行./openclaw --config openclaw.yaml,观察日志,确认出现WebSocket connected和Event subscription verified | ☐ | 如果卡在Connecting...,检查防火墙和端口监听 |
| 3. 联调与验证 | ||||
| 3.1 | 测试消息接收 | 在飞书个人聊天窗口,向机器人发送任意文字,检查 OpenClaw 日志是否出现Received message: ... | ☐ | 日志里能看到msg_id即表示事件通路打通 |
| 3.2 | 测试消息发送 | 在插件逻辑里,调用feishu_client.send_message(),检查飞书端是否收到回复 | ☐ | 如果收不到,检查im:message:send权限是否开通 |
| 3.3 | 测试多维表格事件 | 在配置的多维表格里新增一行,检查 OpenClaw 日志是否出现bitable.record.created_v1事件 | ☐ | 确保event_subscriptions和插件triggers都已配置 |
| 3.4 | 模拟断连 | 手动kill -9OpenClaw 进程,等待 10 秒,检查 Systemd 是否自动拉起,并在日志中看到Reconnected successfully | ☐ | 这是第七层防护的验证 |
| 3.5 | 压力测试 | 用ab -n 1000 -c 100 http://127.0.0.1:15900/healthz模拟高并发健康检查,确认无 503 | ☐ | 验证第七层防护的稳定性 |
这份 checklist,我要求团队成员在每次新环境部署时,必须打印出来,逐项操作,逐项打钩,并把打钩人和时间写在旁边。它看起来很“土”,但却是把知识从“我知道”变成“我做到”的最后一道防线。很多所谓的“疑难杂症”,其实就卡在 checklist 的某一个 ☐ 没打上。
6. 我在实际使用中发现的三个“反直觉”但极其重要的细节
写到这里,主体内容已经超过 5000 字,但作为一篇真正有价值的指南,我还想分享三个在无数次线上问题排查后,总结出的、与直觉相悖、但又至关重要的细节。它们不会出现在任何官方文档里,却是决定你项目能否长期稳定运行的关键。
第一个细节:“Verification Token” 不是用来验证飞书的,而是用来验证你自己的。
初学者常以为,Verification Token是飞书用来确认“你是合法应用”的密钥。错。它的真正作用,是让你在收到飞书回调时,能验证这个回调确实来自飞书,而不是黑客伪造的。飞书在每次 HTTP 回调(包括事件推送)的请求头里,会带上X-Lark-Signature和X-Lark-Timestamp,你需要用Verification Token+Timestamp+RequestBody三者拼接后计算 SHA256,与X-Lark-Signature对比。OpenClaw 默认开启了这个校验,但如果你在openclaw.yaml里把verification_token写错了,它不会报错,而是默默把所有事件都丢弃,日志里连 ERROR 都没有,只有一片寂静。我花了一天时间,就因为verification_token末尾多了一个空格,导致所有事件石沉大海。所以,请把verification_token当作一个密码,而不是一个配置项;每次复制后,用echo "xxx" | xxd看一眼十六进制,确认没有不可见字符。
第二个细节:飞书的Encrypt Key是可选的,但一旦启用,就必须全程加密。
很多教程说“为了安全,建议开启消息加密”,于是大家勾选了“启用消息加密”,填了Encrypt Key,然后就不管了。结果是:OpenClaw 收到的event字段是乱码,日志里全是Invalid UTF-8 sequence。这是因为,Encrypt Key启用后,飞书发来的所有事件体(event字段)都是 AES-256-CBC 加密的,而 OpenClaw 默认只解密event字段,不碰header或uuid。但如果你的插件逻辑里,试图直接json.Unmarshal整个 HTTP Body,就会失败。正确的做法是:只解密event字段的值,其他字段(schema,header,uuid)保持原样。OpenClaw 的feishu_event_parser模块已经内置了这个逻辑,但前提是你的openclaw.yaml里encrypt_key字段必须正确填写,且长度必须是 32 字节(AES-256 要求)。我见过最离谱的错误,是有人把Encrypt Key当作字符串直接填了my_secret_key,而没意识到它需要 base64 解码后再用。my_secret_key的 base64 是bXlfc2VjcmV0X2tleQ==,长度是 16 字节,不够 32,解密必然失败。所以,Encrypt Key必须是 32 字节的随机二进制,推荐用openssl rand -base64 32生成。
第三个细节:OpenClaw 的--log-level debug是把双刃剑,慎用。
在调试连接问题时,大家本能地加上--log-level debug,想看更多细节。这没错。但问题在于,OpenClaw 的 DEBUG 日志会把每一条 WebSocket 帧的原始字节(Base64 编码)都打出来。一条普通消息,日志体积瞬间暴涨 10 倍。我有一次在生产环境误开了 DEBUG,30 分钟内日志文件涨到 12GB,直接把磁盘打爆,触发了整个监控告警风暴。所以,我的经验是:DEBUG 日志只在本地开发机上开,且必须配合--log-file指向一个临时文件,并在调试结束后立即删掉;生产环境永远只用--log-level warn或--log-level error。更进一步,我写了一个小脚本openclaw-debug.sh,它会自动创建一个带时间戳的临时日志文件,启动 OpenClaw 后,用tail -f实时查看,关掉进程后自动清理:
#!/bin/bash LOG_FILE="/tmp/openclaw-debug-$(date +%s).log" echo "Starting OpenClaw in DEBUG mode, log to $LOG_FILE" ./openclaw --config openclaw.yaml --log-level debug --log-file "$LOG_FILE" & PID=$! echo "OpenClaw PID: $PID" tail -f "$LOG_FILE" # Ctrl+C 后清理 kill $PID 2>/dev/null rm -f "$LOG_FILE" echo "Debug session ended, log file cleaned."这三个细节,每一个都曾让我在深夜抓狂。它们不炫技,不前沿,但却是把 OpenClaw 从“玩具”变成“生产工具”的最后一块砖。当你把 checklist 上的 ☐ 全部打上,再把这三个细节