把询盘通知接进飞书群:
从建机器人到收到第一条消息
建机器人、curl 试通、算签名、放进 Worker。一小时能配完,不需要额外服务。
表单提交之后,飞书群里出现一条消息。这篇只讲怎么配出来。
配完的效果是,有人填了询盘,你手机上响一下,点开就是内容。不用再一天刷五遍邮箱。
整个过程可以拆成五步:建机器人、选安全设置、用 curl 试通、算签名、放进 Worker。全部做完,熟练的话一小时以内。不需要额外服务,飞书自定义机器人本身不收费。
一、配完之后是什么样
先看结果,省得中途不知道自己在往哪走。表单提交,群里出现这样一条:
新询盘|德国|Anna Weber
产品:30W 一体机 x 200
留言:请报 FOB 宁波价,10 月底前要
2026-11-06 09:12
五行以内,手机上不折叠就能看全。点末行那个时间,跳到你自己的询盘记录页。
要靠的就是一个 HTTP 请求,往飞书给的那个地址 POST 一段 JSON。后面几步都是围着这一个请求转。
二、建一个自定义机器人
在目标的飞书群里点:群设置 → 群机器人 → 添加机器人 → 自定义机器人。给个名字,比如「网站询盘」,加进群。
完成后飞书会给你一个地址,长这样:
https://open.feishu.cn/open-apis/bot/v2/hook/8f3a1c2e-xxxx-xxxx-xxxx-xxxxxxxxxxxx
这个地址本身就是凭证。谁拿到它,谁就能往这个群里发消息。飞书官方文档里专门写了一句话:不要公布在 Gitlab、博客等可公开查阅的网站上,避免地址泄露后被恶意调用发垃圾消息。
所以这个地址只该放在两个地方:Worker 的环境变量,和你的密码管理器。不要写进代码仓库。
三、选一种安全设置
同一个页面往下拉,有三种安全设置,可以叠加,也可以只开一种。按你的情况选:
| 设置 | 怎么起作用 | 适合谁 |
|---|---|---|
| 自定义关键词 | 消息文本里必须出现其中一个词,最多 10 个 | 想五分钟就配完 |
| IP 白名单 | 只接受指定地址的请求,最多 10 个,支持 123.12.1.* | 有固定出口 IP 的服务器 |
| 签名校验 | 时间戳加密钥算 HmacSHA256,对不上直接拒 | 站放在 Worker、Vercel 这类平台上 |
站放在 Cloudflare Worker 上的,白名单这条路走不通:Worker 的出口 IP 不固定,你没有确定的值可以填。
实际能用的组合有两个。
只想快点跑通:开关键词,填一个每条消息都会带的词,比如 询盘。你的消息模板里出现这两个字就算过。
想稳一点:开签名校验,多写十行代码,第五步给。签名开了就不用关键词,两者同时开会互相卡。
关键词只对文本字段生效。 text、title 这类参数里出现才算,你设在链接的 href 里不算,图片里的文字也不算。
四、用 curl 先试通
写代码之前,先用一条命令确认地址是对的、群是对的。这一步能省掉后面一半的排查。
HOOK="https://open.feishu.cn/open-apis/bot/v2/hook/你的地址"
curl -s -X POST "$HOOK" \
-H "Content-Type: application/json" \
-d '{"msg_type":"text","content":{"text":"测试:新询盘|德国|Anna Weber"}}'
返回这样就算通了:
{"code":0,"msg":"success","data":{}}
code 是 0 才算成功。飞书有时候 HTTP 返回 200,code 却不是 0,要看 body,不要只看状态码。
如果这一步就失败,先对照第八节的返回码表,通常当场就能定位。地址错、关键词没匹配、机器人被移出群,都在这时候暴露,比写进代码之后再去翻日志省事。
五、算签名
这一段只跟开了签名校验的人有关。如果第三节你选的是关键词,可以直接跳到第六节。
开了签名的,每个请求多带两个字段:timestamp 和 sign。
“把 timestamp + "\n" + 密钥 作为签名字符串,使用 HmacSHA256 算法计算签名,再对结果做 Base64 编码。”
这是官方文档对这个算法的描述。它容易被读反:拼接出来的那段是 HMAC 的密钥,要签的数据是空字符串。把
飞书开放平台 · 自定义机器人使用指南ts当数据去签,报的就是 19021。
async function feishuSign(secret, ts) {
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(`${ts}\n${secret}`),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const buf = await crypto.subtle.sign("HMAC", key, new Uint8Array(0));
return btoa(String.fromCharCode(...new Uint8Array(buf)));
}
new Uint8Array(0) 就是那个空字符串。
两个常见的错法,都报同一个错误码 19021:
- 对
ts本身签名,而不是对空字符串签 timestamp用了毫秒。这个字段要的是秒,Math.floor(Date.now() / 1000)
签名有效期一小时(3600 秒),timestamp 要现算,不能写死。
想先用 curl 验证签名对不对,可以这样(HOOK 沿用上一步那个变量):
TS=$(date +%s)
SECRET="你的密钥"
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$(printf '%s\n%s' "$TS" "$SECRET")" -binary | base64)
curl -s -X POST "$HOOK" -H "Content-Type: application/json" \
-d "{\"timestamp\":\"$TS\",\"sign\":\"$SIGN\",\"msg_type\":\"text\",\"content\":{\"text\":\"测试:带签名\"}}"
六、把推送放进 Worker
下面是 Cloudflare Worker 里能直接用的写法,配合表单那条 POST 一起跑。
export default {
async fetch(request, env, ctx) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const form = await request.formData();
const data = {
name: String(form.get("name") || "").slice(0, 60),
email: String(form.get("email") || "").slice(0, 120),
msg: String(form.get("message") || "").slice(0, 300),
};
// feishuSign 见第五步
const ts = Math.floor(Date.now() / 1000);
const body = {
timestamp: String(ts),
sign: await feishuSign(env.FEISHU_SECRET, ts),
msg_type: "text",
content: { text: [
`新询盘|${data.name}`,
data.email,
data.msg,
].join("\n") },
};
// 推送交给后台,访客不用等
ctx.waitUntil(fetch(env.FEISHU_HOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(r => r.text()).then(t => console.log("feishu:", t)));
return new Response("ok", { status: 200 });
},
};
三个地方值得说明。
1. 为什么用 waitUntil
是为了让访客不用等。 推送放进后台跑,请求立刻返回,访客点完提交马上看到成功页。
写成 await fetch(...) 也能发出去,但飞书那边慢半秒,访客就多等半秒。
waitUntil 在响应发出后最多再跑 30 秒。飞书正常几百毫秒就回,够用。
2. 推送之前先存一份原文
发一封邮件给自己、写进 D1 或者 KV,随便哪种。这样即使推送失败,这条询盘还在。顺序别反:先存,后推。
3. 字段截断一下
slice 那几行是防止有人塞一万字进来,撑爆 20 KB 的请求体上限。
七、消息怎么写才好读
飞书的 text 消息支持换行,不支持 Markdown。冒号、竖线、方括号都是普通字符,排版只能靠换行和符号。
一条管用的询盘通知,通常有这几行:
- 第一行给判断要不要现在看的信息:来源站点、客户名或公司名
- 第二行给判断值不值得跟的信息:产品、数量
- 第三行给原文:留言,截到 300 字
- 最后一行给入口:后台链接或提交时间
不要把整个表单原样贴上去。字段一多,手机上要滑两屏,人就不看了。
关于 @ 人
飞书支持 <at user_id="ou_xxxx"></at>,能直接叫醒指定的人。用之前想清楚,一天十几条 @ 会让人把群免打扰。带附件的询盘 @ 一次,其余的不用。
查询 user_id 要调通讯录接口,嫌麻烦的话,用 <at user_id="all"></at> 也行,但那个是 @ 所有人,慎用。
八、报错码对照
出问题基本都在这一张表里。
| code | 提示 | 通常的原因 |
|---|---|---|
| 19021 | sign match fail or timestamp is not within one hour | 对 ts 签名而不是空字符串,或 timestamp 用了毫秒,或后台重置过密钥 |
| 19024 | Key Words Not Found | 关键词没出现在文本字段里,或者只写在链接、图片里 |
| 19022 | Ip Not Allowed | 开了白名单,但请求来源不在里面 |
| 9499 | Bad Request | JSON 格式错误,或者请求体超过 20 KB |
| 11232 | 频率超限 | 单机器人 100 次/分钟、5 次/秒,整点高峰容易撞上 |
机器人被移出群、群被解散这类情况,返回值不固定,有时是 9499,有时什么都不返回。最快的判断办法是去群里看一眼机器人还在不在。
19021 里有一半是密钥问题。 后台点过「重置」,旧密钥当场失效,代码里那个再怎么算都签不过。改了密钥记得同步改环境变量。
九、怎么确认它还在工作
推送坏掉的时候是安静的。 函数不报错,飞书也不报错,就是没消息。
1. 每周手动测一次
配完之后给自己留一个固定动作:每周挑一天,提一次测试表单,看群里有没有出现。三十秒的事。
2. 按静默天数判断
如果站上询盘本来就不多,可以换个判断方式:按过去三个月的日均量定一个天数,超过这个天数零消息,就去翻一眼 Worker 日志。
一个每月二十条的站,连续五天没动静基本可以确认有问题。一个月两条的站,这个办法不适用,还是用每周手动测。
你按这个顺序往下查:先看表单有没有触发函数,再看飞书返回的 code,最后确认机器人还在群里。三层依次走,比乱猜快。
十、留三行记录
只有你自己知道怎么配的,那它就只能由你维护。 配完之后,在运维笔记里写三行:
- webhook 地址和密钥存在哪儿
- 询盘原文存在哪儿
- 改了密钥或换了群,需要同步改哪一处
下次换人接手、或者半年后自己回来看,这三行能省掉重新摸一遍的时间。
常见问题
Q1飞书自定义机器人要付费吗?
不要。群机器人本身免费,只受频率限制:单个机器人 100 次/分钟、5 次/秒。需要付费的是企业内部应用的接口权限,自定义机器人不在这套体系里。
Q2关键词和签名,两个都开行不行?
能开,但没必要。关键词校验的是消息文本里有没有那个词,签名校验的是请求来源。两个同时开,只要消息里没带那个词,签名算对了照样被拒。选一个就够。
Q3静态站没有服务器,怎么发这个请求?
用一个 Cloudflare Worker 接住表单那条 POST,在 Worker 里发出去。不要在浏览器里直接发:webhook 地址会暴露在前端代码里,任何人都能拿去往你群里灌消息。
Q4机器人被移出群,函数还能发成功吗?
返回值不固定,有时是 9499,有时 body 是空的,函数本身不会报错。所以每周手动提交一次测试表单、看群里有没有出现,比盯日志靠谱。
资料来源
- 飞书开放平台 · 自定义机器人使用指南(webhook 地址格式、三种安全设置、签名算法与 1 小时有效期、100 次/分钟与 5 次/秒频率限制、20 KB 请求体上限、19021 / 19022 / 19024 / 9499 / 11232 等错误码、整点限流提醒、密钥重置后失效)
- 文中
ctx.waitUntil的 30 秒上限、「先存原文再推送」、字段截断长度、消息模板的排序方式,是常见的工程做法,不是飞书文档的强制要求。