飞书机器人热点推送
本文说明 ai-hot-trend 当前已接入的飞书群机器人热点推送:它如何从项目热点源拉取数据、如何完成鉴权和签名、如何由 Vercel 每天定时触发,以及需要怎样配置。
适用范围
- 推送目标是飞书群里的自定义机器人(Webhook),不是向个人私聊直接发送消息。
- 需要在飞书群中添加自定义机器人,并取得 Webhook 地址。
- 个人飞书账号通常也可在自己创建或有管理权限的群中添加机器人;如果群设置里没有“自定义机器人”,说明当前租户策略可能已限制该能力。
全链路流程
Vercel Cron / 手动 HTTP 请求
|
v
GET or POST /api/notify/feishu/daily
|
+--> 校验 Authorization: Bearer <token>
| 或 x-cron-token
|
v
pushDailyTrendDigestToFeishu()
|
+--> 按配置的热点来源并行调用 queryTrendItems()
+--> 将各来源 Top N 热点拼成文本摘要
+--> 可选:使用机器人密钥生成 timestamp + sign
|
v
飞书自定义机器人 Webhook
|
v
飞书群消息
组件职责与原理
1. 定时触发
vercel.json 配置了:
{
"crons": [
{
"path": "/api/notify/feishu/daily",
"schedule": "0 1 * * *"
}
]
}
Cron 表达式使用 UTC:0 1 * * * 表示每天 UTC 01:00,即 Asia/Shanghai 的每天 09:00。
Vercel 调用 /api/notify/feishu/daily 后,会进入 Hono 的通知路由。路由同时支持 GET 和 POST,因此既能被 Vercel Cron 调用,也便于本地或 CI 手动验证。
2. 接口鉴权
推送接口位于:
/api/notify/feishu/daily
请求需要以下任意一种请求头:
Authorization: Bearer <FEISHU_PUSH_CRON_TOKEN>
或:
x-cron-token: <FEISHU_PUSH_CRON_TOKEN>
服务端依次读取:
FEISHU_PUSH_CRON_TOKENCRON_SECRET(兼容 Vercel 的常用变量名)
只要配置了 token,缺失或不匹配的请求会返回 401 Unauthorized。生产环境未配置 FEISHU_PUSH_CRON_TOKEN 或 CRON_SECRET 时,服务会拒绝执行推送,避免公开接口被滥用。
3. 热点数据聚合
推送服务复用现有的 queryTrendItems(),不额外维护第二套爬取或缓存逻辑。
每个来源都会按相同参数拉取热点:
source:热点来源,例如ai、github、npm、weibo、zhihuperiod:时间范围,支持today、week、monthlimit:每个来源最多推送多少条offset:固定为0,即只推送当前排行榜前 N 条
来源请求会并行执行。某个来源失败时,其他来源仍会正常推送;消息会在该来源位置标记“拉取失败”,接口响应中的 failedSources 也会列出失败来源,方便从 Vercel 日志排查。
4. 消息格式
当前使用飞书 text 消息,不依赖卡片消息模板。消息内容按来源分组,包含:
- 推送标题与时间范围
- 按
FEISHU_DAILY_TIMEZONE格式化的更新时间 - 每个来源的名称
- 热点排名、标题、星标/热度字段与原始链接
标题超过 56 个字符会截断,避免单条消息过长。
5. 飞书 Webhook 与签名
服务端向 FEISHU_BOT_WEBHOOK_URL 发送 JSON:
{
"msg_type": "text",
"content": {
"text": "每日热点推送内容"
}
}
如果配置了 FEISHU_BOT_SECRET,会附加:
{
"timestamp": "Unix 秒级时间戳",
"sign": "HMAC-SHA256 Base64 签名"
}
签名的目的,是让飞书确认消息来自持有机器人密钥的服务端。仅在飞书机器人安全设置中开启“签名校验”时填写 FEISHU_BOT_SECRET;未开启时留空即可。
配置步骤
1. 在飞书创建机器人
- 创建或进入目标群聊。
- 在群设置中打开“机器人”或“群机器人”。
- 添加“自定义机器人”。
- 复制机器人 Webhook 地址。
- 建议启用“签名校验”,并保存机器人密钥。
Webhook 和密钥都属于敏感信息,不要提交到 Git 仓库,也不要写入前端环境变量(例如 NEXT_PUBLIC_*)。
2. 配置项目环境变量
在部署平台和本地 .env 中配置:
# 必填:飞书机器人 Webhook
FEISHU_BOT_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxx
# 可选:仅在飞书机器人开启签名校验后填写
FEISHU_BOT_SECRET=
# 建议必填:保护 /api/notify/feishu/daily 的随机长 token
FEISHU_PUSH_CRON_TOKEN=replace-with-a-long-random-token
# 可选:默认 ai,github,npm,weibo,zhihu
FEISHU_DAILY_SOURCES=ai,github,npm,weibo,zhihu
# 可选:today | week | month,默认 today
FEISHU_DAILY_PERIOD=today
# 可选:每个来源的 Top N,默认 5,代码上限为 10
FEISHU_DAILY_LIMIT=5
# 可选:消息显示时间使用的时区,默认 Asia/Shanghai
FEISHU_DAILY_TIMEZONE=Asia/Shanghai
FEISHU_DAILY_SOURCES 仅接受项目已注册的热点来源:
github,npm,vue,react,node,ai,python,component,zhihu,juejin,v2ex,weibo
包含不支持的来源或非法的 period/limit 时,接口会返回结构化错误,不会静默跳过。
3. 配置 Vercel
在 Vercel 项目的 Production 环境变量中设置:
FEISHU_BOT_WEBHOOK_URLFEISHU_PUSH_CRON_TOKENFEISHU_BOT_SECRET(如果开启签名校验)- 其他可选的
FEISHU_DAILY_*配置
为了让 Vercel Cron 的请求通过鉴权,推荐同时设置:
CRON_SECRET=<与 FEISHU_PUSH_CRON_TOKEN 相同的值>
这样 Vercel Cron 使用 Authorization: Bearer <CRON_SECRET> 调用时,项目可以直接校验通过。也可以只使用 CRON_SECRET,因为服务会将它作为 FEISHU_PUSH_CRON_TOKEN 的回退值。
部署后,Vercel 会读取根目录的 vercel.json 并按计划调用接口。
本地验证
先启动开发服务:
pnpm dev
再触发一次推送:
curl -X POST "http://localhost:3000/api/notify/feishu/daily" \
-H "Authorization: Bearer replace-with-a-long-random-token"
成功时会返回类似:
{
"success": true,
"data": {
"pushedAt": "2026-07-20T01:00:00.000Z",
"period": "today",
"sources": ["ai", "github", "npm", "weibo", "zhihu"],
"failedSources": []
}
}
常见响应:
401:请求未携带 token,或 token 不匹配。502:Webhook 地址/签名配置错误、飞书返回失败。success: true且failedSources非空:飞书消息已发出,但其中部分热点来源拉取失败。
不同部署平台的定时任务
Vercel:vercel.json 中的 crons 是什么
vercel.json 的 crons 允许在 Vercel 项目中以配置方式声明定时任务。它不会在 Vercel 上常驻运行一段后台脚本,而是让 Vercel 的调度器在指定时间向已部署应用的 HTTP 路径发起请求。
本项目的配置是:
{
"crons": [
{
"path": "/api/notify/feishu/daily",
"schedule": "0 1 * * *"
}
]
}
它表示每天 UTC 01:00(北京时间 09:00),Vercel 会向生产环境的 /api/notify/feishu/daily 发起一次 GET 请求。随后由 Hono 路由执行热点查询、消息拼装和飞书 Webhook 推送。
换句话说,crons 只负责“何时调用”,业务逻辑仍在项目服务端。修改 vercel.json 后要重新部署,Vercel 才会读取新计划。
为保护接口,请在 Vercel Production 环境变量中同时设置:
FEISHU_PUSH_CRON_TOKEN=<随机长 token>
CRON_SECRET=<与 FEISHU_PUSH_CRON_TOKEN 相同的值>
Vercel Cron 会携带 CRON_SECRET 作为 Bearer token;本项目也会把 CRON_SECRET 作为 FEISHU_PUSH_CRON_TOKEN 的回退值进行校验。
官方文档:https://vercel.com/docs/cron-jobs
Vercel 部署配置清单
在 Vercel 项目中打开 Settings -> Environment Variables,并在 Production 环境配置以下变量:
| 变量 | 是否必填 | 说明 |
|---|---|---|
FEISHU_BOT_WEBHOOK_URL | 必填 | 飞书群自定义机器人的完整 Webhook 地址 |
FEISHU_PUSH_CRON_TOKEN | 必填 | 保护推送接口的随机长 token |
CRON_SECRET | 必填 | 与 FEISHU_PUSH_CRON_TOKEN 保持完全相同,供 Vercel Cron 鉴权 |
FEISHU_BOT_SECRET | 可选 | 仅当飞书机器人开启签名校验时填写 |
FEISHU_DAILY_SOURCES | 可选 | 逗号分隔的热点来源列表 |
FEISHU_DAILY_PERIOD | 可选 | today、week 或 month;默认 today |
FEISHU_DAILY_LIMIT | 可选 | 每个来源的推送条数;默认 5,上限 10 |
FEISHU_DAILY_TIMEZONE | 可选 | 消息中显示时间的时区;建议 Asia/Shanghai |
推荐的 Production 配置:
FEISHU_BOT_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxx
FEISHU_PUSH_CRON_TOKEN=replace-with-a-long-random-token
CRON_SECRET=replace-with-the-same-token
FEISHU_DAILY_SOURCES=ai,github,npm,weibo,zhihu
FEISHU_DAILY_PERIOD=today
FEISHU_DAILY_LIMIT=5
FEISHU_DAILY_TIMEZONE=Asia/Shanghai
不要把 Webhook、机器人密钥或 token 配置为 NEXT_PUBLIC_* 环境变量,否则敏感信息可能暴露给浏览器端。
可推送的热点来源
FEISHU_DAILY_SOURCES 可从以下来源中任选一个或多个,并使用英文逗号分隔:
ai,github,npm,vue,react,node,python,component,zhihu,juejin,v2ex,weibo
| 值 | 推送内容 |
|---|---|
ai | AI 生态趋势 |
github | GitHub 热门仓库 |
npm | npm 包趋势 |
vue、react、node、python | 对应技术生态趋势 |
component | 组件库趋势 |
zhihu、juejin、v2ex、weibo | 社区与内容热点 |
例如,只推送 AI、GitHub 和 Python 的当日热点,每个来源取前 10 条:
FEISHU_DAILY_SOURCES=ai,github,python
FEISHU_DAILY_PERIOD=today
FEISHU_DAILY_LIMIT=10
未配置 FEISHU_DAILY_SOURCES 时,默认推送 ai,github,npm,weibo,zhihu。
推送时间配置
时间由根目录 vercel.json 的 schedule 控制,不通过环境变量设置:
{
"path": "/api/notify/feishu/daily",
"schedule": "0 1 * * *"
}
Vercel Cron 使用 UTC;当前 0 1 * * * 表示每天 UTC 01:00,即北京时间每天 09:00。常见时间换算如下:
| 北京时间 | schedule |
|---|---|
| 每天 08:00 | 0 0 * * * |
| 每天 09:00 | 0 1 * * * |
| 每天 12:30 | 30 4 * * * |
| 每天 18:00 | 0 10 * * * |
修改 vercel.json 后需要重新部署,新的 Cron 计划才会生效。FEISHU_DAILY_TIMEZONE 只影响飞书消息内显示的时间,不影响 Vercel 的实际调度时间。
部署后手动验证
配置保存、重新部署完成后,可用以下命令手动触发一次推送:
curl -X POST "https://your-domain.example.com/api/notify/feishu/daily" \
-H "Authorization: Bearer your-feishu-push-cron-token"
飞书群收到消息且接口返回 success: true,表示 Webhook、热点来源和 Vercel 环境变量均已生效。若返回 401,检查 Authorization 中的 token 是否与 FEISHU_PUSH_CRON_TOKEN 一致;若返回 502,检查飞书 Webhook、飞书签名密钥和 Vercel Functions 日志。
Cloudflare Workers:推荐作为定时触发器
当前项目是 Next.js + Node.js + Prisma + MySQL。最稳妥的 Cloudflare 方案是部署一个小型 Worker,使用 Cloudflare Cron Trigger 定时调用当前应用的推送接口,而不是立刻把整套应用迁移到 Worker。
这样热点查询、缓存、MySQL 与飞书推送逻辑仍只维护一份;Cloudflare 只负责调度和鉴权转发。主应用可以继续部署在 Vercel、其他云平台或自建服务器。
新建独立 Worker 项目,创建 wrangler.toml:
name = "ai-hot-trend-scheduler"
main = "src/index.ts"
compatibility_date = "2026-07-20"
[triggers]
crons = [ "0 1 * * *" ]
[vars]
APP_URL = "https://your-app.example.com"
Cloudflare Cron Trigger 使用 UTC,因此 0 1 * * * 同样对应北京时间每天 09:00。
创建 src/index.ts:
interface Env {
APP_URL: string
FEISHU_PUSH_CRON_TOKEN: string
}
export default {
async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext) {
ctx.waitUntil((async () => {
const response = await fetch(`${env.APP_URL}/api/notify/feishu/daily`, {
method: 'POST',
headers: {
Authorization: `Bearer ${env.FEISHU_PUSH_CRON_TOKEN}`,
},
})
if (!response.ok) {
throw new Error(`Daily Feishu push failed: ${response.status}`)
}
})())
},
} satisfies ExportedHandler<Env>
设置 Worker 密钥并部署:
npx wrangler secret put FEISHU_PUSH_CRON_TOKEN
npx wrangler deploy
输入的 token 必须与主应用的 FEISHU_PUSH_CRON_TOKEN 相同。APP_URL 可放在 [vars];如果不希望公开域名,也可通过 npx wrangler secret put APP_URL 设置。
本地测试 scheduled handler:
npx wrangler dev --test-scheduled
curl "http://localhost:8787/__scheduled"
如果要把整个 Next.js 应用直接部署到 Cloudflare Workers,需要额外接入 Next.js 的 Cloudflare 运行时适配,并重新评估 Prisma/MySQL 的兼容性、数据库连接池和网络访问。这是部署架构迁移,不是增加 Cron Trigger 就能完成的改动。
官方文档:https://developers.cloudflare.com/workers/configuration/cron-triggers/
自建服务器:使用 systemd timer
自建 Linux 服务器时,推荐让 Next.js 应用继续作为常驻服务运行,再用 systemd timer 每天调用本机 API。不要把定时器嵌入 Node 进程,这样应用重启、部署和服务器恢复时更容易管理。
企业自建部署的完整链路
企业自建场景中,建议把“Web 应用运行”和“每天触发任务”拆成两个由 systemd 管理的单元:
浏览器 / API 调用
|
v
Nginx(TLS、域名、反向代理)
|
v
Next.js 常驻服务(systemd service,监听 127.0.0.1:3000)
|
v
Hono: /api/notify/feishu/daily
|
+--> queryTrendItems() --> 外部热点来源
|
+--> 飞书机器人 Webhook
每天 09:00 Asia/Shanghai
|
v
systemd timer
|
v
ai-hot-trend-feishu-push.service(一次性 curl)
|
v
http://127.0.0.1:3000/api/notify/feishu/daily
这里有两个不同的 systemd 单元:
ai-hot-trend.service:长期运行的 Next.js 应用进程。ai-hot-trend-feishu-push.timer:保存“每天何时执行”的计划,并在时间到达时启动一次性的ai-hot-trend-feishu-push.service。
业务代码只在 Next.js 服务中执行一次;timer 不直接访问热点源或飞书,它只通过本机 HTTP 调用现有 API。这使手动触发、Vercel Cron、Cloudflare Worker Cron 和自建服务器的行为保持一致。
1. systemd timer 是什么,如何理解
systemd 是大多数现代 Linux 发行版的服务管理器。它不仅可以拉起常驻进程,也可以管理“在指定时间启动某个服务”的任务。
可以把它理解为:
.service文件定义“要做什么工作”。对于本项目,工作是调用一次推送 API。.timer文件定义“何时执行这个工作”。对于本项目,工作日或每天的 09:00 触发一次。systemctl enable让该计划在服务器重启后仍然生效。journalctl记录每次执行的标准输出、错误和退出状态。
与传统 crontab 相比,systemd timer 更容易查看下次执行时间、追踪失败日志、依赖网络就绪,并可通过 Persistent=true 补跑服务器停机期间错过的一次任务。
需要特别区分:这里推荐使用的是“操作系统级 systemd timer”;不推荐的是“嵌在 Next.js/Node 进程里的应用内定时器”。两者不是同一种方案。
2. 如何让 Next.js 常驻运行
生产环境中,先在部署目录构建项目,然后由 systemd 负责启动和重启 pnpm start。示例假定应用位于 /srv/ai-hot-trend,运行用户为 ai-hot-trend,并由 Nginx 反向代理。
创建运行用户和部署目录后,创建 /etc/systemd/system/ai-hot-trend.service:
[Unit]
Description=AI Hot Trend Next.js application
After=network.target
[Service]
Type=simple
User=ai-hot-trend
Group=ai-hot-trend
WorkingDirectory=/srv/ai-hot-trend
Environment=NODE_ENV=production
EnvironmentFile=/etc/ai-hot-trend.env
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env pnpm start
Restart=always
RestartSec=5
TimeoutStopSec=30
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.target
pnpm start 会运行项目的 next start,Next.js 进程因此持续监听服务端口。若服务器上的 pnpm 不在 /usr/local/bin 或 /usr/bin,请先执行 command -v pnpm,再按实际路径调整 PATH 或 ExecStart。
启用应用服务:
sudo systemctl daemon-reload
sudo systemctl enable --now ai-hot-trend.service
sudo systemctl status ai-hot-trend.service
sudo journalctl -u ai-hot-trend.service -f
部署更新的推荐顺序是:拉取新代码、安装依赖、执行 pnpm build、确认迁移成功、然后执行 sudo systemctl restart ai-hot-trend.service。不要在运行中的进程目录里直接覆盖文件,也不要依赖手工在 SSH 会话里启动 pnpm start。
如果使用 Nginx,只将 Next.js 监听到回环地址,再由 Nginx 对外提供 HTTPS。例如反向代理的上游使用:
proxy_pass http://127.0.0.1:3000;
推送接口由 timer 直接访问 127.0.0.1:3000,不需要经过公网域名或 Nginx。可以在 Nginx 中额外拒绝外部访问 /api/notify/feishu/daily,保留 timer 的本机访问路径,进一步缩小暴露面。
3. 如何每天调用 API
timer 不会自己执行 HTTP;它负责按时间启动一个 one-shot service。该 service 使用 curl 向本机接口发送带 Bearer token 的 POST 请求:
09:00 到达
-> systemd 启动 ai-hot-trend-feishu-push.service
-> service 从 /etc/ai-hot-trend.env 读取 token
-> curl POST 127.0.0.1:3000/api/notify/feishu/daily
-> Hono 校验 token,查询热点并推送飞书
-> systemd 将成功或失败写入 journal
下面的 .service 与 .timer 配置就是这一流程的实现。建议先执行一次 service 验证接口、token 和飞书 Webhook 都正确,再启用 timer:
sudo systemctl start ai-hot-trend-feishu-push.service
sudo journalctl -u ai-hot-trend-feishu-push.service -n 100 --no-pager
确认无误后再执行:
sudo systemctl enable --now ai-hot-trend-feishu-push.timer
systemctl list-timers ai-hot-trend-feishu-push.timer
--retry 2 只处理短暂网络故障;如果 Next.js 服务在执行时间不可用,任务会以失败状态结束并留下日志。因此生产环境仍应对该 unit 的失败日志进行监控和告警。
4. 为什么不建议使用应用内定时器
这里不推荐的是在 Next.js 代码里使用 setInterval、node-cron 或类似方案直接执行每日推送。它在单实例的临时工具中可以工作,但不适合通常的企业生产部署,主要原因如下:
- 多实例会重复推送:扩容为两个或多个 Next.js 实例后,每个实例都会注册自己的定时器,除非再实现数据库锁或分布式选主。
- 重启会错过任务:发布、崩溃、机器重启或进程滚动重启时,内存中的 timer 会消失;恢复后通常不会知道是否错过了当天任务。
- 调度与应用生命周期耦合:应用是否成功启动、热更新、进程管理器重拉、数据库暂时不可用,都可能改变 timer 的注册和执行时机。
- 缺少系统级可观测性:应用日志通常只能说明代码执行过,难以像 systemd 那样直接查看任务状态、下次执行时间、退出码和独立日志。
- 需要自行补齐可靠性能力:幂等、分布式锁、失败重试、错过任务补跑、超时控制和告警都要在应用代码中重新实现。
systemd timer 将调度可靠性交给操作系统,而 Next.js 只专注提供一次可鉴权、可重复调用的推送 API。这种分层更符合企业的部署和运维方式。
应用内定时器并非绝对不能用:若系统始终单实例、允许偶发漏推,且已经实现数据库锁、任务记录和监控,它可以作为简化方案。但对每日热点推送这类需要可追踪、可恢复的任务,推荐优先使用 systemd timer、云平台 Cron 或独立任务调度系统。
先准备生产环境变量文件,例如 /etc/ai-hot-trend.env:
FEISHU_BOT_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/xxxx
FEISHU_BOT_SECRET=
FEISHU_PUSH_CRON_TOKEN=replace-with-a-long-random-token
FEISHU_DAILY_SOURCES=ai,github,npm,weibo,zhihu
FEISHU_DAILY_PERIOD=today
FEISHU_DAILY_LIMIT=5
FEISHU_DAILY_TIMEZONE=Asia/Shanghai
限制文件权限:
sudo chown root:root /etc/ai-hot-trend.env
sudo chmod 600 /etc/ai-hot-trend.env
创建 /etc/systemd/system/ai-hot-trend-feishu-push.service:
[Unit]
Description=Push daily AI hot trends to Feishu
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/ai-hot-trend.env
ExecStart=/usr/bin/curl --fail --silent --show-error --max-time 120 --retry 2 --retry-delay 10 -X POST http://127.0.0.1:3000/api/notify/feishu/daily -H "Authorization: Bearer \${FEISHU_PUSH_CRON_TOKEN}"
该任务调用 127.0.0.1:3000,不会经过公网反向代理。若应用端口不同,请同步修改 URL。
创建 /etc/systemd/system/ai-hot-trend-feishu-push.timer:
[Unit]
Description=Run the daily Feishu trend push at 09:00 Asia/Shanghai
[Timer]
OnCalendar=*-*-* 09:00:00 Asia/Shanghai
Persistent=true
RandomizedDelaySec=5m
Unit=ai-hot-trend-feishu-push.service
[Install]
WantedBy=timers.target
启用并验证:
sudo systemctl daemon-reload
sudo systemctl enable --now ai-hot-trend-feishu-push.timer
systemctl list-timers ai-hot-trend-feishu-push.timer
sudo systemctl start ai-hot-trend-feishu-push.service
sudo journalctl -u ai-hot-trend-feishu-push.service -n 100 --no-pager
Persistent=true 会在服务器错过执行时间后、恢复运行时补跑一次。RandomizedDelaySec=5m 用于避开整点并发;如必须精确在 09:00 执行,可以删除这一行。
传统 crontab 也能实现定时调用,但需要自行处理环境变量、日志和错过任务后的补跑。对生产环境而言,systemd timer 更适合这个每日任务。
常见问题
推送接口能访问但飞书没有消息
依次检查:
FEISHU_BOT_WEBHOOK_URL是否为当前群机器人的完整地址。- 飞书机器人是否启用了签名校验;若启用,
FEISHU_BOT_SECRET是否一致。 - Vercel 的环境变量是否配置在 Production 环境,并在重新部署后生效。
- Vercel Functions 日志中的飞书返回错误码和
message。
Vercel Cron 返回 401
将 Vercel 的 CRON_SECRET 与项目的 FEISHU_PUSH_CRON_TOKEN 设为相同的随机值,然后重新部署。不要用 URL 参数传 token,因为 URL 可能被日志记录。
想调整推送时间
修改根目录 vercel.json 的 cron 表达式并重新部署。Vercel 的 cron 表达式使用 UTC;例如北京时间每天 08:30 对应 UTC 30 0 * * *。