当一个平台需要接入搜索、邮件、模型、数据源等外部能力时,最直接的做法是把代码写进主项目。但随着集成数量增加,核心代码会越来越臃肿,第三方依赖和发布节奏也会互相影响。

Dify 选择把扩展能力做成插件,并用独立的 Plugin Daemon 管理插件生命周期。这个设计不只解决“如何增加一个工具”,还要解决安装、依赖、隔离、通信、升级和故障恢复等工程问题。

本文重点介绍插件系统背后的通用设计,而不是某个插件的开发教程。


一、为什么需要插件系统

1.1 直接集成的问题

假设所有模型和工具都直接放进 Dify API:

  • 接入一个服务就要修改、测试并发布整个主项目;
  • 不同插件依赖的 Python 包可能发生版本冲突;
  • 一个插件崩溃或内存泄漏可能拖垮主进程;
  • 平台维护者必须审核和维护所有第三方代码;
  • 用户无法独立安装、升级或卸载扩展;
  • 私有插件难以与社区版本共存。

插件系统的本质是建立一条稳定边界:核心平台定义协议,插件实现能力,运行时负责把二者连接起来。

1.2 Dify 支持哪些扩展点

当前 Dify 插件体系覆盖多类能力:

类型 作用 例子
Tool 为工作流和 Agent 提供动作 搜索、发邮件、生成图片
Model 接入模型供应商 LLM、Embedding、Rerank、TTS
Agent Strategy 替换 Agent 推理循环 Function Calling、ReAct 变体
Datasource 向知识库导入外部文档 网盘、在线文档、网站抓取
Trigger 由外部事件启动工作流 Webhook、文件到达、定时事件
Endpoint 向外暴露插件 HTTP 入口 接收第三方回调

它们使用同一套安装和运行基础设施,但业务协议不同。好的插件系统应该复用生命周期管理,同时保留各扩展点的类型约束。


二、Dify 插件系统的总体架构

2.1 为什么增加 Plugin Daemon

Dify API 负责租户、应用和工作流等核心业务;Plugin Daemon 是独立的 Go 服务,负责插件包、运行实例和调用会话。

