Dify 插件机制详解

本文基于 D:/opencode/q/dify 仓库源码(main 分支)梳理。所有文件路径均为相对仓库根的路径。

0. 一句话总览

Dify 把"插件"当作外部进程 来跑:主 API 进程不直接 import 插件代码,而是通过 HTTP 与一个独立的 plugin-daemon 服务通信;plugin-daemon 负责加载、隔离、调度具体插件实例。两侧的调用是双向 的------主 API 通过 dispatch/* 调插件,插件通过 inner_api/invoke/* 反向调主 API(用 LLM、调工具、跑 App、加解密凭证等)。

整体架构:

bash 复制代码
┌──────────────────────────────────────┐         ┌──────────────────────────┐
│  Dify Main API (Flask)               │         │  Plugin Daemon            │
│                                      │  HTTP   │  (Go 独立进程,            │
│  controllers/console/workspace/      │ ──────► │   外部仓库                 │
│      plugin.py    (前端管理 UI)       │ dispatch│   dify-plugin-daemon)    │
│                                      │         │                          │
│  core/plugin/impl/*.py               │         │   ┌──────────────────┐  │
│      (调用插件: tool/model/endpoint) │         │   │ Plugin A (Python)│  │
│                                      │         │   │ Plugin B (Python)│  │
│  controllers/inner_api/plugin/       │ ◄────── │   │ ...               │  │
│      plugin.py    (反向调用入口)      │ invoke  │   └──────────────────┘  │
│                                      │         │                          │
│  core/plugin/backwards_invocation/   │         └──────────────────────────┘
│      (反向调用业务逻辑)               │
└──────────────────────────────────────┘

1. 核心目录与职责

路径 作用
api/core/plugin/entities/ 数据模型(manifest、daemon 协议、reverse-invoke 请求)
api/core/plugin/impl/ 主进程 → daemon 的 HTTP 客户端实现(base.py, tool.py, model.py, endpoint.py, plugin.py 等)
api/core/plugin/backwards_invocation/ daemon → 主进程的反向调用逻辑(tool.py, model.py, app.py, node.py, encrypt.py
api/core/plugin/utils/ 流式分片合并等工具(chunk_merger.py
api/services/plugin/ 业务服务层(plugin_service.py、权限、OAuth、endpoint 服务)
api/controllers/console/workspace/plugin.py Console 前端管理 API(列表、安装、卸载、升级等)
api/controllers/inner_api/plugin/plugin.py 反向调用入口(仅供 plugin-daemon 调用)
api/configs/feature/init.py PLUGIN_DAEMON_* 等配置项

2. 插件类型(Category)

参见 PluginCategory

python 复制代码
class PluginCategory(StrEnum):
    Tool = auto()              # 工具插件:被工作流/Agent 调用执行某种操作
    Model = auto()             # 模型提供商:LLM/Embedding/Rerank/TTS/STT/Moderation
    Extension = auto()         # 通用扩展(默认兜底)
    AgentStrategy = "agent-strategy"   # Agent 策略(ReAct、Function Calling 等的实现)
    Datasource = "datasource"          # 数据源
    Trigger = "trigger"                # 触发器(事件入口)

每个插件包(.dify_pkg)只声明一种主能力(tool/model/endpoint/agent_strategy/datasource/trigger 字段),但同一个插件可以同时暴露 endpoint (向外提供 HTTP 接口)。category 是从这些字段自动推断 的,见 validate_category()

3. Manifest 声明

PluginDeclaration 是 manifest.yaml 反序列化后的根模型:

python 复制代码
class PluginDeclaration(BaseModel):
    version: str                                # SemVer
    author: str                                 # ^[a-zA-Z0-9_-]{1,64}$
    name: str                                   # ^[a-z0-9_-]{1,128}$
    description: I18nObject                     # 多语言
    icon: str
    label: I18nObject
    category: PluginCategory                    # 自动推断
    created_at: datetime
    resource: PluginResourceRequirements        # 资源/权限声明
    plugins: Plugins                            # 内含的 provider yaml 列表
    meta: Meta                                  # minimum_dify_version 等
    # 主能力(互斥):
    tool: ToolProviderEntity | None
    model: ProviderEntity | None
    endpoint: EndpointProviderDeclaration | None
    agent_strategy: AgentStrategyProviderEntity | None
    datasource: DatasourceProviderEntity | None
    trigger: TriggerProviderEntity | None

3.1 资源与权限声明

PluginResourceRequirements 是插件能"做什么"的清单------daemon 在运行期会按这个表强制限制:

python 复制代码
class Permission(BaseModel):
    class Tool(BaseModel):     enabled: bool
    class Model(BaseModel):
        enabled: bool
        llm: bool; text_embedding: bool; rerank: bool
        tts: bool; speech2text: bool; moderation: bool
    class Node(BaseModel):     enabled: bool          # 调用工作流节点(参数提取/问题分类)
    class Endpoint(BaseModel): enabled: bool          # 注册 HTTP 端点
    class Storage(BaseModel):
        enabled: bool
        size: int = Field(ge=1024, le=1073741824, default=1048576)  # 1KB~1GB

如果插件想反向调用 LLM,必须在 manifest 里声明 permission.model.llm: true,否则反向调用会被拒。

4. Provider 三段式 ID

GenericProviderID 把所有插件资源用 org/plugin_name/provider_name 三段式标识:

  • 完整 ID:langgenius/web_reader/web_reader
  • plugin_idlanggenius/web_reader(用作 HTTP header X-Plugin-ID
  • provider_nameweb_reader(daemon 内部分发给具体 provider 实现)

子类型见 ModelProviderID / ToolProviderID / DatasourceProviderID / TriggerProviderID

5. 安装与生命周期

5.1 四种安装来源

PluginInstallationSource

来源 说明
Marketplace 官方商店一键安装
Github 给定 repo + version + package 从 GitHub release 下载
Package 上传本地 .dify_pkg 文件
Remote 远程调试模式(下面 §10 单独讲)

5.2 前端入口(Console API)

集中在 api/controllers/console/workspace/plugin.py

路由 控制器 行为
POST /workspaces/current/plugin/upload/pkg PluginUploadFromPkgApi 上传 .dify_pkg 验签解码,不立即安装 ,返回 unique_identifier
POST /workspaces/current/plugin/install/pkg 类似 用上一步的 identifier 触发安装任务
POST /workspaces/current/plugin/install/github PluginInstallGithubApi repo+version+package 触发
POST /workspaces/current/plugin/install/marketplace PluginInstallMarketplaceApi identifiers 触发
GET /workspaces/current/plugin/tasks/<task_id> 查询任务进度
POST /workspaces/current/plugin/uninstall 卸载
POST /workspaces/current/plugin/upgrade/* 升级

所有动作最终都委托给 PluginInstaller,它把请求翻译成 daemon 的 plugin/{tenant_id}/management/install/* HTTP 调用。

5.3 安装是异步的

PluginInstallTask 跟踪每个安装请求:

python 复制代码
class PluginInstallTask(BasePluginEntity):
    status: PluginInstallTaskStatus  # pending / running / success / failed
    total_plugins: int
    completed_plugins: int
    plugins: list[PluginInstallTaskPluginStatus]

前端在安装后轮询 tasks/{task_id} 直到 success/failed。

5.4 三层权限控制

系统级 ------ 限制可安装的插件来源(plugin_service.pyPluginVerification):

  • OFFICIAL_ONLY -- 仅 langgenius 官方
  • OFFICIAL_AND_SPECIFIC_PARTNERS -- 官方 + 白名单 partner
  • ALL -- 任意

租户级 ------ TenantPluginPermission

python 复制代码
class InstallPermission: EVERYONE / ADMINS / NOBODY
class DebugPermission:   EVERYONE / ADMINS / NOBODY  # 用于 Remote 调试

插件级 ------ manifest 中的 resource.permission,运行期由 daemon 与反向调用接口共同强制。

5.5 自动升级策略

TenantPluginAutoUpgradeStrategy 支持 disabled / fix_only / latest × all / partial / exclude,并指定 upgrade_time_of_day,由后台计划任务(见 api/schedule/)触发。

6. 主 → 插件:dispatch 调用

6.1 统一 HTTP 客户端

所有调用都走 BasePluginClient。三个关键约定:

(a) 鉴权头 ------ _prepare_request()

python 复制代码
prepared_headers["X-Api-Key"] = dify_config.PLUGIN_DAEMON_KEY  # 共享密钥
prepared_headers.setdefault("Accept-Encoding", "gzip, deflate, br")
self._inject_trace_headers(prepared_headers)                   # W3C traceparent

(b) URL 模板 ------ {PLUGIN_DAEMON_URL}/plugin/{tenant_id}/dispatch/{type}/{action},租户隔离体现在 URL path 里。

(c) 流式响应 ------ _stream_request() 用 SSE-like 协议(data: {json}\n),逐行 decode 后 yield 给上层 pydantic 模型反序列化。

6.2 Tool 调用

PluginToolManager.invoke()

css 复制代码
POST /plugin/{tenant_id}/dispatch/tool/invoke
Headers: X-Plugin-ID: {org}/{plugin_name}
Body: {
  "user_id": ..., "conversation_id": ..., "app_id": ..., "message_id": ...,
  "data": {
    "provider": "...", "tool": "...",
    "credentials": {...}, "credential_type": "api_key|oauth2|...",
    "tool_parameters": {...}
  }
}
Response: 流式 ToolInvokeMessage(...)

返回流经过 merge_blob_chunks() 合并 BLOB 分片(详见 §8)。

6.3 Model 调用

PluginModelClient 暴露 invoke_llm / invoke_text_embedding / invoke_rerank / invoke_tts / invoke_speech2text / invoke_moderation。例如 LLM:

vbnet 复制代码
POST /plugin/{tenant_id}/dispatch/model/invoke/llm
Headers: X-Plugin-ID: {plugin_id}
Body: {
  "user_id": ...,
  "data": {
    "provider": ..., "model": ...,
    "prompt_messages": [...], "completion_params": {...},
    "stream": true
  }
}
Response: 流式 LLMResultChunk

6.4 Endpoint 管理

PluginEndpointManager 把租户的"端点"配置(启用/禁用、绑定的 provider、settings)下发给 daemon:

路径 作用
POST plugin/{tenant_id}/endpoint/setup 创建
GET plugin/{tenant_id}/endpoint/list 列表
POST plugin/{tenant_id}/endpoint/update 更新
POST plugin/{tenant_id}/endpoint/remove 删除
POST plugin/{tenant_id}/endpoint/enable / disable 状态切换

7. 插件 → 主:反向调用 (Backwards Invocation)

这是插件机制最容易被忽略但最强大的部分。插件代码在 daemon 沙箱里跑,没有 DB、没有 Redis、没有 LLM provider,所有需要主服务能力的事都要通过反向调用 HTTP 拿回来

7.1 入口与认证

集中在 api/controllers/inner_api/plugin/plugin.py。每个路由用三层装饰器(wraps.py):

python 复制代码
@plugin_inner_api_only          # 校验 INNER_API_KEY_FOR_PLUGIN(daemon 共享密钥)
@get_user_tenant                 # 解析 body 里的 tenant_id/user_id,加载 Tenant + EndUser/Account
@plugin_data(payload_type=...)   # pydantic 校验业务 payload
def post(self, user_model, tenant_model, payload):
    ...

请求体的固定外壳:

json 复制代码
{
  "tenant_id": "...",
  "user_id":   "...",
  "data":      { ... 具体 payload ... }
}

租户隔离tenant_id 是装饰器强制读取的,业务代码拿到的 tenant_model.id 只能是这一个,无法跨租户访问。

7.2 LLM 反向调用

PluginInvokeLLMApiPluginModelBackwardsInvocation.invoke_llm()

python 复制代码
def invoke_llm(cls, user_id, tenant, payload):
    # 1. 取该租户绑定的真实模型实例(走主服务的 ModelManager,带配额/Provider 凭证)
    model_instance = cls._get_bound_model_instance(
        tenant_id=tenant.id, user_id=user_id,
        provider=payload.provider,
        model_type=payload.model_type, model=payload.model,
    )
    # 2. 调真实 LLM
    response = model_instance.invoke_llm(prompt_messages, completion_params, tools, stop, stream=True)
    # 3. 流式扣费
    if isinstance(response, Generator):
        def handle():
            for chunk in response:
                if chunk.delta.usage:
                    deduct_llm_quota(tenant.id, model_instance, chunk.delta.usage)
                chunk.prompt_messages = []   # 不回传给插件,省带宽
                yield chunk
        return handle()

关键点:

  1. 插件用的是主租户的模型配置和配额------它无需自带 OpenAI key,就能用工作区已经配好的 GPT-4。
  2. Token 用量也算到该租户头上 ------deduct_llm_quota
  3. 完整路由列表(行号见 plugin.py):
    • /invoke/llm/invoke/llm/structured-output
    • /invoke/text-embedding/invoke/rerank
    • /invoke/tts/invoke/speech2text/invoke/moderation
    • /invoke/summary/invoke/parameter-extractor/invoke/question-classifier
    • /invoke/tool/invoke/app
    • /invoke/encrypt/invoke/fetch-app/invoke/upload-file/invoke/system-tool

7.3 Tool 反向调用

PluginToolBackwardsInvocation.invoke_tool():插件可以反过来调用 dify 注册的任意工具 (包括内置 tool、其他插件的 tool、Workflow as Tool)。底层走 ToolManager.get_tool_runtime_from_plugin() + ToolEngine.generic_invoke(),并把结果用 ToolFileMessageTransformer 处理后流式回传。

workflow_call_depth=1 用来防止"插件 → 调 workflow as tool → 又触发同一个插件"的递归。

7.4 App 反向调用

PluginInvokeAppApiPluginAppBackwardsInvocation.invoke_app()。支持的 App 模式(行 75-86):ADVANCED_CHAT / AGENT_CHAT / CHAT / WORKFLOW / COMPLETION。这意味着一个插件可以整段地把 Dify 上别的应用当函数调

7.5 工作流节点反向调用

backwards_invocation/node.py 暴露两个工作流节点能力:

  • Parameter Extractor(行 17-65):从自然语言提取结构化参数
  • Question Classifier(行 68-114):把问题分类到给定 label

插件不必自己实现这些 prompt,直接复用 dify 里成熟的节点逻辑。

7.6 加密服务

PluginEncrypter.invoke_encrypt()encrypt / decrypt / clear 三种操作,用主服务的密钥环对插件凭证字段做对称加解密。插件本身永远不持有密钥------它只能拿密文,到调用时让主服务现解。

7.7 流式响应封装

反向调用大多是流式 generator,统一用 length_prefixed_response(0xF, generator())(见 length_prefixed_response 用法),一种"长度前缀"分帧协议------比纯 SSE 更鲁棒,daemon 端解析也更简单。

8. BLOB 分片合并

插件可能产出大文件(图片/音频/PDF),但单次 HTTP chunk 不能太大。约定是插件把文件切成 ≤8KB 的 BLOB_CHUNK 消息流式吐回,主 API 再合并。

merge_blob_chunks() 关键逻辑:

python 复制代码
def merge_blob_chunks(response, max_file_size=30MB, max_chunk_size=8KB):
    files: dict[str, FileChunk] = {}
    for resp in response:
        if resp.type == BLOB_CHUNK:
            chunk_id = resp.message.id
            files.setdefault(chunk_id, FileChunk(resp.message.total_length))

            if files[chunk_id].bytes_written + len(blob) > max_file_size: raise ...
            if len(blob) > max_chunk_size: raise ...

            files[chunk_id].data[offset:offset+len(blob)] = blob
            files[chunk_id].bytes_written += len(blob)

            if resp.message.end:                            # 最后一片
                yield ToolInvokeMessage(type=BLOB, ...)     # 合成完整 BLOB 抛给上层
                del files[chunk_id]
        else:
            yield resp                                       # 非 BLOB 透传

限制可调,见配置项 PLUGIN_MAX_FILE_SIZE

9. 配置项

api/configs/feature/init.pyPLUGIN_* 一组:

变量 默认 含义
PLUGIN_DAEMON_URL http://localhost:5002 daemon 地址
PLUGIN_DAEMON_KEY plugin-api-key 主 → daemon 共享密钥
PLUGIN_DAEMON_TIMEOUT 600 HTTP 超时(秒)
INNER_API_KEY_FOR_PLUGIN --- daemon → 主 共享密钥(反向调用)
PLUGIN_REMOTE_INSTALL_HOST localhost 远程调试 host
PLUGIN_REMOTE_INSTALL_PORT 5003 远程调试 port
PLUGIN_MAX_PACKAGE_SIZE 15 MB 单包大小上限
PLUGIN_MAX_BUNDLE_SIZE 180 MB bundle 大小上限
PLUGIN_MODEL_SCHEMA_CACHE_TTL 3600 模型 schema 缓存秒数
PLUGIN_MAX_FILE_SIZE 30 MB 反向产物文件大小上限

10. 远程调试模式(Remote)

PluginInstallationSource.Remote + PLUGIN_REMOTE_INSTALL_HOST/PORT 提供了一种"插件代码在你本地跑,挂到远程 dify 实例"的开发模式:

  1. 开发者用 dify-plugin CLI 在本地以 debug 模式启动插件,连到 daemon 的 5003 端口;
  2. daemon 把这个 socket 当成一个普通插件实例注册进来;
  3. 该插件只对开启 debug 的租户可见(受 TenantPluginPermission.DebugPermission 控制)。

这种模式下不需要打包上传,改一行代码立刻生效------插件作者最常用的工作流。

11. Plugin-Daemon 本身

dify 主仓库 不包含 daemon 源码 。daemon 是独立 Go 项目:langgenius/dify-plugin-daemon。它的职责(从主 API 这边的接口反推):

  • 接收 .dify_pkg、解压、校验签名、放入插件仓库
  • 用子进程/容器隔离每个插件实例(Python/Node 运行时)
  • 对外暴露 plugin/{tenant_id}/dispatch/* 路由,把请求路由到具体插件实例
  • 对外暴露 plugin/{tenant_id}/management/* 路由,处理安装/卸载/升级
  • 当插件需要 LLM/Tool/App 时,反向 HTTP 回调主 API inner_api/invoke/*
  • 强制 manifest 中 resource.permission 的限制
  • 端点路由:根据 endpoint 配置把外网 HTTP 流量打进对应的插件实例

12. 端到端数据流示例

12.1 工作流调用插件 Tool

scss 复制代码
用户工作流执行 → Tool Node
   │
   ▼
ToolEngine.generic_invoke(tool_runtime, params)        api/core/tools/tool_engine.py
   │
   ▼  (插件类型 tool 走这里)
PluginToolManager.invoke(tenant_id, user_id, ...)      api/core/plugin/impl/tool.py
   │  HTTP POST plugin/{tid}/dispatch/tool/invoke
   ▼
plugin-daemon 路由到具体插件 provider/tool
   │  执行插件 _invoke()
   ▼  流式 yield ToolInvokeMessage
主 API 收到 SSE 流
   │  merge_blob_chunks() 合并 BLOB
   ▼
回到 Tool Node 的输出

12.2 插件回调主服务的 LLM

css 复制代码
插件代码:  self.session.model.llm.invoke(...)
   │  daemon SDK 序列化 → HTTP POST
   ▼
主 API: POST /inner_api/invoke/llm
   │  @plugin_inner_api_only(校验 INNER_API_KEY_FOR_PLUGIN)
   │  @get_user_tenant(校验 tenant_id 隔离)
   ▼
PluginInvokeLLMApi.post()
   │
   ▼
PluginModelBackwardsInvocation.invoke_llm()
   │  ModelManager 取出该租户 provider 实例
   ▼
真实 LLM provider (OpenAI/Anthropic/...)
   │  流式 LLMResultChunk
   ▼  每 chunk: deduct_llm_quota()
length_prefixed_response 帧编码
   ▼
回到 daemon → 回到插件代码 (yield 出每个 chunk)

13. 关键设计取舍

  1. 进程隔离 > 性能:HTTP 而非 in-process 调用,单次请求多了一跳网络开销;换来的是插件崩溃/内存泄漏不会拖垮主 API、可以独立扩容、可以用任意语言写插件。
  2. manifest 驱动权限:所有"插件能干什么"在 manifest 静态声明,安装时即可审计;运行时由 daemon 强制 + 主 API 反向调用接口二次校验。
  3. 租户绑定式凭证:插件不持有用户密钥,所有需要 LLM/Tool 的地方都用主服务上下文,自然继承租户的 provider 配置和配额。
  4. 反向调用优先于内置能力:与其把 100 种 LLM 接入打包进 daemon,不如让 daemon 反过来调主服务------主服务已经做好了所有 provider 适配。
  5. 流式优先:所有 invoke 接口默认返回 generator,长任务(大模型生成、大文件生成)天然支持。
  6. 三段式 ID + plugin_id header :URL 里走 tenant_id 做隔离,header 里走 X-Plugin-ID 做插件路由,业务参数走 body 里的 provider/tool 做具体分发,三层正交。

14. 与前端的整合

web/app/components/plugins/ 下:

子目录 作用
marketplace/ 商店浏览/搜索/详情
install-plugin/ 安装对话框(Marketplace/GitHub/本地包三个 Tab)
plugin-page/ 已安装插件的管理列表
plugin-detail-panel/ 单个插件详情、版本、端点配置
plugin-mutation-model/ 凭证 / 配置编辑器

工作流编辑器中的 Tool 节点选择器、模型选择器都会按租户已安装的插件实时刷新。

15. 阅读源码的推荐顺序

如果你想深入下去,按下面顺序读最快:

  1. api/core/plugin/entities/plugin.py ------ 先把 manifest 的形状装进脑子
  2. api/core/plugin/impl/base.py ------ 看清主 → daemon 的 HTTP 协议
  3. api/core/plugin/impl/tool.py ------ 一个具体 dispatch 调用示例
  4. api/controllers/inner_api/plugin/plugin.py + api/core/plugin/backwards_invocation/ ------ 反向调用全景
  5. api/core/plugin/utils/chunk_merger.py ------ 流式 BLOB 协议
  6. api/services/plugin/plugin_service.py + api/controllers/console/workspace/plugin.py ------ 安装/管理业务流
  7. dify-plugin-daemon 的 README ------ 看 daemon 这一侧

读完这 7 个文件,整套机制就拼齐了。

相关推荐
渐儿1 小时前
Spring Boot 异步并发实现原理详解
后端
来一斤小鲜肉1 小时前
Spring AI 多模态能力全景
后端·aigc
张立立1 小时前
震惊!用Python每天早上8点,我准时给女神发早安,只因这个脚本…
后端·python
渐儿1 小时前
Python 并行与并发:案例与实现
后端
神奇小汤圆1 小时前
面试官问:让你设计一个消息队列,你会怎么答?
后端
techdashen1 小时前
Cloudflare 如何用 Rust 构建一个高性能解释器
开发语言·后端·rust
sing~~1 小时前
SpringCloud的了解和使用
后端·spring·spring cloud
神奇小汤圆2 小时前
K8s生产环境那些文档不会告诉你的坑
后端
流觞 无依2 小时前
Spring Boot 未授权访问漏洞排查与修复指南
java·spring boot·后端