news 2026/10/21 18:26:53

Node.js桥接飞书与OpenClaw:构建群聊AI执行代理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Node.js桥接飞书与OpenClaw:构建群聊AI执行代理

1. 为什么要把OpenClaw接进飞书

工作群里这类消息其实挺多的:“帮我查一下测试环境这个订单状态”“把昨天的数据汇总发我”“这个报错日志帮忙看看是什么原因”。每条消息背后都意味着一次人工查询、一次上下文切换,通常要花上一两分钟。几十个人同时提问的时候,一个人基本就被钉在工位上做重复答疑了。这个项目解决的,就是把这部分重复消耗交给AI代理。

OpenClaw是一个开源AI代理框架,它和其他“聊天机器人”最大的区别在于:它不仅能生成回复,还能真正去执行任务——调用本地命令、读写文件、请求外部API、串联多步骤流程。你可以把它理解成,一个“有手有脚”的数字员工,而不只是一个“会说话”的客服。飞书在整个系统里承担入口和出口的角色:用户以对话方式发指令,飞书把消息以事件形式推送出来;AI执行完之后,Node.js再把结果投递回会话里。

Node.js在这中间就是那层“桥”:把飞书事件协议翻译成OpenClaw能理解的任务请求,再把OpenClaw的结果翻译成飞书能渲染的消息格式。如果你正在用飞书做团队协作,恰好又写过一点Node.js,同时不想把团队内部数据送到第三方平台里去,这个项目就很对胃口。我之前试过直接用飞书官方SDK写机器人,也试过套一些通用的聊天机器人框架,最后都因为两件事放弃了:一是飞书的回调格式和权限体系需要专门适配,二是通用框架很难把“群聊里的一句话”变成“一个可执行任务”。自己动手拆三层,反而最清醒。

2. 三条链路的职责划分与消息流转

先把这个项目的架构在脑子里过一遍,后面写代码就不会乱。飞书、OpenClaw、Node.js这三者,各自管的事情完全不同。

飞书侧负责的是“人与Agent之间的通道”。用户消息进来,飞书开放平台把它包装成标准事件结构,推送到你配置的回调地址;Agent需要回复时,也是通过飞书的消息API把内容发回对应会话。飞书不管你的AI怎么思考、怎么执行,它只负责传递和呈现。所以权限、事件订阅、消息格式,这些是飞书侧的核心配置项。

OpenClaw侧负责“思考与行动”。它接收的自然语言指令,经过LLM推理后被拆成可执行的步骤,可以调用工具、运行脚本、读取文件,最后产出一个结果文本。它对外提供一个本地的HTTP接口或CLI入口,让外部程序可以发起一次“对话式任务”。需要注意的是,OpenClaw本身不关心消息来自飞书还是命令行,它只在乎你给它什么指令、希望它返回什么格式的结果。

Node.js是中间的适配层。它要处理完飞书那边所有“烦人但必须做”的事情:签名校验、事件重试、消息去重、权限令牌获取、文本提取、把长篇结果截断或分片。然后把干净的指令传给OpenClaw,拿到结果后再组织成飞书消息格式发送出去。整个过程本质上就是一次“翻译 + 转发”,但因为飞书协议细节多、边界情况多,这层薄薄的适配层往往比想象中复杂。

我在设计时把调用链路拆成四步:

  1. 用户在飞书群聊或单聊中发消息,飞书推送im.message.receive_v1事件到Node.js。
  2. Node.js校验签名,确认事件真实有效,提取消息文本、发送者、会话ID。
  3. 将指令包装成带会话上下文的任务请求,发给OpenClaw的HTTP API。
  4. OpenClaw执行工具调用和推理后返回结果,Node.js调用飞书消息API把结果回复给用户。

为什么要选Node.js而不是Python?坦白说这类适配层用Python写也很顺手,但Node.js有几个现实优势:事件模型天然适合这种“收到webhook后异步处理”的场景;Express等框架轻量,部署起来资源占用小;如果团队前端是JavaScript技术栈,这个服务的维护门槛会低很多。你完全可以用Python重写一遍,核心逻辑不会有太大区别,真正的复杂度全在飞书协议侧,跟语言关系不大。