flowchart LR
    User["用户 / 工作流 / Agent"] --> API["Dify API
业务与权限"] API -->|"HTTP / 流式调用"| Daemon["Plugin Daemon
生命周期与会话"] Daemon --> Manager["Plugin Manager"] Manager --> Local["Local Runtime
本地子进程"] Manager --> Debug["Debug Runtime
开发机连接"] Manager --> Serverless["Serverless Runtime
远程执行环境"]

拆分后的职责更清晰:

  • Dify API 判断“哪个租户可以调用哪个插件”;
  • Plugin Daemon 判断“插件安装在哪里、如何启动和调用”;
  • Plugin SDK 规定插件如何声明能力、接收请求和返回结果;
  • 具体运行时决定使用子进程、TCP 连接还是远程 HTTP。

2.2 三类运行时

运行时 通信方式 适用场景 主要取舍
Local 子进程 STDIN/STDOUT 自托管、普通生产部署 简单高效,但共享宿主机资源
Debug 插件主动建立 TCP 连接 本地开发和断点调试 反馈快,不适合生产
Serverless 通过标准 HTTP 接口调用 弹性和更强隔离 部署复杂,有冷启动和网络成本

三种运行时对 Dify API 暴露相同能力。业务层不应关心插件究竟运行在本机还是远端,这就是运行时抽象的价值。


三、插件从安装到调用

3.1 Manifest:插件的身份证和契约

每个插件都以 manifest.yaml 描述自身,而不是让平台运行代码后再猜测能力。典型信息包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: 0.0.1
type: plugin
author: example
name: web_search
label:
en_US: Web Search
zh_Hans: 网页搜索
created_at: 2026-08-30T11:00:00+08:00

plugins:
tools:
- provider/search.yaml

meta:
version: 0.0.1
arch:
- amd64
- arm64
runner:
language: python
version: "3.12"
entrypoint: main

Manifest 解决四个问题:

  1. 发现:平台无需执行代码就知道插件提供什么;
  2. 校验:安装前检查版本、架构、入口和文件是否完整;
  3. 展示:名称、图标、多语言说明可直接用于界面;
  4. 兼容:平台根据最低版本和协议版本判断能否安装。

声明式元数据比在代码中硬编码注册逻辑更容易检查、缓存和迁移。

3.2 安装流程

一个插件包进入系统后,通常经历以下阶段:

flowchart LR
    Package[".difypkg"] --> Verify["校验包与 Manifest"]
    Verify --> Store["保存包和元数据"]
    Store --> Resolve["创建 Python 环境
安装依赖"] Resolve --> Register["注册到租户"] Register --> Start["按需启动实例"] Start --> Ready["可调用"]

安装不是简单解压:

  • 压缩包需要防止路径穿越和超大文件;
  • 插件标识、版本和校验值需要唯一;
  • 依赖安装要有超时、缓存和失败清理;
  • 插件包与租户安装记录应分开管理;
  • 重启后要能恢复已安装插件状态;
  • 升级失败不能破坏仍在工作的旧版本。

3.3 调用流程

以工作流调用工具插件为例:

  1. 工作流节点解析插件、工具名和参数;
  2. Dify API 校验租户是否已安装插件,并读取凭证;
  3. API 向 Plugin Daemon 创建调用请求;
  4. Daemon 建立会话并定位运行实例;
  5. Runtime 把请求送入插件进程;
  6. 插件通过 SDK 执行业务逻辑,流式返回文本、文件或日志;
  7. Daemon 将事件传回 API;
  8. 工作流把最终结果写入节点输出。
1
2
Workflow → Dify API → Plugin Daemon → Runtime → Plugin
Workflow ← Dify API ← Plugin Daemon ← Runtime ← Plugin

调用链虽然更长,却换来了统一的权限、超时、日志和运行时切换能力。

3.4 为什么使用流式协议

模型生成、文件处理和 Agent 推理都可能耗时较长。如果只等待一个最终 JSON:

  • 中间进度不可见;
  • 大结果需要一次性放入内存;
  • 用户无法及时取消;
  • 日志和业务结果难以区分。

因此 Plugin Daemon 使用流式会话传递不同事件。一个插件可以先返回日志,再返回文本片段,最后返回结束事件。调用方可以实时展示,也能在连接中断时尽早释放资源。


四、隔离、安全与可靠性

4.1 插件进程不等于安全沙箱

Local Runtime 把插件放到独立进程和 Python 环境中,可以隔离依赖与普通崩溃,但这不等于强安全隔离。插件仍可能访问其操作系统账号可见的文件和网络。

生产环境还需要:

  • 只安装可信或经过审核的插件;
  • Plugin Daemon 使用非 root 用户运行;
  • 限制工作目录和文件权限;
  • 限制 CPU、内存、进程数和执行时间;
  • 出站网络经过 SSRF 代理或 Egress Policy;
  • Daemon 与 Dify API 使用强随机内部密钥;
  • 不把数据库、对象存储等高权限凭证注入插件进程。

如果需要执行不可信插件,应进一步使用容器、gVisor、Kata 或 Serverless Runtime 隔离。

4.2 凭证应该属于谁

插件经常需要第三方 API Key。正确边界是:

  • 凭证按租户或工作空间保存;
  • 数据库存储密文,日志和错误信息中脱敏;
  • 只在实际调用时注入对应插件;
  • 插件 A 无法读取插件 B 的凭证;
  • 用户导出 DSL 时不默认导出明文密钥;
  • 删除租户或卸载插件时清理关联凭证。

插件包是共享的软件,租户凭证是私有的数据,两者不能混为一体。

4.3 故障与升级

问题 处理策略
插件进程崩溃 标记实例失效,按策略重启,不拖垮 API
调用超时 取消会话并回收资源,向工作流返回明确错误
Daemon 重启 从持久化记录恢复插件和租户安装关系
新版本安装失败 保留旧版本,不做破坏性覆盖
协议不兼容 安装前检查 Dify、SDK 和 Manifest 版本
流式连接中断 关闭上下游会话,避免孤儿任务

升级插件时尤其要区分“上传新包”“切换租户使用版本”和“回收旧实例”,否则正在执行的工作流可能被中途打断。


五、通用设计启示

从 Dify 插件系统可以总结出一套可复用的扩展架构:

  1. 核心系统只依赖稳定协议,不依赖具体插件实现;
  2. 用 Manifest 在运行前声明能力、入口和兼容信息;
  3. 用独立 Daemon 收敛安装、实例、会话和运行时差异;
  4. 插件包、租户安装关系和租户凭证分别管理;
  5. 长任务使用流式事件,而不是等待单个大响应;
  6. 进程隔离主要解决依赖和故障,不应被误认为安全沙箱;
  7. 版本升级必须可回滚,并保护正在运行的调用;
  8. 所有内部通信都需要认证、超时、大小限制和审计。

插件系统真正困难的地方不是“动态导入一段代码”,而是让第三方能力在长期运行、频繁升级和多租户环境中仍然可控。