背景
我需要将飞书表格的状态变更实时通知到 kanban 流水线。最早用的是 cron 轮询——每 2 分钟扫一次。延迟高,而且空跑多:大部分轮询没有任何新消息。
目标是换成实时触发。飞书表格支持 Webhook 输出,通过 ntfy.sh 这个公共 pub-sub relay 做事件中转,后端需要有一个订阅端接收消息后执行本地脚本。
第一反应是用 n8n。它的可视化编排、社区节点、Webhook 节点——怎么看都合适。但实际走下来,这条路埋了三个坑。
方案一:n8n + WebSocket 社区节点(死胡同)
思路
ntfy.sh 提供 SSE(Server-Sent Events)和 WebSocket 两种实时订阅方式。n8n 社区有一个 n8n-nodes-websockets-trigger 节点,看起来就是干这个的。
在 n8n 容器里安装社区节点,配置 WebSocket URL,连上 ntfy 的 wss://ntfy.sh/{topic}/ws——消息确实收到了。执行历史里能看到 event: "message"。
问题
下游节点永不执行。Set 节点、Code 节点——全都卡在第一个节点上,像被截断了。
排查后发现根因在社区节点的 package.json:
# 触发器应有的 inputs
"inputs": []
# 这个节点实际定义的
"inputs": ["main"]
inputs: ['main'] 是一个普通节点的参数签名,不是触发器。n8n 的触发器节点应该用 inputs: []——表示”我产生数据,不接收上游数据”。用 ['main'] 意味着”我接收上游数据”,在 n8n 的拓扑图里,这个节点被当作普通节点而不是触发器节点。this.emit() 调用了,但数据传播不到下游。
这不是配置问题,是节点作者写错了元数据。社区版本冻结,上游已停更。这条路判了死刑。
更多踩坑
这条路上还撞到了几个 n8n 社区节点的参数问题:
| 问题 | 说明 |
|---|---|
Code 节点的参数叫 jsCode 不是 code |
文档与实现不一致 |
WebSocket URL 参数名是 websocketUrl 不是 url |
容易误写 |
Switch V2 缺 mode: "rules" |
报 “output 4 not allowed”,需显式声明 |
n8n API 的 active 和 tags 是只读 |
创建工作流后需单独调激活接口 |
方案二:n8n + 原生 Webhook 节点(部分失败)
思路
飞书 Workflow 支持 HTTP POST 到指定 URL。不用 WebSocket 了,直接用 n8n 的原生 Webhook 接收 POST 请求。这是 n8n 最成熟的功能之一,总该没问题了吧?
问题 —— 两个已知 bug
-
API 创建的 webhook 路径不注册:用 n8n API(POST /workflows)创建并激活工作流后,网页端访问 webhook URL 返回 404。必须手动在 UI 里打开工作流并保存一次,触发一次内部端点调用,webhook 才会生效。
-
Webhook 返回 200 后下游节点不执行:即使 webhook 能工作,n8n 的执行引擎只跑到第一个 Webhook 节点。Set、HTTP Request、Code——下游所有节点都原地踏步。社区早有报告:
-
n8n GitHub #14646(2025年4月):Webhook not responding after creating workflow via API
- n8n GitHub #21614(2025年11月):Deployment + Activation via API does not register webhook
这些 issue 的评论区里大量用户表示”每次部署都得手动保存一次才能用”,但核心团队迟迟没有在 LTS 分支上修复。
两个 bug 叠加,让 n8n 这条路线彻底无法用 API 自动化管理。如果每个工作流部署后都要手动去 UI 点一次保存才能用,那和手动操作又有什么区别?
方案三:Python 桥接脚本直连(成功)
核心思路

彻底跳过 n8n。ntfy 侧负责接收飞书的 HTTP 通知并广播到订阅者,本地跑一个 Python 脚本通过 SSE 订阅 ntfy 的消息流。收到消息后,用 HMAC-SHA256 签名验证身份,然后直接 POST 到本地的 pipeline server 执行脚本。
飞书表格 → ntfy.sh (public relay)
↓ SSE 订阅
ntfy-bridge.py (Python 脚本)
↓ HMAC-SHA256 签名
rhook-pipeline-server.py (:8645)
↓
create-pipeline.py → kanban 卡片