3. 飞书应用后台和本地开发环境准备

动手写代码之前,先得把飞书开放平台这边的配置搞清楚。这些东西不配好,后面代码写得再漂亮都跑不通。

3.1 创建企业自建应用并启用机器人

打开飞书开放平台,进入开发者后台,创建一个“企业自建应用”。创建完进入应用详情页,第一件事是找到“机器人”这项能力,把开关打开。这一步决定了应用能不能以机器人身份出现在群聊里、能不能被用户@。

接着去“凭证与基础信息”页面,记下App ID和App Secret。App ID相当于应用的身份标识,App Secret是调用API时换token的凭证,后面Node.js服务里要用。还有一个容易漏的细节:权限管理页面里必须给机器人加上消息相关权限,常见的有接收消息、发送消息、读取群信息等。如果权限没配好,最常见的表现是应用能上线但收不到任何消息事件。

部署方式有webhook回调地址和长连接两种。我建议先选webhook地址联调,原因很简单:可以在代码里加日志,看到飞书到底推送了什么payload,排查问题更直观。长连接适合内网环境或没有公网IP的场景,后面再切换也来得及。在“事件订阅”页面,选择“使用webhook接收事件”,填上你本地或服务器的回调URL,比如https://your-domain.com/webhook/event。页面会要求验证URL有效性,这就是飞书发来的挑战请求,等会儿代码里会处理。

3.2 订阅消息事件

事件订阅页面里要添加事件。常用的事件类型是im.message.receive_v1,这个事件对应“接收消息”,无论单聊还是群聊都会触发。如果你的机器人需要在群里被@后响应,还要在权限管理里确认有接收群聊消息的权限。

订阅事件时可以拿到Verification Token和Encrypt Key。Verification Token用于识别事件来源,Encrypt Key是给事件内容加密用的。刚开始联调时,建议不要在飞书后台开启“加密”,先把明文模式跑通,减少一个变量。签名校验建议保留,这个不复杂,而且能挡住伪造请求。

3.3 Node.js项目初始化与OpenClaw启动

本地建一个项目目录,执行npm init -y,然后安装依赖。我这里实际只用到了三个核心依赖:express提供HTTP服务,axios或Node 18以上的原生fetch来调用飞书API和OpenClaw接口,dotenv管理环境变量。如果你打算用长连接模式,再额外加飞书的官方Node SDK,webhook模式用不到。

项目结构上我习惯分成三个文件:app.js是入口和路由,feishu.js封装飞书API相关调用,openclaw.js封装对OpenClaw的请求。这样后面改动权限或换模型时不用翻一整片代码。

OpenClaw侧的准备取决于你拿到的具体版本。比较通用的做法是:从官方仓库clone源码,安装依赖,配置好LLM的API Key或本地模型地址,然后启动服务。启动成功后,验证一下本地HTTP接口是否正常。拿我用的版本举例,它默认监听在127.0.0.1:8888,本地发一条curl请求就能知道服务是否待命。这一步值得先单独验证,不要等飞书消息都推过来了才发现Agent服务没起来。

4. 打通链路:Node.js如何把飞书消息交给OpenClaw

环境准备好了,接下来是核心代码。整个服务可以用一个Express程序撑起来,核心模块就五块:HTTP入口、签名校验、挑战响应、事件处理、消息发送。

4.1 搭建基础HTTP服务和签名校验

先写入口文件,结构大致这样:

const express = require('express'); const crypto = require('crypto'); const dotenv = require('dotenv'); dotenv.config(); const app = express(); app.use(express.json()); const FEISHU_APP_ID = process.env.FEISHU_APP_ID; const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET; const FEISHU_VERIFY_TOKEN = process.env.FEISHU_VERIFY_TOKEN; const FEISHU_ENCRYPT_KEY = process.env.FEISHU_ENCRYPT_KEY || '';

签名校验是飞书webhook接入里最容易被忽视的一环。飞书每次推送事件到你的服务器时,请求头里会带上X-Lark-Request-Timestamp、X-Lark-Request-Nonce和X-Lark-Signature。我们需要用同样的算法自己算一遍签名,比对结果。实现如下:

