本文基于
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_id:langgenius/web_reader(用作 HTTP headerX-Plugin-ID)provider_name:web_reader(daemon 内部分发给具体 provider 实现)
子类型见 ModelProviderID / ToolProviderID / DatasourceProviderID / TriggerProviderID。
5. 安装与生命周期
5.1 四种安装来源
| 来源 | 说明 |
|---|---|
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.py 中 PluginVerification):
OFFICIAL_ONLY-- 仅 langgenius 官方OFFICIAL_AND_SPECIFIC_PARTNERS-- 官方 + 白名单 partnerALL-- 任意
租户级 ------ 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 调用
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 反向调用
PluginInvokeLLMApi → PluginModelBackwardsInvocation.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()
关键点:
- 插件用的是主租户的模型配置和配额------它无需自带 OpenAI key,就能用工作区已经配好的 GPT-4。
- Token 用量也算到该租户头上 ------
deduct_llm_quota。 - 完整路由列表(行号见 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 反向调用
PluginInvokeAppApi → PluginAppBackwardsInvocation.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.py 中 PLUGIN_* 一组:
| 变量 | 默认 | 含义 |
|---|---|---|
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 实例"的开发模式:
- 开发者用
dify-pluginCLI 在本地以 debug 模式启动插件,连到 daemon 的 5003 端口; - daemon 把这个 socket 当成一个普通插件实例注册进来;
- 该插件只对开启 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. 关键设计取舍
- 进程隔离 > 性能:HTTP 而非 in-process 调用,单次请求多了一跳网络开销;换来的是插件崩溃/内存泄漏不会拖垮主 API、可以独立扩容、可以用任意语言写插件。
- manifest 驱动权限:所有"插件能干什么"在 manifest 静态声明,安装时即可审计;运行时由 daemon 强制 + 主 API 反向调用接口二次校验。
- 租户绑定式凭证:插件不持有用户密钥,所有需要 LLM/Tool 的地方都用主服务上下文,自然继承租户的 provider 配置和配额。
- 反向调用优先于内置能力:与其把 100 种 LLM 接入打包进 daemon,不如让 daemon 反过来调主服务------主服务已经做好了所有 provider 适配。
- 流式优先:所有 invoke 接口默认返回 generator,长任务(大模型生成、大文件生成)天然支持。
- 三段式 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. 阅读源码的推荐顺序
如果你想深入下去,按下面顺序读最快:
- api/core/plugin/entities/plugin.py ------ 先把 manifest 的形状装进脑子
- api/core/plugin/impl/base.py ------ 看清主 → daemon 的 HTTP 协议
- api/core/plugin/impl/tool.py ------ 一个具体 dispatch 调用示例
- api/controllers/inner_api/plugin/plugin.py + api/core/plugin/backwards_invocation/ ------ 反向调用全景
- api/core/plugin/utils/chunk_merger.py ------ 流式 BLOB 协议
- api/services/plugin/plugin_service.py + api/controllers/console/workspace/plugin.py ------ 安装/管理业务流
- dify-plugin-daemon 的 README ------ 看 daemon 这一侧
读完这 7 个文件,整套机制就拼齐了。