上周我们团队在飞书里正式上线了一个 Clawdbot,现在群里直接 @ 它,就能让它帮忙查日志、写周报、跑代码片段,甚至接一条命令去调内部接口。整个部署过程从飞书开放平台创建应用,到 Ubuntu 服务器上把服务拉起来,再到把消息链路调通,踩了不少坑。这篇就把完整部署流程、关键代码和排错经验一次性写清楚,给准备把 Clawdbot 接进飞书(飞连)的读者一个可以直接照着做的参考。
先说清楚一件事:如果你只是想快速搞一个问答机器人,Coze 这类平台也能一键发布到飞书,没必要自己折腾。但 Clawdbot 是自托管的方案,代码、上下文、工具链全在你手里,适合把机器人做成团队基础设施的场景。它能读群消息、能回单聊、能跑自定义工具,甚至能接定时任务。这篇教程覆盖部署、配置、踩坑三个部分,单聊群聊都讲,适合有一定命令行基础但没做过飞书应用开发的人。
1. 为什么非要把 Clawdbot 塞进飞书
1.1 团队协作环境里的 AI 助手,入口很重要
我们团队日常所有协作都在飞书(飞连)里完成,之前也有同事用网页版 Claude、命令行工具,但问题是:这些工具不在你工作流里,每次切过去都要重新粘贴上下文,消息记录也没法和项目讨论保持在同一个地方。把 Clawdbot 部署到飞书之后,所有交互都发生在群聊或者单聊窗口里,消息记录随飞书走,手机端也能用,体验完全不一样。
另外一个隐藏价值是"上下文聚合"。在群聊里 @ 机器人,它能看到当前群的上下文,知道大家在讨论什么。比如研发群里有人说"刚才线上接口报错那个问题排查一下",Clawdbot 可以直接理解这是哪条线上问题,而不是像网页版那样什么都不记得。这种"长在 IM 里"的交互方式,比单独开一个聊天窗口自然得多。
1.2 自托管与平台型智能体的取舍
Coze 智能体接入飞书确实省事,不用维护服务器,点几下就发布。但实际用下来你会发现几个问题:平台侧的 prompt 编排和工具链是封闭的,想接入公司内部接口、数据库、私有知识库都很麻烦;数据全过第三方平台,敏感信息不敢往里放。
Clawdbot 这类自托管方案的优势正好补上这些短板:模型 API 由你自己配置,可以接 Anthropic 官方接口,也可以接任意 OpenAI 兼容的模型服务(比如 DeepSeek),数据流完全可控;工具调用支持自定义脚本,团队内部的服务可以通过它统一暴露成自然语言入口。代价就是部署和运维需要花点时间,但一次搭好,长期受益。
1.3 这篇教程适合谁来读
如果你是团队里负责基础设施的工程师、想给工作群加一个 AI 助手的个人开发者,或者正在评估飞书机器人方案的架构师,这篇都适用。下面从飞书开放平台配置开始,一步一步写。基础要求是:会 Linux 基本命令、能读懂 Node.js 代码、有服务器部署权限。不用提前了解飞书开放平台,我尽量把每个操作对应的"为什么"也讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前先理清这套架构的三个核心概念
2.1 Clawdbot 到底是什么:事件网关、会话管理、工具执行器
Clawdbot 这个名字里的 Clawd 是 Claude 的社区昵称,所以它本质上是把 Claude 系列模型能力包装成 IM 机器人的一个服务。从架构角度看,它做三件事:
第一,事件网关。它监听飞书推送过来的消息事件,解析出文本、发送者、群 ID 这些信息。第二,会话管理。它维护每个用户或每个群的多轮对话历史,调用模型时把历史一起拼接进去,这样机器人才能"记得"上下文。第三,工具执行器。遇到模型想调用工具时,它负责执行本地脚本、HTTP 请求,再把执行结果返回给模型,让模型继续生成回复。
模型后端是可插拔的。Clawdbot 官方默认走 Anthropic 的接口规范,但大多数实现也支持 OpenAI 兼容协议。如果你手里的模型服务是 DeepSeek、自托管模型或者其他兼容网关,只要在配置里把 MODEL_PROVIDER 切到 openai-compatible,填上对应的 API Key 和模型名就行。这也是自托管方案最灵活的地方。
2.2 飞书机器人如何收到消息:事件订阅是核心开关
飞书机器人本质上是一个"企业自建应用",它靠事件订阅机制接收消息。你在飞书开放平台后台创建一个应用,开启机器人能力,然后订阅 im.message.receive_v1 这个事件。之后,用户给机器人发消息、或者在群里 @ 机器人,飞书服务器就会把这条消息推送到你配置的回调地址,或者通过 WebSocket 长连接推送到你的服务。
这里有两个接收通道:Webhook 模式需要你的服务器有一个公网可访问的 HTTPS 地址,飞书服务器会把事件 POST 到这个地址;长连接模式则是你的服务主动和飞书服务器建立 WebSocket 连接,不需要公网回调地址。我强烈推荐长连接模式,它是较新的方案,不用处理回调地址暴露在外网的安全风险,也不用担心内网环境回调不到的问题,部署难度低很多。
2.3 一条消息从群聊到 Clawdbot 再回到群聊的完整链路
为了后面排查问题方便,先记清楚这条链路。假设你在群里发了一条"@Clawdbot 帮我看一下 Nginx 日志",它的生命周期是这样的:
- 飞书客户端把消息发到飞书服务器。
- 飞书服务器根据事件订阅配置,把
im.message.receive_v1事件推给 Clawdbot 服务(长连接或 Webhook)。 - Clawdbot 服务校验事件有效性,提取消息文本和会话信息。
- Clawdbot 把文本拼上对话历史,发给模型 API,模型返回回复。
- Clawdbot 调用飞书开放平台的"发送消息" API,把回复发到同一个聊天会话里。
关键点在第 2 步到第 5 步之间。飞书对于 Webhook 推送有超时要求,建议 3 秒内返回 HTTP 200,所以工程师的通用做法是:先立刻返回 200 确认收到,再异步去调模型、发消息。长连接模式没有明确的超时限制,但同样建议用异步处理,避免事件堆积阻塞。这条链路是后面所有逻辑的基础,记住它,后面看代码就不乱了。
3. 飞书开放平台侧配置:应用、权限、事件订阅
3.1 创建企业自建应用
打开飞书开放平台,进入开发者后台,点击"创建企业自建应用"。应用名称最好带个容易辨认的后缀,比如"Clawdbot 助手",图标随便传一个,审核主要看名称是否规范,不会卡太严。创建完成后,进入应用详情页。
这里有个刚接触的人容易懵的点:飞书开放平台有"企业自建应用"和"商店应用"两种。自建应用只给当前企业用,审核流程短;商店应用要上架审核,不需要。我们做内部工具,一律选企业自建应用。
3.2 开启机器人能力,配置权限
在应用详情页左侧菜单找到"添加应用能力",选择"机器人",启用后你的应用就拥有了机器人身份。接着在"权限管理"页面配置权限。这是最关键的一步,权限没配上,后面事件订阅验证成功也收不到消息。
需要配的权限清单如下:
| 权限标识 | 用途 |
|---|---|
im:message |
获取消息内容的基本权限,必开 |
im:message.p2p_msg |
读取单聊消息,用于处理用户私聊机器人 |
im:message.group_at_msg |
读取群聊中 @ 机器人的消息,群聊场景必开 |
im:chat |
获取群基础信息,用于知道消息来自哪个群 |
im:resource |
如果要接收图片、文件等附件,需要这个权限 |
contact:user.base:readonly |
可选,用于获取发送者姓名等用户信息 |
权限申请后,有的需要管理员审核。自建应用一般企业管理员在后台点一下就行。
3.3 配置事件订阅:推荐长连接模式
在"事件与回调"页面,选择事件订阅方式。如果你不想处理公网回调地址,就选"使用长连接接收事件"。选了长连接之后,你的服务端只需要用官方 SDK 启动一个 WebSocket 客户端,飞书服务器会把事件推过来。
订阅事件时,在事件列表里搜索并添加 接收消息 im.message.receive_v1。此外,如果你未来要做加群欢迎、群成员变动通知,还可以订阅 群配置变更、群成员变更 等事件,但第一版只需要消息事件就够了。
如果你所在团队的网络环境允许暴露一个 HTTPS 公网地址,也可以选 Webhook 回调。这时飞书会要求你填一个"请求地址 URL",并且会立刻发一个验证请求过来,你的服务端必须正确处理 challenge 校验才能保存成功。这个校验逻辑我在后面代码部分会专门写。
还有一个建议:在"事件与回调"页面开启"加密策略",生成一个 Encrypt Key。启用加密后,飞书推送的事件内容会加密,你的服务端需要解密才能拿到原始事件。多一层加密多一层安全,特别是机器人要处理内部信息时,强烈建议开。
3.4 拿到关键凭证:App ID 和 App Secret
在应用详情页的"凭证与基础信息"页面,你能看到 App ID 和 App Secret。这两个就是你的服务端和飞书平台通信的身份证,后面 .env 配置文件里要填。
特别注意:App Secret 是敏感信息,不要提交到 Git 仓库,也不要随手发到群里。建议直接保存在服务器上的环境变量文件里,权限设为 600。后面 Clawdbot 服务启动时会读取它。
3.5 发布应用版本
配置完以上内容后,在"版本管理与发布"里创建一个版本,填写版本号和更新说明,提交发布。自建应用发布后会出现在企业工作台里,但这不是机器人能收消息的前提——实际上只要权限审核通过、事件订阅配置好了,机器人就能工作。
不过有个细节:如果你在权限管理里新加了权限,必须重新发布版本或创建版本让权限生效。很多人配置完发现机器人还是收不到消息,就是卡在权限没有重新发布这一步。
4. 服务器端部署:Ubuntu 环境与核心服务启动
4.1 准备一台服务器
Clawdbot 对服务器要求不高,一台 2 核 4G 的 Ubuntu 22.04 服务器足矣。如果你只是本机调试,Ubuntu 桌面版也行,但生产环境建议用云服务器,网络稳定,日志也好统一采集。
有个常见疑问:Ubuntu 上要不要装飞书客户端?如果服务器是纯命令行环境,不需要,也装不了。如果是 Ubuntu 桌面环境,想用飞书客户端看消息,可以直接去飞书官网下 deb 包安装,sudo dpkg -i feishu.deb 即可。但这跟 Clawdbot 部署没有关系,客户端只是你的调试窗口。
4.2 安装 Node.js 环境
Clawdbot 通常基于 Node.js 开发,建议使用 Node.js 20 LTS。用官方源安装比较稳定:
bash复制sudo apt update
sudo apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
验证安装:
bash复制node --version
npm --version
建议再装一个 pm2,用来做进程守护和日志管理:
bash复制sudo npm install -g pm2
4.3 拉取 Clawdbot 源码与安装依赖
把你的 Clawdbot 仓库 clone 到 /opt/clawdbot(路径按你习惯来):
bash复制sudo mkdir -p /opt/clawdbot
sudo chown $USER:$USER /opt/clawdbot
git clone <你的仓库地址> /opt/clawdbot
cd /opt/clawdbot
npm install
如果项目里有的原生依赖编译不了,可能要装 build-essential:
bash复制sudo apt install -y build-essential python3
4.4 配置 .env 环境变量
在项目根目录创建 .env 文件,把之前拿到的信息填进去。核心变量如下:
| 变量 | 必填 | 说明 |
|---|---|---|
FEISHU_APP_ID |
是 | 飞书应用的 App ID |
FEISHU_APP_SECRET |
是 | 飞书应用的 App Secret |
FEISHU_ENCRYPT_KEY |
否 | 事件加密 Key,没开启加密可不填 |
FEISHU_VERIFICATION_TOKEN |
否 | 旧版校验 Token,一般不需要 |
MODEL_PROVIDER |
是 | anthropic 或 openai-compatible |
ANTHROPIC_API_KEY |
视情况 | 使用 Anthropic 官方接口时填 |
OPENAI_API_KEY |
视情况 | 使用 OpenAI 兼容接口时填 |
MODEL_NAME |
是 | 模型名,如 claude-sonnet-4-20250514、deepseek-chat |
BOT_NAME |
否 | 机器人在飞书里的名称,用于自我识别 |
ALLOWED_USERS |
否 | 允许使用机器人的用户 ID 白名单,逗号分隔,不填则全员可用 |
示例:
bash复制FEISHU_APP_ID=cli_xxxxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxxxx
MODEL_PROVIDER=openai-compatible
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
MODEL_NAME=deepseek-chat
BOT_NAME=Clawdbot
4.5 启动服务并配置开机自启
直接用 pm2 启动:
bash复制cd /opt/clawdbot
pm2 start src/index.js --name clawdbot-feishu
pm2 save
pm2 startup
pm2 startup 会生成一条开机自启命令,按提示执行即可。
如果你更习惯 systemd,也可以写一个 service 文件:
ini复制[Unit]
Description=Clawdbot Feishu Gateway
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/clawdbot
EnvironmentFile=/opt/clawdbot/.env
ExecStart=/usr/bin/node src/index.js
Restart=always
RestartSec=5
User=clawdbot
[Install]
WantedBy=multi-user.target
启动:
bash复制sudo systemctl daemon-reload
sudo systemctl enable clawdbot-feishu
sudo systemctl start clawdbot-feishu
4.6 验证服务是否连上飞书
启动后查看日志:
bash复制pm2 logs clawdbot-feishu
如果一切正常,你会看到类似 WebSocket connected 或 长连接已建立 的日志。看到这个,说明你的服务已经成功连上飞书服务器,准备接收事件了。如果报错,八成是 App ID / App Secret 填错,或者网络不通,按日志里的报错信息排查。
5. 消息链路调通:回调验证、事件分发、主动回复
5.1 长连接模式的事件处理入口
这是最推荐的接收方式。使用飞书官方 Node.js SDK,启动一个 WebSocket 客户端监听事件。核心逻辑如下:
javascript复制import lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
appType: lark.AppType.SelfBuild,
domain: lark.Domain.Feishu,
});
const wsClient = new lark.WSClient({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
domain: lark.Domain.Feishu,
});
wsClient.start({
onEvent: async (data) => {
const { header, event } = data;
if (header.event_type === 'im.message.receive_v1') {
handleMessage(event).catch((err) => console.error('handle message error:', err));
}
},
});
handleMessage 在下面会写。注意要在 onEvent 里直接用异步函数包一层,不要阻塞事件循环。
5.2 Webhook 模式的 challenge 与签名校验
如果你用的是 Webhook 模式,飞书保存回调地址时会发一个验证请求,请求体长这样:
json复制{
"challenge": "ajls384kdjx98XX",
"token": "xxxx",
"type": "url_verification"
}
你必须返回:
json复制{
"challenge": "ajls384kdjx98XX"
}
用 Express 写就是:
javascript复制app.post('/webhook/feishu', express.json(), async (req, res) => {
const body = req.body;
// 飞书的 URL 验证
if (body.type === 'url_verification') {
return res.json({ challenge: body.challenge });
}
// 先立刻返回,避免飞书超时重试
res.json({ code: 0 });
// 异步处理事件
handleEvent(body).catch((e) => console.error(e));
});
如果你开了加密策略,收到的 body 里只有一个 encrypt 字段,需要用 Encrypt Key 解密。SDK 里通常有现成方法,手动实现的话是 AES-256-CBC 解密,密钥取 Encrypt Key 的 MD5 前 16 位作为 key,前 16 位作为 iv。建议直接用 SDK 的解密方法,别手动造轮子。
签名校验同样建议做。飞书每次推送都会在请求头带 X-Lark-Signature,用 Encrypt Key 和时间戳、随机数做 HMAC-SHA256 得到签名,不匹配就丢弃。这样能防止有人伪造事件推给你的服务。
5.3 消息解析:去 @ 标记、过滤自己、拉上下文
这是实际业务里最容易被忽视的部分。群聊里用户 @ 机器人时,飞书推过来的文本是这样的:
code复制@_user_1 帮我看看今天的发布会状态
你要把 @_user_1 这个占位符去掉,只留下纯文本。另外还要过滤一类消息:机器人自己发送的消息!如果你不过滤,机器人回复一条,飞书又推一条"机器人收到消息"事件,可能造成死循环或者自我刷屏。
处理流程大致如下:
javascript复制function normalizeEvent(event) {
const message = event.message;
const messageType = message.message_type;
const chatType = message.chat_type; // p2p 或 group
const chatId = message.chat_id;
const senderId = event.sender.sender_id.user_id;
let text = '';
if (messageType === 'text') {
const content = JSON.parse(message.content);
text = content.text || '';
}
// 去掉群聊 @ 占位符
text = text.replace(/@_user_\d+/g, '').trim();
return { chatType, chatId, senderId, text, messageId: message.message_id };
}
拿到 text 之后,拼上该会话的历史记录,交给 Clawdbot 核心去调模型:
javascript复制const sessions = new Map(); // chatId -> [{role, content}]
async function handleMessage(event) {
const { chatType, chatId, senderId, text, messageId } = normalizeEvent(event);
// 白名单校验
const allowed = process.env.ALLOWED_USERS;
if (allowed && !allowed.split(',').includes(senderId)) {
return;
}
// 取历史上下文
let history = sessions.get(chatId) || [];
history.push({ role: 'user', content: text });
// 调 Clawdbot / 模型接口
const reply = await askClawdbot(history, { chatId, senderId });
history.push({ role: 'assistant', content: reply });
// 裁剪历史太长,只保留最近 20 条
if (history.length > 20) history = history.slice(-20);
sessions.set(chatId, history);
await sendTextMessage(chatId, reply);
}
5.4 发送回复:文本和卡片两种方式
发送消息用飞书开放平台的 im/v1/messages 接口。最简单的方式是用 SDK:
javascript复制async function sendTextMessage(chatId, text) {
const resp = await client.im.message.create({
params: { receive_id_type: 'chat_id' },
data: {
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text }),
},
});
return resp;
}
这里有个坑:content 必须是 JSON 字符串,不是普通对象。很多人直接传 content: { text } 会报 message content invalid。JSON.stringify 不能省。
如果想让回复更美观、能解析简单 Markdown,可以用 interactive 卡片:
javascript复制async function sendCardMessage(chatId, markdownText) {
const content = {
config: { wide_screen_mode: true },
header: {
title: { tag: 'plain_text', content: process.env.BOT_NAME || 'Clawdbot' },
},
elements: [
{
tag: 'div',
text: { tag: 'lark_md', content: sanitizeMarkdown(markdownText) },
},
],
};
await client.im.message.create({
params: { receive_id_type: 'chat_id' },
data: {
receive_id: chatId,
msg_type: 'interactive',
content: JSON.stringify(content),
},
});
}
5.5 Markdown 适配:把代码块、mermaid 这类"飞书无力渲染"的内容处理掉
这里要专门提醒一个坑。Clawdbot 的回复是标准 Markdown,里面经常带代码块,甚至带 mermaid` 流程图。飞书文本消息不支持 Markdown,卡片消息的 `lark_md` 也只支持一部分语法,对 mermaid` 这种代码块更是无能为力,直接发出去会变成一坨没有渲染的原始文本,非常难看。
我的做法是写一个 sanitizeMarkdown 函数:
javascript复制function sanitizeMarkdown(md) {
// 去掉 mermaid 代码块,替换成文字说明
md = md.replace(/```mermaid\n[\s\S]*?```/g, '[流程图暂不支持在飞书渲染,请查看源码仓库]');
// 其他代码块尽量保留,但去掉语言标记那一行
md = md.replace(/```(\w+)?\n/g, '[代码块]\n');
// 标题转成加粗
md = md.replace(/^#{1,4}\s+/gm, '【');
md = md.replace(/\n$/gm, '\n');
// 去掉行内 code 标记
md = md.replace(/`([^`]+)`/g, '$1');
return md;
}
如果你对格式要求高,可以进一步把 Markdown 转成飞书 post 富文本结构,无非是逐行解析,遇到标题、加粗、链接分别映射到富文本的对应标签。第一版先做"能看、不乱码"就行,别在这里耗太多时间。
6. 高频报错排查:2700002 与那些让人头秃的细节
6.1 飞书错误码 2700002:URL 校验失败,先查这三件事
很多人在配置 Webhook 事件订阅时遇到 2700002,页面提示"URL 校验失败"之类。这个错误码对应的核心问题就是:飞书服务器在保存回调地址时,尝试往你的 URL 发了一个验证请求,但没有得到它预期的响应。说白了,就是你的服务端没有正确响应 challenge 校验。
排查按这三步走:
第一,确认你的回调地址能从公网访问。飞书服务器不会访问 localhost 或内网地址,必须是 HTTPS 公网地址。可以先用浏览器或 curl 从外部测一下你的回调地址是否通。
第二,确认验证接口在 3 秒内返回了正确的 challenge。飞书发来的验证请求是 POST,你的服务端要解析出 challenge 字段,再原样返回。注意:如果开启了加密,返回的 challenge 也要按加密协议处理,不能直接明文返回。
第三,确认响应格式是 JSON,且 Content-Type 是 application/json。很多人用字符串拼接返回,飞书解析不了。
如果你用的是长连接模式,根本不会遇到 2700002。所以我一般建议:能走长连接就走长连接,省掉公网回调这一堆头疼事。
6.2 回调验证成功,但机器人收不到任何消息
这个问题比 2700002 更常见,而且排查点更隐蔽。配置界面显示"验证成功",但实际在群里 @ 机器人完全没反应。大概率是下面几个原因:
权限没有重新发布。这是头号原因。你在权限管理里加了 im:message.p2p_msg、im:message.group_at_msg,但在"版本管理与发布"里没有创建新版本,这些权限实际没生效。飞书开放平台的权限生效机制是跟着应用版本走的,只保存权限不发布版本等于没配。
事件订阅里没加 im.message.receive_v1。有些人只加了机器人能力,忘了在事件列表里订阅消息事件。这个没有任何提示,但消息就是推不过来。
服务端长连接没连上。pm2 进程虽然在跑,但日志里可能报连接失败。确认 App ID 和 App Secret 没填反,确认网络能访问飞书的 WebSocket 域名。
还有一个隐蔽问题:应用还没有通过审核或者还没有加测试成员。自建应用虽然不需要上架审核,但如果你是"测试模式",只有加入测试范围的人才能用。如果你自己在测试范围外,发给机器人的消息根本不会触发事件。
6.3 token 过期和 JSON 序列化问题
调用飞书 API 发送消息时,最常见的报错是 Invalid access token。飞书 API 使用的 tenant_access_token 有效期只有 2 小时,官方 SDK 会自动缓存并刷新,但如果你自己写 HTTP 请求,必须要做 token 缓存,别每次都重新获取,也别一直用同一个 token 超过 2 小时。
再一个是 content 格式问题。飞书发送消息接口要求 content 是 JSON 字符串,比如发送文本消息要传:
json复制{"text":"你好"}
如果你传的是:
json复制["你好"]
或者直接传了转义后的对象字符串,就报 content invalid。所有 JSON.stringify 都不能省,建议封装一个统一的消息发送函数,避免各处手写。
6.4 回复乱码、换行丢失、开头多个空格
飞书文本消息对换行有讲究。text 消息里的换行符要用 \n,但如果你在代码里写的是模板字符串里的真实换行,发出去可能被吞掉。建议统一先做一次处理:
javascript复制const safeText = reply.replace(/\r\n/g, '\n').replace(/\n/g, '\\n');
如果你的 Clawdbot 回复里带了 Markdown 行首的 > 引用符、列表符号,飞书文本消息不会解析,会原样显示。走 sanitizeMarkdown 时把这些符号清理掉会好看很多。
6.5 本地调试的小技巧
我调试时最喜欢用的方法是 curl 直接模拟飞书事件。本地起一个临时 HTTP 服务,手动 POST 一个测试事件,看自己的代码是否按预期处理,比反复在群里发消息高效得多:
bash复制curl -X POST http://localhost:3000/webhook/feishu \
-H "Content-Type: application/json" \
-d '{"type":"url_verification","challenge":"test123"}'
事件处理逻辑的日志一定要打好。建议在入口处打完整 event 的 header 和关键字段,在调模型前后打耗时,在异常分支打堆栈。日志打得好,排错时间能省一半。
7. 进入生产环境后值得做的三个增强
7.1 把对话记录写进飞书多维表格
Clawdbot 跑起来之后,你会很快发现会话上下文存在内存里重启就没了,而且对话记录没法追溯。飞书多维表格是一个很合适的落点,它本质是一个可编程的表格数据库,可以直接用开放 API 写入。
思路很简单:每次 handleMessage 处理完一轮对话后,异步把 {时间, 用户, 群组, 问题, 回复} 写成一行记录。用到的是多维表格的 bitable API,先拿到表格的 app_token 和 table_id,然后调用记录接口追加数据:
javascript复制async function logToBitable({ chatId, senderId, question, answer }) {
const token = await getTenantAccessToken();
const appToken = process.env.BITABLE_APP_TOKEN;
const tableId = process.env.BITABLE_TABLE_ID;
await fetch(`https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields: {
时间: new Date().toISOString(),
用户: senderId,
群组: chatId,
问题: question,
回复: answer,
},
}),
});
}
这个表记录积累起来之后,可以用来分析团队都在问什么、哪类问题最多,甚至可以把高频问答抽出来做成知识库。多维表格本身就是飞书原生产品,团队成员直接看表格就行,不用另外开发管理后台。
7.2 用 uptime Kuma 把监控告警推进飞书群
如果你的 Clawdbot 服务本身需要监控(它挂了团队就哑巴了),或者你想让其他监控工具的告警也统一推到飞书群,uptime Kuma 是个零成本方案。
做法分两步。第一步,在 uptime Kuma 里给 Clawdbot 的服务地址加一个 HTTP 监控,频率设 1 分钟。第二步,在通知设置里添加 Webhook 通知,指向你的 Clawdbot 服务里的一个 /alert 接口,接口里做两件事:校验请求来源(太简陋的话至少校验一个自定义 header),然后调用飞书消息 API 把告警内容发到指定的运维群里。
javascript复制app.post('/alert', express.json(), (req, res) => {
const secret = req.headers['x-alert-secret'];
if (secret !== process.env.ALERT_SECRET) {
return res.status(401).json({ error: 'unauthorized' });
}
const { message } = req.body;
sendCardMessage(process.env.ALERT_GROUP_CHAT_ID, `监控告警:${message}`);
res.json({ ok: true });
});
这样做的好处是,整个团队的监控告警入口统一在飞书,钉钉、邮件、短信这些渠道全都可以砍掉,只留一个飞书群。
7.3 给机器人加一个内部 Web 面板,顺便接飞书免登
当机器人开始承载多个工具调用、需要看运行状态时,你可以给它加一个简单的 Web 管理面板。面板不需要自建账号体系,直接复用飞书免登能力:前端页面把用户重定向到飞书 OAuth 授权地址,拿到 code 后服务端换用户信息,白名单用户才能访问面板。
这个方案在 Vue 项目里很常见,流程是:
- 前端调用飞书网页授权链接,带上你的 App ID 和回调地址。
- 用户同意授权后,飞书重定向回你的回调地址,带
code。 - 前端把 code 传给后端,后端用
authen/v1/access_token接口换取用户身份。 - 校验通过后,再正常使用你自己的会话体系。
如果只是内部小范围使用,这个面板可以很简单:展示当前会话数量、最近对话记录、Clawdbot 进程状态。如果给团队用,可以做成一个"Clawdbot 控制台",管理员在里面开关工具、查看日志。飞书免登省掉了自建账号体系的大量工作,很划算。
8. 个人经验补充:这个方案的适用边界
部署完成、群里跑起来之后,我有几点个人体会想补充。Clawdbot 接飞书这套方案,最适合的场景是内部工具助手、自动化运维入口、团队知识问答。它不适合用来做对外客服、也不适合承载敏感数据的高合规场景,因为这些场景需要严格的租户隔离、操作审计、数据脱敏,普通自部署默认配置达不到。
消息格式的坑比想象中多。飞书对 Markdown 的渲染支持很有限,Clawdbot 默认输出又是标准 Markdown,所以 sanitizeMarkdown 这个函数不是可有可无的优化,而是必需的适配层。建议在早期就把代码块、mermaid、表格这些内容的降级策略定好,否则每次模型回复里带个流程图,群里就会多一条被"残缺格式"污染的回复。
工具调用权限要做最小化。我给 Clawdbot 接内部工具时,一开始把所有脚本都暴露给模型了,结果模型在某个会话里把一条删除命令的参数推断错,虽然最后被人工发现没出事,但那次之后我把工具分成两层:只读工具默认放行,写操作必须二次确认。飞书支持卡片按钮,可以让模型在要执行高危操作时先把确认卡片发到群里,等用户在卡片上点了"确认"再执行,这个是技术团队上生产环境前必须做的事。
再说一个选型上的建议:如果你的服务器部署环境对公网回调不友好,长连接模式是最省心的。它不需要对外暴露任何端口,出站连接到飞书服务器就行。团队内网环境、服务器只有出站权限的情况下,长连接几乎是唯一解。
最后分享一个小技巧:Clawdbot 上线后,我每天会花十分钟看多维表格里的对话记录,不是为了监控谁在用,而是看哪些问题模型答得不好。时间长了你会发现,很多问题不是模型能力不够,而是上下文里缺信息。把团队常用的内部知识、项目背景写进系统提示词里,机器人效果会有一个质的提升。这个调优过程,才是 Clawdbot 这种自托管方案真正的价值所在。