SSE 订阅实现
ntfy 提供了两种实时订阅方式:WebSocket 和 SSE。WebSocket 有握手开销,SSE 基于 HTTP 长连接,更轻量可靠。核心代码只有几十行:
import urllib.request
import json
sse_url = f"https://ntfy.sh/{topic}/sse"
req = urllib.request.Request(sse_url, method="GET")
req.add_header("Accept", "text/event-stream")
resp = urllib.request.urlopen(req, timeout=None)
current_data = ""
for line_bytes in resp:
line = line_bytes.decode("utf-8", errors="replace").strip()
if line.startswith("data: "):
current_data = line[6:] # 累积 data 行
elif line == "" and current_data:
msg = json.loads(current_data) # 空行分隔一条消息
if msg.get("event") == "message":
payload = json.loads(msg["body"])
forward_to_pipeline(payload)
current_data = ""

几个技术要点:
timeout=None:urllib 的 timeout=None 让连接保持无限长,适合 SSE 长连接- errors=”replace”:防止某个字节序列在解码时抛 UnicodeDecodeError 导致整个连接断开
- 空行判结束:SSE 协议以空行分隔事件,需要累积 data 行直到遇到空行才解析
- 重连:HTTP 错误后用
while True + time.sleep(5)重新建立连接
HMAC-SHA256 签名
消息从 ntfy 到达本地脚本后,需要转发到 pipeline server。转发时需要验证身份,防止伪造请求。
import hmac, hashlib
signature = hmac.new(
SECRET.encode(),
json_body.encode(),
hashlib.sha256
).hexdigest()
# 服务端验证
headers = {"X-Webhook-Signature": signature}
resp = requests.post(pipeline_url, data=json_body, headers=headers)
Secret 来源需要注意:如果 secret 存在 webhook_subscriptions.json 这种嵌套 JSON 文件里,需要先遍历找到正确的条目。不要直接 json.load() 后就当字符串用。
Pipeline Server
本地 HTTP 服务监听 0.0.0.0:8645,接收 POST 请求后验证签名,解析事件类型,然后通过 subprocess 调用 create-pipeline.py 创建 kanban 流水线卡片。
完整的请求格式:
{
"event_type": "gzh-article/create",
"title": "...",
"slug": "...",
"record_id": "..."
}
踩坑总结
SSE 桥接脚本
| 问题 | 现象 | 解决 |
|---|---|---|
| SSE 流中途断开 | json.decoder.JSONDecodeError | 累积完 data 再解析,不要逐行单独解析 |
| HMAC key 格式 | 读 JSON 后没有正确提取 | 遍历嵌套结构找到有 secret 字段的条目 |
| 连接中断后不恢复 | HTTP 错误后无重连 | while True + sleep(5) 循环 |
| systemd 启动失败 | exit-code 203/EXEC | chmod +x + ExecStart 用绝对路径 |
| urllib 解码错误 | UnicodeDecodeError 导致连接断开 | errors="replace" |
这场迁移教会我的
-
已知 bug 比新功能更致命:n8n 的 #14646 和 #21614 都是 2025 年就报告的 bug。已知 bug 意味着你花了时间排查发现不是自己的问题,而是框架的问题,而且短期内可能不会修复。
-
可视化编排的边界:n8n 在常见场景(定时、数据库操作、API 调用)上很稳定,但当你用到社区节点、触发器等边缘功能时,问题密度急剧上升。在这个问题上,50 行 Python 代码替代了一个 Docker 容器 + 一个社区节点 + 若干 API 调用。
-
SSE 比 WebSocket 更适合这个场景:ntfy 的 SSE 端点基于 HTTP 长连接,没有握手开销,Python 标准库就能消费。WebSocket 需要专用客户端库、心跳维持、重连逻辑——对一个单向消息订阅来说太重型了。
后续可做的优化
- pipeline server 注册为 systemd 服务,实现自启动和自动重启
- 添加心跳检测,长时间无消息时主动重连
- 扩展支持多个 event_type(通知青龙面板、Home Assistant 等)
- 考虑
ntfy subscribe --call命令行替代自定义 Python 脚本,进一步简化