Dify 插件开发实验(10):自定义节点扩展——不改平台代码,插件如何补节点能力?

Dify 实验系列 · 插件开发 10/12 | 实验编号:DIFY-106-10
基于 Dify 1.16.1 实测(2026-08)

1. 业务场景

先讲一个我们实际遇到的场景。

客服工单 SaaS 的工单编号有一套业务规则:DTF-YYYYMMDD-序号(序号每日从 001 递增,跨天归零)。比如今天第一张工单是 DTF-20260805-001,第二张是 DTF-20260805-002,明天又从 DTF-20260806-001 开始。这个编号是工单的对外门面——客户报修、财务对账、跨系统引用都靠它。

我们第一次接这类需求时,第一反应也是「编号嘛,代码节点里写个计数器不就行了」。真正动手才发现——代码节点每次运行相互独立,根本记不住「今天发到第几个号了」;让 LLM 生成编号,既可能重复也可能跑偏。我们最初想做「自定义工作流节点」插件,实测 1.16.1 daemon 根本没有 extensions 节点类型——平台不支持,只能找等效路径。

这不是个例。任何业务系统都有自己的编号规则:订单号、发票号、流水号、审批单号——「前缀 + 日期 + 当日自增序号」是最常见的形态,而「跨运行有状态」恰恰是普通工作流节点最难满足的需求。

2. 场景痛点

这个流程的痛点,在编号生成时体现得最直接:

  • 代码节点无状态:工作流代码节点每次运行相互独立,记不住「今天已经发到第几个号了」,序号永远从 001 开始。
  • LLM 生成不可控:让模型生成编号,既可能编出重复号,也可能格式跑偏——编号这种确定性数据,不该交给概率。
  • 并发重复:多个客服同时建单,各自都拿到 001,工单号撞车,对账直接乱套。
  • 跨天不归零:用固定键存计数,第二天忘了重置,序号一路涨下去,规则就废了。

本质上,编号生成是「有状态 + 确定性」的需求——现有节点凑不出,需要的是一个「拖入即用、跨运行有状态」的能力。

3. 方案:为什么先探边界再选等效路径

选这个方案,我们实际对比过:

  • 先探平台边界:目标本是「自定义工作流节点」(extension 类型),但实测 1.16.1 daemon 的 PluginExtensions 只有 tools / models / endpoints / agent_strategies / datasources / triggers 六类,无 extensions 节点类型——平台当前不支持自定义节点插件(非版本落后,main 分支源码同样没有);
  • 工具插件等效替代generate_ticket_no 工具 + KV 序列存储,工作流工具节点调用——「拖入即用 + 跨运行有状态」两个诉求都能满足,功能与自定义节点等价;
  • 日期键天然跨天归零:KV 键按日期设计(ticket_seq_YYYYMMDD),换键即重计,不用显式判断日期变化。

这篇文章我们就用它搭一个「工单编号生成器」:工具插件 dify106_10_ticket_no_tool(generate_ticket_no + KV 序列),跑通「当日自增、跨天归零、跨运行有状态」的完整链路,并实测平台边界给出结论。

4. 整体架构

【等效方案(实测)】

开始(date/prefix 可选)

工具节点 generate_ticket_no

输出 ticket_no

end

工具内部:读 KV ticket_seq_YYYYMMDD → seq+1 → 写回 → 返回 {ticket_no, date, sequence}

【预期方案(平台不支持)】

工作流画布

自定义节点「工单编号生成器」(extension 类型)

出现在节点面板

链路很清晰:收日期/前缀 → 读当日 KV 计数 → 自增 → 写回 → 返回编号。关键设计是「日期键」——KV 键里带日期,换键即重计,跨天归零是天然行为,不是额外逻辑。

5. 模块设计

5.1 工具参数声明(tools/generate_ticket_no.yaml)

date 与 prefix 都是可选参数,默认值在实现里兜底:

