❝本文深入剖析 Dify 插件系统的核心机制,揭秘插件守护进程如何加载、启动和执行插件代码,以及参数传递的完整链路。❞
Dify 作为一款开源的 LLM 应用开发平台,其插件系统是扩展平台能力的核心机制。很多开发者在阅读源码时会产生疑问:
本文将逐一解答这些问题,带你深入理解 Dify 插件系统的运行原理。
在了解执行机制之前,我们先看看一个标准的 Dify 插件包长什么样:
my_plugin.difypkg (压缩包)
├── manifest.yaml # 插件清单(入口点、权限、资源限制)
├── _assets/ # 图标等资源
├── provider/ # 提供商配置
├── tools/ # 工具实现代码
│ ├── my_tool.yaml # 工具配置
│ └── my_tool.py # 工具代码
└── requirements.txt # Python 依赖
其中 manifest.yaml 是插件的"身份证",定义了插件的元信息和入口点:
version: 0.0.1
type:plugin
author:developer
name:my_plugin
meta:
runner:
language:python
version:"3.12"
entrypoint:main# 关键:入口点
当用户上传一个 .difypkg 插件包时,守护进程会执行以下步骤:
┌──────────────┐
│ 上传 .difypkg │
└──────┬───────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 1. 解压插件包到 /plugins/{plugin_id}/ │
│ └── 提取 manifest.yaml、代码、依赖 │
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 2. 创建 Python 虚拟环境 │
│ └── python -m venv /plugins/{id}/venv │
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 3. 安装依赖(注意:不是安装插件本身) │
│ └── pip install -r requirements.txt │
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 4. 预编译 .pyc 文件(加速启动) │
│ └── python -m compileall /plugins/{id}/ │
└──────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 5. 注册到 Plugin Manager │
│ └── 保存插件元信息到数据库 │
└──────────────────────────────────────────────────┘
Dify 插件系统采用「多进程架构」,守护进程(Go 实现)与插件进程(Python 实现)通过管道通信:
Plugin Daemon (Go)
│
│ exec.Command("python", "-m", "main")
▼
┌──────────────────────────────────────┐
│ Plugin Process (Python) │
│ │
│ sys.stdin ◄──── JSON 请求消息 │
│ │ │
│ ▼ │
│ Message Handler │
│ │ │
│ ├─── route to Tool._invoke() │
│ ├─── route to Model._invoke() │
│ └─── route to Extension.handle()│
│ │ │
│ ▼ │
│ sys.stdout ────► JSON 响应消息 │
└──────────────────────────────────────┘
当首次调用插件时,守护进程会懒加载启动插件进程:
// Plugin Daemon 启动插件进程(伪代码)
func (p *PluginManager) LaunchLocalPlugin(pluginId string) {
// 1. 读取 manifest.yaml 获取入口点
manifest := loadManifest(pluginId)
entrypoint := manifest.Meta.Runner.Entrypoint // "main"
// 2. 构建启动命令
cmd := exec.Command(
venvPythonPath, // 虚拟环境的 Python
"-m", entrypoint, // python -m main
)
cmd.Dir = pluginDir // 关键:设置工作目录
// 3. 建立通信管道
cmd.Stdin = stdinPipe
cmd.Stdout = stdoutPipe
// 4. 启动进程
cmd.Start()
}
当执行 python -m main 时,Python 的工作流程:
sys.path 中查找 main 模块__init__.py),执行 __main__.py__name__ = "__main__"插件的入口文件 main.py 通常这样实现:
# main.py
from dify_plugin import Plugin
# 创建插件实例,自动发现并加载组件
plugin = Plugin()
if __name__ == "__main__":
plugin.run() # 启动消息循环,监听 STDIN
Plugin SDK 会根据目录结构自动发现和加载工具、模型等组件:
# Plugin SDK 内部逻辑(简化)
class Plugin:
def __init__(self):
# 1. 读取 manifest.yaml
self.manifest = self._load_manifest()
# 2. 扫描目录,动态加载模块
self.tools = self._discover_tools("tools/")
self.models = self._discover_models("models/")
def _discover_tools(self, path):
tools = {}
for yaml_file in glob(f"{path}/*.yaml"):
config = load_yaml(yaml_file)
py_file = yaml_file.replace(".yaml", ".py")
# 动态导入 Python 模块
module = importlib.import_module(py_file)
tool_class = getattr(module, config["class_name"])
tools[config["name"]] = tool_class
return tools
守护进程与插件进程通过 「STDIN/STDOUT 管道 + JSON 消息」进行通信:
┌─────────────────┐
│ Dify 前端/API │
│ parameters: { │
│ query: "xxx" │
│ } │
└────────┬────────┘
│ HTTP
▼
┌─────────────────┐
│ Plugin Daemon │──── 封装 JSON 消息
└────────┬────────┘
│ STDIN (管道)
▼
┌─────────────────┐
│ 插件子进程 │
│ json.loads() │──── 解析参数
│ tool._invoke() │──── 执行逻辑
└────────┬────────┘
│ STDOUT (管道)
▼
┌─────────────────┐
│ Plugin Daemon │──── 解析响应
└─────────────────┘
守护进程发送给插件的请求消息:
{
"type": "invoke",
"session_id": "abc123",
"plugin_type": "tool",
"action": "invoke",
"data": {
"tool_name": "google_search",
"parameters": {
"query": "Dify AI",
"max_results": 10
},
"credentials": {
"api_key": "sk-xxx"
},
"tool_runtime": {
"tenant_id": "tenant-001",
"user_id": "user-001"
}
}
}
# Plugin SDK 消息循环
whileTrue:
line = sys.stdin.readline()
request = json.loads(line)
# 提取参数
tool_name = request["data"]["tool_name"]
params = request["data"]["parameters"]
credentials = request["data"]["credentials"]
# 路由到具体工具并传递参数
tool = self.tools[tool_name]
result = tool._invoke(
tool_parameters=params,
credentials=credentials
)
# 返回结果
sys.stdout.write(json.dumps({"result": result}) + "\n")
sys.stdout.flush()
开发者实现的工具类:
# tools/google_search.py
class GoogleSearchTool(Tool):
def _invoke(self, tool_parameters: dict, credentials: dict):
# 从 tool_parameters 获取用户输入
query = tool_parameters.get("query")
max_results = tool_parameters.get("max_results", 10)
# 从 credentials 获取凭证
api_key = credentials.get("api_key")
# 执行具体逻辑
results = self.search(query, api_key, max_results)
return results
对于需要流式输出的场景(如 LLM 调用),通过多次写入 STDOUT:
def _invoke(self, ...):
for chunk in llm.stream(prompt):
sys.stdout.write(json.dumps({
"type": "stream",
"chunk": chunk
}) + "\n")
sys.stdout.flush()
# 完成信号
sys.stdout.write(json.dumps({"type": "end"}) + "\n")
最后,我们用一张图总结从安装到执行的完整链路:
安装阶段:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 解压包 │───▶│ 创建venv │───▶│ 安装依赖 │───▶│ 预编译 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
运行阶段 (懒加载):
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ 首次调用 │───▶│ exec.Command │───▶│ python -m main│
└──────────┘ │ 启动子进程 │ └──────┬───────┘
└──────────────┘ │
▼
┌────────────────────┐
│ Plugin SDK 初始化 │
│ - 读取 manifest │
│ - 发现 tools/models │
│ - 注册处理器 │
│ - 启动消息循环 │
└────────────────────┘
调用阶段:
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ API 请求 │───▶│ JSON 消息 │───▶│ STDIN 传递 │
└──────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌────────────────────┐
│ tool._invoke() │
│ - 解析参数 │
│ - 执行业务逻辑 │
│ - 返回结果 │
└────────────────────┘
Dify 插件系统的设计有以下特点:
pip install 插件,通过设置工作目录实现模块导入这种设计在保证安全隔离的同时,也提供了良好的开发体验和热更新能力,是一种值得借鉴的插件架构模式。