从轮询到实时消息:用 ntfy + n8n 搭建消息驱动的自动化流水线

从轮询到实时消息:用 ntfy + n8n 搭建消息驱动的自动化流水线

2026-06-05

背景

我维护的公众号文章流水线一直靠 cron 轮询驱动。每 2 分钟扫描一次 ntfy 主题,判断有没有新消息。够用,但明显粗糙——大部分轮询是空跑,消息到达后要等最长 2 分钟才能触发。随着接入的自动化场景增多(青龙面板任务、Home Assistant 设备控制、通知转发),这个方案越来越捉襟见肘。

需要改成实时触发。但生态太碎片化:ntfy 原生支持 WebSocket,n8n 有社区节点,怎么把它们串起来又不引入复杂的外部依赖?

方案

核心思路是三个组件串成一条链:ntfy 做消息总线n8n 做工作流引擎WebSocket 做实时通道

ntfy.sh 提供免费的消息队列服务,支持 HTTP POST 发送、WebSocket 实时订阅、JSON 格式解析。n8n 的社区节点 n8n-nodes-websockets-trigger 可以原生连接 WebSocket 端点,不需要中间翻译层。

统一 Hook 规范

为了让消息自描述、好路由,设计了一套轻量的 Hook 格式:

{
  "target": "hermes",
  "hook": "gzh-article/create",
  "id": "evt-xxxxx",
  "payload": { "title": "文章标题", "record_id": "rec_xxx" }
}
  • target 决定消息去哪个子系统(hermes、qinglong、homeassistant 等)
  • hook 是具体动作(create、cancel、ping、task-run)
  • id 保证幂等,下游可以按 ID 去重

ntfy 主题用 rhook- 前缀 + 16 位随机编码防撞。同一个主题下所有子系统共享消息通道,由 n8n 工作流里的 IF 节点分拣。

ntfy+n8n 架构总览:外部系统 → ntfy → WebSocket → n8n → SSH

实施

n8n 工作流节点结构:WebSocket Trigger → Code 校验 → IF 路由分支

分四步走。

1. 安装 WebSocket 社区节点

# 进入 n8n 容器
docker exec -it n8n sh

# 安装
npm install n8n-nodes-websockets-trigger@latest
npm install ws@^8.18.0

# 重启容器加载新节点
docker restart n8n

也可以用 docker-compose 把安装写到构建步骤里。

2. 创建工作流(9 节点)

WebSocket Trigger (wss://ntfy.sh/rhook-xxx/ws)
        Code 节点(校验 schema + 提取 target + hook       IF(target = hermes?)
    ├─ true  IF(hook = create?)
               ├─ true  SSH(create-pipeline.py)
               └─ false  IF(hook = ping?)
                            ├─ true  Pong 响应
                            └─ false  未匹配 hook
    └─ false  待扩展的其他 target

WebSocket Trigger 节点的关键配置:

参数
URL wss://ntfy.sh/你的主题/ws
Options { "json": true }

Code 节点用 jsCode 参数存储 JavaScript 逻辑,不要传 languagecode(n8n 2.x 的坑)。

IF 节点替代了 Switch V2,原因是后者在 n8n 2.19 上需要额外配置 mode: "rules" 参数才能正常工作,IF 节点开箱即用。

3. 处理 SSH 的 non-interactive shell 问题

n8n 的 SSH 节点默认使用 non-interactive shell,不加载 .bashrc.bash_profile。这意味着你装在用户目录下的 CLI 工具(比如 hermespip--user 安装)都不在 PATH 里。

解决方案是用 bash -l -c 强制加载 login shell:

const shellEscape = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
const sshCmd = "bash -l -c " + shellEscape("cd /project && python3 scripts/deploy.py ...");

4. 激活工作流

curl -X POST https://your-n8n-host:5678/api/v1/workflows/{id}/activate \
  -H "X-N8N-API-KEY: your-api-key"

激活后,ntfy 主题收到消息,WebSocket 通道在毫秒级内推送到 n8n,经过 Code 节点校验→IF 分拣→SSH 执行,端到端延迟取决于 SSH 连接的建立时间。

统一 Hook 规范:消息格式与已定义路由表

踩坑记录

SSH PATH 问题

这是排查最久的。症状:SSH 节点执行脚本时报 FileNotFoundError: hermes,但手动 SSH 登录后执行完全正常。原因就是 non-interactive shell 不加载 .bashrc。统一用 bash -l -c 包装所有 SSH 命令即可解决。

Code 节点参数名

n8n 工作流 API 创建 Code 节点时,传 language: "javascript" 会导致 Unsupported language: javascript 报错。n8n 2.x 的 Code 节点只用 jsCode 参数存代码,把其他参数去掉就好了。

Switch V2 vs IF

Switch V2 在 2.19 版本上需要明确传 mode: "rules" 参数,否则报 output 4 not allowed。如果只有两个分支,直接用 IF 节点更省事。

重复触发

这是设计层面的坑。cron 轮询脚本中曾有一个字符串比较的 bug("skAuWM6PHQNO" > "TfVFwc5C7TaU" 按 ASCII 首字母比较),导致消息指针永远不前进,每 2 分钟重复处理同一条消息,产生 8 份重复流水线。改用 Unix 时间戳比较后修复。

另外要注意:两条自动触发路径(cron 直接调用 + 飞书 Workflow 状态变更触发)如果同时启用同一选题,会创建两套重复的流水线。

测试验证

两条测试链路全部通过:

心跳检测 — 发送 { target: "hermes", hook: "hermes/ping" } → 收到 { ok: true, pong: "2026-06-05T12:43:21Z" }。链路过:ntfy → WebSocket → Code → IF → Pong 响应。

流水线触发 — 发送 { target: "hermes", hook: "gzh-article/create", payload: { title: "测试", record_id: "rec_test" } } → SSH 执行成功,kanban 卡片创建完成。

扩展方向

这个架构天然支持多 target 扩展。目前只走了 hermes 分支,其他 target 可以按同样模式接入:

  • qinglong — target 匹配后走 HTTP Request 调用青龙面板 API 运行任务
  • homeassistant — 调用 HA API 控制设备开关
  • notify — 转发到 ntfy 的其他主题或飞书

也可以加一个 config-reload hook,让消息通道同时承担配置热加载的职责。

总结

ntfy + n8n WebSocket 这个组合的优点是零外部依赖——ntfy 免费、n8n 已有,WebSocket 社区节点即装即用,不需要 Redis、RabbitMQ 之类的消息中间件。适合已有 n8n 部署、需要轻量消息触发的场景。

缺点是 ntfy.sh 是公共服务,消息经过外部网络,不适合内网敏感场景。如果对延迟或安全性有更高要求,可以自托管 ntfy 服务器(单二进制部署,资源消耗极低)。