function verifySignature(req, res, next) { const timestamp = req.headers['x-lark-request-timestamp']; const nonce = req.headers['x-lark-request-nonce']; const signature = req.headers['x-lark-signature']; if (!timestamp || !nonce || !signature) { return res.status(401).json({ code: -1, msg: 'missing signature headers' }); } const hmac = crypto.createHmac('sha256', `${timestamp}\n${nonce}`); hmac.update(`${FEISHU_ENCRYPT_KEY}${timestamp}\n${nonce}`); const expected = hmac.digest('base64'); if (signature !== expected) { return res.status(401).json({ code: -1, msg: 'invalid signature' }); } next(); }

注意,不同版本的飞书文档对字符串拼接方式有细微差异,有的用换行符有的不用,你对着官方文档核对一次就行。这个校验的本质是:如果第三方能伪造请求,那它也能伪造签名头,我们设一道防线拦住绝大多数无效流量。

4.2 处理URL验证挑战

飞书在后台保存你的回调URL时,会主动发一个验证请求,body长这样:

{ "challenge": "xxxxxx", "token": "xxxxxx", "type": "url_verification" }

我们必须在路由里判断这个情况,把挑战原样返回,否则飞书后台会一直显示验证失败。代码很简单:

app.post('/webhook/event', verifySignature, (req, res) => { const { type, challenge } = req.body; if (type === 'url_verification') { return res.status(200).json({ challenge }); } res.status(200).json({ code: 0 }); // 这里先不阻塞,后续异步处理消息事件 handleFeishuEvent(req.body).catch((err) => console.error('handle event error:', err)); });

先立即返回200是关键。飞书对回调响应时间有要求,如果你在处理AI任务之后再返回,很可能已经超时了,飞书会判定事件投递失败并重试,导致用户那边看到重复回复。正确的做法是:先确认收到事件,立刻返回,然后把耗时的处理放在异步任务里。

4.3 解析消息并调用OpenClaw

收到事件后,先判断事件类型。im.message.receive_v1的body里,消息内容在event.message中,发送者信息在event.sender中。飞书消息的content字段是JSON字符串,需要解析一次才能拿到具体文本。我处理时还做了两件小事:过滤掉机器人自己发的消息,避免死循环;群聊场景下剥离@机器人的那段内容。

async function handleFeishuEvent(body) { const header = body.header; if (header.event_type !== 'im.message.receive_v1') return; const event = body.event; const message = event.message; const messageId = message.message_id; const chatId = message.chat_id; const messageType = message.message_type; if (messageType !== 'text') return; const content = JSON.parse(message.content); const userQuery = content.text; const reply = await callOpenClaw(userQuery); const token = await getTenantAccessToken(); await sendFeishuMessage({ chatId, msgType: 'text', content: reply, token }); }

callOpenClaw这一层根据你实际安装的OpenClaw版本接口来适配。我这里提供的是通用结构:

async function callOpenClaw(text) { const resp = await fetch('http://127.0.0.1:8888/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, session_id: 'feishu_' + chatId, // 用会话ID保持上下文 stream: false }) }); if (!resp.ok) { return 'Agent服务暂时不可用,请稍后再试。'; } const data = await resp.json(); return data.reply || data.text || '没有拿到回复内容'; }

这里的session_id很重要。如果不传,OpenClaw每次都是“失忆”状态,用户上一句说“帮我查订单”,下一句问“结果呢”就没法接上。我把飞书的chatId映射成OpenClaw的session_id,这样同一个群聊或单聊的上下文是连续的。

4.4 获取token并发送消息

飞书OpenAPI要求每次请求都带tenant_access_token。获取方式是对内接口换token,代码很少但要注意缓存——token有效期约两小时,别每次都请求,否则高并发下容易把接口频率打满。

let cachedToken = ''; let tokenExpireAt = 0; async function getTenantAccessToken() { if (cachedToken && tokenExpireAt > Date.now()) { return cachedToken; } const resp = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET }) }); const data = await resp.json(); cachedToken = data.tenant_access_token; tokenExpireAt = Date.now() + (data.expire - 60) * 1000; return cachedToken; }

发送消息使用飞书的im/v1/messages接口,注意receive_id_type参数决定receive_id的类型。群聊场景传chat_id,单聊场景传open_id或user_id。实现如下:

