【Dify】插件离线安装完整踩坑记录
在无法访问外网的环境中部署 Dify 时,插件安装是最大的障碍之一。本文记录了从打包到成功安装的完整过程,以及途中遇到的三个关键问题和解决方案。
背景
Dify 1.0 引入了插件系统,插件以 .difypkg 格式分发。在线环境下,可以直接从 Marketplace 一键安装。但在离线(air-gapped)环境中,plugin daemon 无法联网下载 Python 依赖,安装必然失败。
社区提供了 dify-plugin-repackaging 工具,可以将插件重打包为带有全部 Python 依赖的离线包。但实际操作中,我踩了三个坑,花了不少时间才最终搞定。最后我自己用 Claude 临时修改了一个版本的重打包脚本:dify-plugin-repackaging。
本地环境
- Dify 通过 Docker Compose 部署
- Plugin daemon 镜像:
langgenius/dify-plugin-daemon:0.5.3-local - 容器环境:Ubuntu 24.04, Python 3.12.3, uv 0.9.26
- 目标插件:
langgenius/openai_api_compatible:0.0.35
离线打包基本流程
使用 plugin_repackaging.sh 脚本,三种输入方式:
# 从 Marketplace 下载并重打包
./plugin_repackaging.sh market langgenius openai_api_compatible 0.0.35
# 从 GitHub Release 下载并重打包
./plugin_repackaging.sh github langgenius/dify-plugin-xxx v0.0.1 xxx.difypkg
# 从本地已有包重打包
./plugin_repackaging.sh local ./langgenius-openai_api_compatible_0.0.35.difypkg
脚本的核心逻辑(repackage() 函数):
- 解压
.difypkg(实际是 zip) pip download把requirements.txt中的所有依赖下载到./wheels/- 将 sdist 格式的包编译为 wheel
- 在
requirements.txt头部插入--no-index --find-links=./wheels/ - 用
dify-pluginCLI 重新打包为*-offline.difypkg
安装时,在 Dify 控制台的插件管理页上传这个离线包即可。
环境配置
离线包通常未经 Marketplace 签名,且体积较大,需要调整 .env:
# 允许安装未签名插件
FORCE_VERIFYING_SIGNATURE=false
# 放大包体限制到 500MB
PLUGIN_MAX_PACKAGE_SIZE=524288000
# Nginx 上传限制也要放大
NGINX_CLIENT_MAX_BODY_SIZE=500M
改完后 docker compose up -d 重启。
坑一:daemon 用的是 uv,不是 pip
现象
上传离线包后,daemon 报错:
error: Request failed after 3 retries
Caused by: Failed to fetch: `https://pypi.org/simple/setuptools/`
Caused by: dns error: failed to lookup address information: Name or service not known
明明 wheels 都打进了包里,为什么还在联网?
原因
打包脚本在 requirements.txt 头部加了 --no-index --find-links=./wheels/,这是 pip 的语法。但 Dify 的 plugin daemon 使用的是 uv(日志可见 uv 0.9.26),它通过 uv sync 读取 pyproject.toml 来解析依赖,完全忽略 requirements.txt 中的 pip 选项。
解决
在 pyproject.toml 中添加 [tool.uv] 配置,告诉 uv 使用本地 wheels:
[tool.uv]
no-index = true
find-links = ["./wheels/"]
同时修改 plugin_repackaging.sh,在打包流程中自动注入这段配置(新增 inject_uv_offline_config() 函数)。
坑二:Python 版本不匹配
现象
加了 [tool.uv] 配置后,不再联网了,但报新错误:
gevent==25.5.1 cannot be used.
And because dify-plugin==0.7.1 depends on gevent==25.5.1,
your project's requirements are unsatisfiable.
原因
我本机是 Python 3.13,pip download 下载的 wheel 都是 cp313 标签的(如 gevent-25.5.1-cp313-cp313-manylinux_2_17_x86_64.whl)。但 daemon 容器里是 Python 3.12.3,cp313 的 wheel 无法在 3.12 上使用。
解决
两种方式:
- 用 Python 3.12 执行打包脚本(推荐),下载的 wheel 标签就是
cp312 - 指定平台参数:
./plugin_repackaging.sh -p manylinux_2_17_x86_64 local xxx.difypkg
坑三:uv 的 universal resolution 与离线模式冲突
现象
换成 Python 3.12 后,wheels 标签正确了,但还是报错:
Because only cffi{platform_python_implementation == 'CPython' and
sys_platform == 'win32'}<1.17.1 is available and gevent==25.5.1 depends
on cffi{...sys_platform == 'win32'...}>=1.17.1,
we can conclude that gevent==25.5.1 cannot be used.
补上 cffi 后,又冒出来:
Because there are no versions of colorama{sys_platform == 'win32'}
and tqdm==4.67.1 depends on colorama{sys_platform == 'win32'},
we can conclude that tqdm==4.67.1 cannot be used.
原因
这是 uv 和 pip 的核心行为差异:
| pip | uv | |
|---|---|---|
| 依赖解析范围 | 只为当前平台解析 | 默认做 universal resolution(全平台) |
cffi; sys_platform == 'win32' | Linux 上直接跳过 | 尝试为 Windows 也找到 cffi |
在 no-index 模式下 | 无影响 | 找不到 Windows 依赖就报错 |
uv 为了生成跨平台通用的 lock 文件,会同时为 Linux、Windows、macOS 等所有平台解析依赖。在有网络的情况下这没问题,但在 no-index 离线模式下,wheels 目录里当然只有 Linux 的包,Windows-only 的依赖(cffi、colorama 等)自然找不到。
解决
在 [tool.uv] 中加上 environments 配置,限定只为 Linux 解析:
[tool.uv]
no-index = true
find-links = ["./wheels/"]
environments = ["sys_platform == 'linux'"]
这样 uv 就不再尝试解析 Windows-only 的依赖了。
对重打包脚本的修改
解决上面三个坑的关键,是修改 plugin_repackaging.sh,让它在打包时自动将 uv 离线配置注入 pyproject.toml。下面详细说明修改过程。
原始脚本的问题
原始 plugin_repackaging.sh 的 repackage() 函数核心流程如下:
repackage(){
# ... 解压 ...
pip download -r requirements.txt -d ./wheels # 下载依赖到 wheels/
build_wheels_from_sdists # 编译 sdist → wheel
cleanup_wheels_non_whl # 清理非 .whl 文件
# 在 requirements.txt 头部插入 pip 离线选项
sed -i '1i\--no-index --find-links=./wheels/' requirements.txt
# 处理 ignore 文件、重打包 ...
}
问题在于:脚本只修改了 requirements.txt,加入的 --no-index --find-links=./wheels/ 是 pip 的参数语法。但 Dify plugin daemon 内部使用 uv sync,它只读 pyproject.toml,完全忽略 requirements.txt 中的 pip 选项。所以虽然 wheels 打进了包里,daemon 安装时依然去联网下载。
修改一:新增 inject_uv_offline_config() 函数
我们需要一个函数,在打包时将 [tool.uv] 配置注入 pyproject.toml。这段配置告诉 uv:不访问远程索引,从本地 ./wheels/ 目录查找包。
第一版只解决了坑一(离线下载)的问题:
inject_uv_offline_config(){
if [ ! -f "pyproject.toml" ]; then
return 0
fi
python - <<'PY'
from pathlib import Path
import re
path = Path("pyproject.toml")
content = path.read_text(encoding="utf-8")
uv_block = """
[tool.uv]
no-index = true
find-links = ["./wheels/"]
"""
# Remove any existing [tool.uv] section to avoid duplicates
content = re.sub(r'\n?\[tool\.uv\][^\[]*', '', content, flags=re.DOTALL)
content = content.rstrip("\n") + "\n" + uv_block.lstrip("\n")
path.write_text(content, encoding="utf-8")
PY
echo "Injected [tool.uv] offline config into pyproject.toml"
}
这里用内嵌 Python 而非纯 shell 来操作 TOML,是因为需要先用正则删除可能已存在的 [tool.uv] 段(避免重复注入),再追加新配置。纯 sed 处理多行 TOML 段落很容易出错。
修改二:加入 environments 约束
解决坑三后,我们在 uv_block 中加入了一行:
uv_block = """
[tool.uv]
no-index = true
find-links = ["./wheels/"]
+environments = ["sys_platform == 'linux'"]
"""
这行告诉 uv 只为 Linux 平台解析依赖,不再尝试寻找 Windows-only 的包(如 cffi; sys_platform == 'win32'、colorama; sys_platform == 'win32')。
修改三:在 repackage() 中调用
在 repackage() 函数中,紧跟 sed 修改 requirements.txt 之后,加入一行调用:
# sed 修改 requirements.txt(保留 pip 兼容性)...
sed -i '1i\--no-index --find-links=./wheels/' requirements.txt
+ # Patch pyproject.toml so that uv also uses local wheels (offline)
+ inject_uv_offline_config
# 处理 ignore 文件 ...
IGNORE_PATH=.difyignore
这样在原有的 pip 离线配置基础上,同时为 uv 生成了对应的离线配置,两个工具都能正确使用本地 wheels。
最终注入效果
经过修改后的脚本,打包出的离线插件中 pyproject.toml 会自动带上:
[tool.uv]
no-index = true
find-links = ["./wheels/"]
environments = ["sys_platform == 'linux'"]
三个配置项各自解决一个问题:
| 配置 | 作用 | 对应坑 |
|---|---|---|
no-index = true | 不访问任何远程 PyPI | 坑一 |
find-links = ["./wheels/"] | 从本地 wheels 目录查找包 | 坑一 |
environments = ["sys_platform == 'linux'"] | 只为 Linux 平台解析依赖 | 坑三 |
完整的离线安装操作步骤
# 1. 确保使用 Python 3.12(与 daemon 容器一致)
python3 --version # 应输出 3.12.x
# 2. 打包
cd dify-plugin-repackaging
./plugin_repackaging.sh local langgenius-openai_api_compatible_0.0.35.difypkg
# 3. 调整 Dify 的 .env
# FORCE_VERIFYING_SIGNATURE=false
# PLUGIN_MAX_PACKAGE_SIZE=524288000
# NGINX_CLIENT_MAX_BODY_SIZE=500M
# 4. 重启
cd dify/docker
docker compose up -d
# 5. 在 Dify 控制台 → 插件管理 → 上传本地包
# 选择 langgenius-openai_api_compatible_0.0.35-offline.difypkg
总结
| 问题 | 根因 | 解决方案 |
|---|---|---|
| 离线包仍联网下载 | daemon 用 uv 不用 pip,不读 requirements.txt 的 pip 选项 | 在 pyproject.toml 加 [tool.uv] 的 no-index + find-links |
| wheel 版本不兼容 | 打包机 Python 版本与 daemon 不一致 | 用 Python 3.12 打包,或用 -p 指定平台 |
| uv 解析 Windows 依赖失败 | uv 默认全平台解析,离线模式下缺少 Windows-only 的 wheel | 加 environments = ["sys_platform == 'linux'"] 限定解析范围 |
核心教训:uv ≠ pip。uv 的 universal resolution 设计在在线场景下很优雅,但在离线场景下会产生意想不到的问题。理解工具链的实际行为比盲目排错重要得多。