parameters:
  - name: date
    type: string
    required: false
    form: llm
    llm_description: 'Date in YYYYMMDD format, e.g. 20260805, default today'
  - name: prefix
    type: string
    required: false
    form: llm
    llm_description: 'Ticket number prefix, 1-8 chars, default DTF'

5.2 日期键 + 参数校验(tools/generate_ticket_no.py)

日期键换键即重计,天然支持跨天归零:

DATE_PATTERN = re.compile(r"^\d{8}$")
PREFIX_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,8}$")

date_str = (tool_parameters.get("date") or "").strip() or datetime.now(timezone.utc).strftime("%Y%m%d")
prefix = (tool_parameters.get("prefix") or "").strip() or "DTF"
if not DATE_PATTERN.match(date_str):
    yield self.create_text_message(err("param_invalid", "date must be YYYYMMDD (e.g. 20260805)"))
    return

key = "ticket_seq_" + date_str          # 日期键:换键即归零,天然跨天
seq = read_kv_seq(kv_url, key)          # 读当日计数(KV 持久化,复用 106-05 模式)
seq += 1
write_kv_seq(kv_url, key, seq, date_str)  # 先查后写(与 106-05 同款竞态语义)
yield self.create_text_message(json.dumps(
    {"ticket_no": f"{prefix}-{date_str}-{seq:03d}", "date": date_str, "sequence": seq},
    ensure_ascii=False))

6. 运行验证

验证项 输入/场景 预期 结果
注册 安装工具插件 工具目录出现 generate_ticket_no
基本 默认参数运行 输出 DTF-20260810-001
序列递增 连续运行 3 次 001/002/003
跨天归零 date=20260806 换键重计,序号 001
并发 5 线程同日期 序号不重复 ⚠️ 全 seq 1(竞态窗口实测,预期内;生产 Redis INCR)
对照 代码节点 vs 工具插件 6 维度 差异表

与代码节点对照结论(6 维度差异表):跨运行状态——代码节点无(每次独立),工具插件 KV 持久化 ✅;复用性——代码节点每工作流复制粘贴,工具插件一次安装全局可用 ✅;可配置性——代码节点改代码,工具插件参数+凭证配置 ✅;维护性——代码节点多处副本难同步,工具插件单点更新 ✅;开发成本——代码节点低(但做不了有状态),工具插件中(插件流程已熟);可测试性——代码节点无独立测试,工具插件本地单测+冒烟 ✅。有状态需求(编号序列/计数/审批状态)是代码节点盲区——工具插件(KV 持久化)是官方支持路径下的等效解法;真·自定义节点(面板新节点类型)需等 Dify 支持节点扩展插件。

7. 实战坑

现象 修复
节点扩展不支持(核心) 1.16.1 daemon PluginExtensions 无 extensions 类型(仅六类插件);main 分支源码同样无 工具插件等效替代(generate_ticket_no + KV 序列),等 Dify 升级支持节点扩展再实测
节点接口不熟 sdk 0.7.4 workflow_node 仅支持内置节点运行时(parameter_extractor/question_classifier) 不写自定义节点;按工具插件标准结构开发,工作流用工具节点调用
本地持久化失败 节点/工具内写本地文件被沙箱拦截(104-01 实测 code 沙箱禁写) 序列计数走 KV 外部存储(复用 106-05 模式)
并发重复序号 先查后写竞态,5 线程并发全 seq 1 生产用 Redis INCR 原子自增替代
跨天不归零 若用固定键,需要显式判断日期变化才能归零 日期键设计 ticket_seq_YYYYMMDD,换键即重计,天然跨天归零

8. 实验文档及源码获取

文章聚焦核心配置与采坑点,完整分步操作与代码节点对照实验记录见实验文档原文。

下一篇:Dify 插件开发实验(11):打包分发与离线安装——插件如何打包签名、分发与离线安装?

💬 你在这个实验的场景里踩过什么坑?欢迎评论区分享你的实战经验。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