async function sendFeishuMessage({ chatId, msgType, content, token }) { await fetch('https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ receive_id: chatId, msg_type: msgType, content: JSON.stringify({ text: content }) }) }); }

跑通这一条链路之后,你会看到飞书群里用户发消息,过几秒Agent自己回复了。那种“循环真的走动起来”的瞬间是有点上头的。

5. 调试和上线中踩过的坑

代码写出来只是第一步,真正耗时间的是联调和上线时那些不出现在文档里的问题。我挑几个印象最深的讲。

5.1 回调必须秒回,长任务要另想办法

飞书的webhook事件对响应时间有要求,我印象里是要求3秒内返回2xx,超时就会触发重新投递机制。这意味着你绝对不能同步地把OpenClaw的完整执行过程放在回调里。我一开始就踩了这个坑:把Agent任务放在回调函数里同步等待,结果飞书不断重发事件,用户收到了好几条重复回复。

正确的做法前面已经体现:先返回200,异步处理。但异步处理也有一个副作用——消息可能乱序。一个群里同时有人发两条消息,两个异步任务并发执行,慢的那条结果可能后发先至。如果你的Agent是幂等任务,这不是问题;如果是强秩序任务,可以考虑在项目里加一个简单的队列,串行处理同一个会话ID的任务。

5.2 事件重复和消息去重

飞书事件投递是“至少一次”的语义,意味着同一事件可能被推送多次。网络抖动、超时重试、长连接断线重连,都可能让你重复处理同一条消息。如果Agent执行的任务有副作用,比如写文件、发通知、调用外部系统,重复执行就是灾难。

我的做法是维护一个已处理消息ID的缓存。飞书的每条消息都有唯一的message_id,我用它做键,Redis或普通Map都行。处理前先检查是否已存在,存在就跳过,不存在则写入再处理。注意给缓存加过期时间,不然内存会随着时间涨上去。单机部署用Map就够了,多实例部署必须用Redis这类共享存储。

5.3 权限不全导致的“幽灵问题”

最常见的现场是:机器人明明上线了,消息事件却一只推不过来。排查到最后,多半是权限没配齐。飞书对消息权限拆分得很细:接收单聊消息、接收群聊消息、发送消息,是三个不同的权限;在群里被@后读取群信息,还需要获取群列表的权限。权限管理这块没有捷径可走,把自己用到的能力逐个核对一遍,尤其是“接收群聊中@机器人消息的事件”这一项,不开启的话,群里@机器人根本没反应。

还有一个坑:改了权限之后要重新发布应用版本。开发后台的权限修改不会自动生效,必须走一次版本发布流程。很多人以为后台按钮点完就完事了,结果线上一直收不到消息,其实是没重新发布。

5.4 长文本回复被截断和格式丢失

OpenClaw执行复杂任务时,返回结果可能是几百行日志,甚至包含markdown格式的代码块。飞书普通文本消息对长度有限制,内容太长会被截断或发送失败。我处理时加了一个长度判断:超过一定阈值就做截断,并提示“完整结果过长已省略,建议查日志”;如果是代码块或结构化文本,考虑改用飞书富文本或帖子post消息类型,展示效果更好。

另外注意转义问题。用户消息里的特殊字符、换行符,在拼接JSON时一定要处理好,不然解析阶段就挂了。飞书的消息content字段是JSON字符串,里面再嵌套JSON,极其容易出多层转义错误。我的建议是统一用JSON.stringify来构建content,不要手工拼接字符串。

5.5 本地调试时用“内网穿透”还是“长连接”

本地开发时没有公网地址接收webhook,最直接的做法是用内网穿透工具把本地端口暴露出去。但我后来发现,飞书的webhook调试模式有个更好的选择:直接把事件订阅方式切换成“长连接”。长连接模式下,SDK会主动建立一条到飞书服务器的WebSocket连接,消息直接推送到本地进程,不需要公网回传。调试时信息都留在本地,日志查看体验比穿透工具好一个档次。

不过长连接模式也有自己的坑:网络不稳定时会掉线,SDK会自动重连,重连期间的事件会有积压。建议在重连回调里加告警,别在黑盒状态下跑生产环境。

6. 把它真正用起来:几个能落地的场景

项目跑通之后,价值取决于你给Agent派了什么活。我实际用下来,下面几个场景最顺手。

第一个是运维辅助。群里有人贴一段报错日志,问“这是怎么回事”,OpenClaw可以基于日志内容调监控接口、查服务状态,然后给出判断。这比人肉翻日志效率高得多,而且多个人同时提问也不慌。第二个是数据查询与汇总。比如“把昨天测试环境的通过率统计一下”,Agent自动拉数据、算结果、回复摘要。这类定时重复任务让Agent做,能省下每天早上的固定功课。

第三个是文档与知识库问答。把内部文档地址放在Agent可读取的路径下,让它基于文档内容回答团队问题。相比直接把文档丢进大模型对话,它有执行能力,可以自己翻文件、搜索、比对,回答更扎实。

再往后可以扩展的方向也很多。比如把飞书交互卡片做起来,让用户在卡片上点按钮触发任务,而不是纯指令输入;比如把OpenClaw的会话历史存到外部存储,让它在服务重启后保留记忆;比如在Node.js层接一个简单的权限系统,只有特定群下的消息才会被转给Agent,避免机器人被拉进不相关群聊后乱执行。

就我自己来说,这个项目的最大收获其实不是代码,而是搞清楚了一件事:AI代理能不能融入日常工作流,关键在于这层“桥”修得好不好。飞书、OpenClaw、Node.js各自都不神秘,真正花时间的是把边界条件都磨平。现在我的机器人稳定挂在团队群里两个多月,最大的变化是那些“查一下”开头的消息有了去处,而我也终于不用在早上九点被钉在聊天窗口前了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 18:26:37

KUKA机器人暂停与再启动控制:KRL程序指针与检查点实现

简介:面向工业自动化与KUKA机器人应用人群,这份docx文档系统整理了在SPS子程序中实现机器人暂停与再启动的编程方法。资源聚焦WorkVisual环境下的逻辑控制设计,详细说明了如何通过输入信号触发暂停、再启动和程序关闭三类控制动作&#xff0c…

作者头像 李华
网站建设 2026/10/21 18:22:46

基于Fabric的Linux服务器合规聚合器设计与落地

手里管着几十台Linux服务器,最头疼的不是服务部署,而是安全基线能不能统一。今天聊的这套东西,核心是用Fabric把安全加固和合规检查攒成一个聚合器来用,统一跑、统一收、统一出报告。这篇文章就来拆解一下,这个“统一合…

作者头像 李华
网站建设 2026/10/21 18:22:09

AI代理技能系统:从工具函数到可验证的行为契约

代理工程做到一定阶段,都会碰到同一个坎:单次模型调用在 demo 里什么都能干,可一旦对业务结果负责,模型本身的输出方差就成了最大的隐患。真正的可靠性往往不来自模型,而来自它周围那层被称为技能(skills&a…

作者头像 李华
网站建设 2026/10/21 18:20:15

车道线检测透视变换全解析:从数学原理到工程实践

做车道线检测项目,第一次把检测结果叠加到原图上时,我一度以为算法出了问题:明明车辆正行驶在直线车道上,检测出来的左右两条车道线却在远处越收越窄,最终交汇成一个点。这不是模型的问题,而是单目相机成像…

作者头像 李华
网站建设 2026/10/21 18:19:36

主数据治理实战:从多系统数据不一致到单一可信版本

任何一个在企业里做过数据治理的人,大概都经历过这样的场面:业务部门开会拍桌子,说财务系统的客户余额和 CRM 对不上;供应链说自己发的料号和电商平台挂在商品详情页的编码不是同一个东西;老板问"我们到底有多少家…

作者头像 李华
网站建设 2026/10/21 18:18:39

原理图符号自动生成:从引脚表到可用符号的工具实践

1. 从“画符号画到手抽筋”说起 做硬件设计这些年,最让我头疼的杂活之一就是画原理图符号。一个稍微复杂一点的器件,几十个引脚,每个引脚叫什么、什么类型、放在哪一侧,都要手工摆好,漏一个或者摆错一个,到…

作者头像 李华