Dify 插件开发实验(06):通知渠道插件——如何把 Dify 推送到钉钉/企业微信等渠道?
Dify 实验系列 · 插件开发 06/12 | 实验编号:DIFY-106-06
基于 Dify 1.16.1 实测(2026-08)
1. 业务场景
先讲一个我们实际遇到的场景。
客服工单 SaaS 的通知场景:用户提交工单后,客服要收到「新工单待处理」的提醒;工单状态变了,用户要收到「您的工单已更新」的消息。团队内部用企业微信,对外通知走钉钉风格 webhook,还可能对接自建的消息网关——同一个通知内容,要按渠道适配格式发出去,发失败了还要自动重试、能查回执。
我们第一次做通知时,第一反应也是「POST 一下 webhook 不就行了」。真正动手才发现——「发出去」和「确保送达」,是两回事:企微和钉钉的 payload 格式不一样,混着用直接发不出去;webhook 静默失败没人知道,工单晾几个小时没人处理;网络抖动失败不重试就丢通知,4xx 的错重试一百遍也没用。
这不是个例。任何「系统要主动触达用户/内部协作」的场景都是这个模式:工单通知、告警推送、审批提醒、营销触达——渠道多、格式杂、发送还可能失败,通知能力必须是一个「带渠道校验/模板/重试/回执的完整能力」,而不是一段裸 http 调用。
2. 场景痛点
这个流程的痛点,在通知链路上体现得最直接:
- 渠道格式各不同:企业微信的 payload 和钉钉不一样,混着用直接发不出去——每接一个渠道就要写一套适配。
- 发送失败无感知:webhook 静默失败,客服没收到提醒,工单晾了几个小时没人处理,业务无感知。
- 重试无策略:临时网络抖动就失败,不重试会丢通知;4xx 类错误(URL 错/鉴权错)重试又毫无意义还拖时间。
- 渠道地址写死在代码:每个环境(测试/生产/客户现场)的 webhook 地址都不同,写死意味着每换一处就改代码。
本质上,通知的难点不在「发出一条消息」,而在「多发、可重试、可回执」——渠道要抽象、失败要降级、状态要可查。
3. 方案:为什么是通知工具插件
选通知工具插件,我们实际对比过:
- 渠道抽象:同一消息按渠道适配 payload(企业微信/钉钉/mock),新增渠道只加一个适配函数,不破坏现有;
- 重试策略分层:仅对可重试错误(超时/5xx)重试 3 次×2s,4xx 不重试直接失败——不浪费时间也不丢通知;
- 回执结构化:
{sent, message_id?, reason?}显式返回,下游 IF-ELSE 按回执分流,失败走降级分支,业务有感知。
这篇文章我们就用它把工单流程里的 http 通知节点升级为正式插件:notify工具(channel 枚举 wecom/dingtalk/mock + title/body/priority),跑通「渠道适配 → 重试 → 回执 → 降级」的完整通知链路。
4. 整体架构
链路很清晰:收通知参数 → 渠道校验 → 模板渲染 → payload 适配 → 带重试发送 → 回执分流。关键设计是「失败显式化」——回执里带 sent/reason,下游永远知道这次通知到底发出去没有。
5. 模块设计
5.1 渠道适配与重试(tools/notify.py)
同一消息按渠道输出不同 payload,重试只对可重试错误:
CHANNELS=("wecom","dingtalk","mock")RETRY_TIMES=3RETRY_INTERVAL=2TIMEOUT=5defadapt_payload(channel,title,body,priority):content=f"[{priority}]{title}\n{body}"ifpriorityandpriority!="normal"elsef"{title}\n{body}"ifchannel=="wecom":return{"msgtype":"text","text":{"content":content}}ifchannel=="dingtalk":return{"msgtype":"text","text":{"content":content}}return{"channel":channel,"title":title,"body":body,"priority":priority}def_send_with_retry(self,url,payload):last_reason=""forattemptinrange(RETRY_TIMES):try:resp=requests.post(url,json=payload,timeout=TIMEOUT)ifresp.status_code<400:return{"sent":True,"message_id":mid}# 成功回执if400<=resp.status_code<500:# 4xx 不重试:URL 错/鉴权错,重试无意义return{"sent":False,"reason":f"http{resp.status_code}(not retried)"}last_reason=f"http{resp.status_code}"exceptrequests.exceptions.Timeout:last_reason="timeout"exceptrequests.exceptions.RequestExceptionase:last_reason=f"network error:{type(e).__name__}"ifattempt<RETRY_TIMES-1:time.sleep(RETRY_INTERVAL)return{"sent":False,"reason":f"{last_reason}(retried{RETRY_TIMES}x)"}5.2 渠道 URL 走凭证(provider/notify_tool.yaml)
wecom_url/dingtalk_url/mock_url 三个 credential(环境差异),模板在代码(业务差异)——换环境只改凭证。注意 manifest tags:1.16 daemon 合法枚举没有communication,用utilities(详见采坑点)。
6. 运行验证
| 输入 | 预期 | 结果 |
|---|---|---|
| 三渠道各发一条(mock 端核对) | payload 与平台格式一致(wecom/dingtalk text 结构) | ✅ 一致 |
| priority=high / normal | high 拼[high]前缀,normal 不拼 | ✅ 一致 |
| mock 配置 ?fail=2 | 前两次 500 第三次成功,回执 sent=true | ✅ 实测耗时 4.0s(2 次间隔) |
| mock 持续 500 | 重试耗尽 sent=false + reason | ✅ 4.0s;4xx 不重试 0.0s 直接失败 |
| 非法 channel / 缺 title | 参数错误 param_invalid | ✅ 一致 |
| workflow 注入 fail=99 | 回执 contains"sent": false→ 降级分支 | ✅ 双出口正确 |
环境:Dify 1.16.1(Docker Compose),mock webhook 服务 tmp/mock_webhook.py(127.0.0.1:8004,?fail=N 模拟失败 / ?delay=秒 模拟慢响应 / GET /received 查看),凭证指向 host.docker.internal:8004。
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| plugin_tag 枚举 | manifest tags 写 communication → 打包报错 plugin_tag 校验失败 | 用合法枚举 utilities(1.16 合法集:search/image/videos/weather/finance/design/travel/social/news/medical/productivity/education/business/entertainment/utilities/other/agent/rag/trigger) |
| 4xx 也重试 | URL 错/凭证错重试无意义还拖时间 | 4xx 不重试直接失败,仅 5xx/超时/网络重试 3 次×2s |
| 降级静默 | 发送失败不告知下游,业务无感知 | 回执 sent=false 显式返回,IF-ELSE contains"sent": false分流降级分支 |
| 渠道 payload 差异 | 企微与钉钉格式混用发不出去 | 每渠道 adapt_payload 函数,新增渠道加函数不破坏现有 |
| webhook URL 写死 | 换环境就要改代码 | 渠道 URL 在 credentials(环境差异),模板在代码(业务差异) |
| 通知超时拖慢主流程 | webhook 慢响应卡住整个流程 | requests timeout=5,超时归重试路径 |
| mock 未知渠道不报错 | 测不出 4xx 不重试路径 | mock 对白名单外渠道返回 404(模拟真实 URL 错) |
8. 实验文档及源码获取
- 实验文档:DIFY-106-06:通知渠道插件.md
- 验证应用 DSL:dify106_06_验证应用.yml
- 插件安装包:dify106_06_notify_tool.signed.difypkg
- 源码目录:dify-106/dsl | dify-106/plugins
文章聚焦核心配置与采坑点,完整分步操作与渠道 payload 对照表见实验文档原文。
下一篇:Dify 插件开发实验(07):私有模型网关接入——如何让 Dify 用上私有模型网关?
💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。