ComfyUI插件wheel化:custom_nodes加载与离线部署
发布时间:2026/11/4 22:00:36 锦皓数字建站

给一台不联网的工作站配 ComfyUI 环境最耗时的环节往往出在custom_nodes上十几个插件每个都要git clone、逐个装requirements.txt、还得记住当时拉的是哪个 commit。用这种方式重装过一次之后我开始认真考虑另一条路能不能把 ComfyUI-VideoHelperSuite 这类插件当作标准 Python 包构建成 wheel 文件再用pip install一步到位。这个想法听起来顺理成章实际实施下来却发现ComfyUI 的插件加载机制和常规 Python 包的安装路径之间存在不一致涉及加载器、资源目录、前端扩展映射、二进制依赖等多个环节。这篇笔记记录的是我在 ComfyUI-VideoHelperSuite 上验证可行性的完整过程怎么读插件结构、怎么改写打包配置、怎么把装进site-packages的模块接入回custom_nodes、以及过程中踩到的几类典型故障。适合手上有多个 ComfyUI 环境需要同步、或者要给离线机器做插件交付的读者参考如果你只是在自己机器上随便装个插件玩玩后半部分的折腾大概率用不上。1. 为什么我盯上了把插件打成 wheel 这件事1.1 custom_nodes 的加载逻辑决定了插件不能随便挪位置要理解为什么打成 wheel是个需要探索的问题得先搞明白 ComfyUI 是怎么把custom_nodes里的东西变成工作流里那些节点的。主程序里负责这块的代码思路非常朴素启动时遍历custom_nodes目录对每一个子目录检查是否存在__init__.py存在就用importlib.util.spec_from_file_location按文件路径加载然后把模块里导出的NODE_CLASS_MAPPINGS、NODE_DISPLAY_NAME_MAPPINGS合并进全局节点表。整个流程的关键点在于——它认的是文件路径不是包名。这个设计带来两个直接后果。第一插件的模块名通常就是目录名模块的身份由目录位置决定。第二加载器根本不会去site-packages里找任何东西。所以哪怕你用pip install把一个插件装得干干净净import也没问题ComfyUI 启动后依然一个节点都不会多出来。这不是插件写错了而是两套定位机制压根没对上pip认的是sys.pathComfyUI 认的是custom_nodes下的物理目录。我在某实验室的一台离线工作站上第一次撞上这个墙。当时想着装个包而已pip install完重启节点列表纹丝不动日志里连报错都没有因为加载器压根没扫到那个位置。排查的时候还一度怀疑是缓存问题清了好几遍__pycache__才意识到方向错了。这个认知很关键它决定了后面所有方案的走向wheel 化不是终点把装好的模块重新接回custom_nodes的视野才是。顺带说一句目录名尽量用下划线别用中划线或者中文。虽然按路径加载时名字不合法的模块名一般也能跑起来但一旦涉及import、日志打印、前端扩展路径映射不规范的目录名就会在某个环节给你使绊子我在第 5 章会具体讲。1.2 wheel 化真正想解决的是三个痛点不是装得更优雅把插件打成 wheel 这件事如果只是想装起来好看那完全不值得投入时间。我动手的真实动机来自三个具体的运维场景每个都对应着实打实的成本。痛点场景git clone 方式的表现wheel 化之后的预期多台机器环境同步每台都要重新拉取、装依赖commit 漂移一个文件复制过去pip install即可离线环境交付要打包整个仓库加依赖容易漏文件wheel 自带全部代码与资源依赖可预先离线准备版本回滚需要记住具体 commit 并手动切换pip install 指定版本号回滚是原子操作依赖一致性每个插件各自pip install -r互相覆盖依赖声明集中可配合锁文件统一管理第一个场景最痛。同一套工作流在三台机器上跑其中一台的 VideoHelperSuite 是两个月前拉的另外两台是上周拉的结果同一个视频加载节点在一台机器上能读某些容器格式在另一台上直接报错。查了半天发现是依赖里imageio-ffmpeg版本不同导致解码走的路径不一样。这种隐性版本漂移在git clone模式下几乎无法避免因为你没有一份可被机器读取的版本契约。第二个场景是离线的硬约束。那台工作站没有外网一切靠拷贝。git clone的仓库能拷但requirements.txt里的依赖得一个个找 whl 文件还得按正确顺序装漏一个就是启动时ModuleNotFoundError。如果插件本身就是个 wheel并把它依赖的纯 Python 包也一起构建成 wheel 放进同一个目录交付就变成了拷一个文件夹、跑一条pip install --no-index --find-links命令。这个收益是实打实的。第三、四个场景属于长期收益。这里要先说清楚一件事wheel 化解决的是代码与资源的可复现分发它并不会自动解决依赖冲突——甚至有可能因为pip开始解析install_requires而引入新的冲突。这一点我在第 5 章专门有一节讲也是整个探索里最容易被低估的风险点。所以在动手前我的预期是代码打包这条路走得通但依赖声明要么写得很保守要么干脆不写交给上级环境统一管。1.3 先划一条可行性边界什么样的插件适合 wheel 化不是所有 ComfyUI 插件都值得走这条路。我按经验把插件粗分成三类判断标准很实际——看它有多少非 Python 代码和运行时副作用。第一类是纯节点类插件只有几个nodes.py没有前端 JS、没有额外数据文件、依赖也少。这类打成 wheel 最顺几乎零改造shim 一写就能跑。第二类是带前端扩展和数据资源的插件VideoHelperSuite 就属于这一档它有web/目录写 JS有格式定义 JSON还依赖外部 ffmpeg 二进制。这类能打但package_data的配置必须精确漏一个文件就是前端按钮消失或者格式列表为空。第三类是那种需要在导入时干很多事的插件比如启动子进程、监听端口、动态下载模型。这类打 wheel 也不是不行但每次 import 的副作用会让调试变得极其恶心我更倾向于让它老老实实待在custom_nodes里。VideoHelperSuite 落在第二类正好是我认为有代表性又不会太离谱的样本。它的目录结构完整、资源文件典型、依赖里既有纯 Python 包也有需要编译的二进制约包把它跑通基本等于把这类插件的问题都摸了一遍。后面几章就沿着读结构 → 写打包配置 → 解决加载 → 排障的顺序展开。2. 先把 VideoHelperSuite 的目录结构摸清楚2.1 一个典型 ComfyUI 插件的骨架长什么样打包之前必须先读结构这一步没有捷径。我拿到的是仓库目录摊开来看大概是这么个样子文件清单会随版本变化以你手上那份为准但布局逻辑基本一致ComfyUI-VideoHelperSuite/ ├── __init__.py ├── requirements.txt ├── pyproject.toml # 部分版本自带 ├── videohelpersuite/ │ ├── __init__.py │ ├── nodes.py │ ├── load_video_nodes.py │ ├── load_audio_nodes.py │ ├── utils.py │ ├── logger.py │ └── server.py ├── web/ │ ├── js/ │ └── ... ├── video_formats/ │ └── *.json └── ...这个布局里__init__.py是入口它干的事大致包括从videohelpersuite子包里导入各个节点类组装成NODE_CLASS_MAPPINGS和NODE_DISPLAY_NAME_MAPPINGS两个字典然后声明WEB_DIRECTORY ./web。这个WEB_DIRECTORY是个相对路径ComfyUI 在加载完模块后会拿模块所在目录拼上这个相对路径把里面的 JS 作为前端扩展挂到/extensions/模块名/下。这里是最重要的一个观察WEB_DIRECTORY的相对性是相对于入口__init__.py所在的目录而不是相对于videohelpersuite/子包。这意味着只要我在打包时保持入口文件和web/目录在同一层这个相对关系源码里这行WEB_DIRECTORY ./web一行都不用改。反过来说如果打包时把web/塞进了子包里、入口留在外面这行就得跟着改改动越多出错概率越大。这个发现直接决定了我后面的打包布局选择。2.2 那些藏在代码之外的运行时资源才是打包的真正难点Python 代码打包是成熟工艺packages一列就完事。麻烦的是那些不是.py的文件。VideoHelperSuite 里至少有三类第一类是前端 JS在web/目录下。它在浏览器端跑负责给视频节点加上预览、帧数滑块、格式下拉这些交互。这些文件如果不进 wheel节点在服务端能跑但前端看不到任何 UI 增强表现为节点能用但长得跟原版不一样很容易被误判成打包失败。第二类是格式定义数据在video_formats/目录下是一堆 JSON。代码在初始化时会去读这个目录拼出容器格式和编码器的可选项。目录缺失的话节点上的格式下拉框会是空的或者回落到一个极小的内置列表。第三类是 ffmpeg 二进制。这个严格说不在仓库里而是靠imageio-ffmpeg这个依赖在安装时带下来的静态二进制。所以它属于依赖的副作用不是包内资源。但它对插件能不能工作至关重要第 4 章会专门讲它在 wheel 安装场景下的路径问题。我把这三类资源对打包的要求列一下方便对照检查资源类型位置打包归属漏掉的后果前端 JSweb/package_data节点无 UI 增强预览/滑块失效格式定义video_formats/*.jsonpackage_data格式下拉为空或选项极少ffmpeg 二进制依赖包提供走dependencies或预装视频转码与部分解码失败Python 源码videohelpersuite/packages直接导入失败package_data这块的坑在于通配符写法。web/*只能匹配一层web/js/下的文件不会被打进去。正确写法要么写web/**/*新版 setuptools 支持要么老实列出每一层或者干脆用web/*/*。我第一次构建出来的 wheel 就是前端不生效解压一看web/目录里只有一层文件js/子目录整个没进去。这个问题在构建阶段完全静默只有装完跑起来才发现。提示每次构建完务必用python -m zipfile -l dist/xxx.whl或者unzip -l把产物列一遍逐条比对源目录的关键文件清单。这一步花三十秒能省掉后面半小时的排查。2.3 依赖清单里最容易被 pip 顺手改坏的三个方向打开requirements.txtVideoHelperSuite 这一档的插件通常会声明这么几类深度学习框架torch、torchvision、图像视频处理opencv-python、Pillow、编解码与 IOimageio、imageio-ffmpeg、数值计算numpy。这四类里前三类都是重量级且与主环境强耦合的。如果你在pyproject.toml里把它们原样写进dependenciespip install这个 wheel 的时候会做完整的依赖解析结果是它认为环境里现有的torch版本不满足约束于是给你升或者降一个版本。而 ComfyUI 主环境对torch版本是有要求的CUDA 版本、驱动匹配一旦被换掉可能整个推理全挂。opencv-python也有类似问题。很多环境里装的是opencv-python-headless服务器无 GUI 场景的常规选择而插件声明的是opencv-python。这两个包提供同样的cv2模块但互相冲突pip会提示后者覆盖前者装完之后 import 倒是能过但某些环境下会牵扯到图形库依赖的问题。numpy的坑更隐蔽。numpy 2.x 改了一批 C ABI一些用旧版编译的扩展包尤其是预编译的opencv轮子在 numpy 2.x 下会报_ARRAY_API not found之类的错误。如果你 wheel 里写了numpy但不锁版本pip有可能把环境里的 numpy 拉到最新。所以我的策略很明确wheel 的dependencies只写那些轻量、纯 Python、版本约束宽松的包重型依赖一律不写靠环境预装。代价是安装时必须加--no-deps或者人工核对收益是不会动到主环境的基石。这个取舍在第 6 章还会展开讲因为它决定了这套方案到底适不适合你的场景。3. 从源码目录到 wheel打包配置怎么写才不出错3.1 布局选择为什么我最终选了整目录提包而不是原地打包拿到结构之后第一个决策是打包布局。有两条路一是把仓库根目录当成一个包原地打包二是新建一个包目录把需要的东西全部搬进去。原地打包的诱惑是改动最小但它有个硬伤setuptools 的包模型要求每个包目录里都有__init__.py而web/、video_formats/这两个目录里显然没有。它们既不能作为packages要作为package_data又必须挂在某个包下面。仓库根目录本身不是一个合法的包名它的名字就是仓库名带中划线不是合法标识符所以package_data找不到归属。我的做法是新建一个包目录直接把原插件的入口和资源一起搬进去保持内部相对结构不变wheel-src/ ├── pyproject.toml └── vhs_plugin/ ├── __init__.py # 原仓库根目录的 __init__.py ├── videohelpersuite/ # 整个子包搬过来 │ ├── __init__.py │ ├── nodes.py │ └── ... ├── web/ # 原样搬 │ └── js/... └── video_formats/ # 原样搬 └── *.json这个布局的好处在第 2 章已经埋下伏笔vhs_plugin/__init__.py和vhs_plugin/web/在同一层所以源码里WEB_DIRECTORY ./web保持成立。video_formats/同理代码里如果是用os.path.dirname(__file__)拼路径去找这个目录只要目录相对__init__.py的位置没变也照样能找到。代价是入口文件里的绝对导入可能要动。原__init__.py里如果写的是from videohelpersuite.nodes import ...顶层绝对导入搬进vhs_plugin/之后这个导入就找不到了因为videohelpersuite现在在vhs_plugin下面。改法是换成相对导入from .videohelpersuite.nodes import ...。这个改动很小但必须做而且要检查所有出现的地方——不只是__init__.py子包内部如果互相之间用的绝对导入也得一并改成相对导入或者统一加前缀。我在这步漏了两处第 5 章会讲具体表现。3.2 pyproject.toml 的关键字段逐条说明配置用的是 setuptools 后端总共不到四十行但每一行都有讲究[build-system] requires [setuptools68, wheel] build-backend setuptools.build_meta [project] name comfyui-videohelpersuite-pack version 1.7.0 description Packaged distribution of the video helper node collection requires-python 3.10 dependencies [ Pillow, psutil, ] [tool.setuptools] packages [vhs_plugin, vhs_plugin.videohelpersuite] include-package-data true zip-safe false [tool.setuptools.package-data] vhs_plugin [ web/*, web/*/*, video_formats/*.json, ]逐条拆一下。name用不带中划线的名字并且和原插件名区分开避免和你环境里其他东西撞车version手动维护这就是你的版本契约回滚全靠它。dependencies里我只留了Pillow和psutil这种版本宽松、纯 Python 或轻编译的包torch、opencv、imageio一个都没写——原因在 2.3 节说过。packages必须把每一层子包都列出来vhs_plugin.videohelpersuite这条不能省。新手最容易只写顶层包结果子包里的.py全没进 wheel装完 import 报No module named vhs_plugin.videohelpersuite。另外如果videohelpersuite/下面还有更深一层的子包也得照列。懒得手动维护的话可以用[tool.setuptools.packages.find]自动发现但我更倾向于显式列出来因为自动发现有时候会把不该打进去的测试目录、脚本目录也带上。package-data里的通配符我写了两行web/*和web/*/*就是为了覆盖「一层文件 一层子目录里的文件」这两种情况。如果你的前端目录层次更深要么再加一层通配要么直接上web/**。别指望web/或者web这种写法能递归它不会。include-package-data true这个开关在旧版 setuptools 里语义容易混它管的是通过MANIFEST.in声明的文件对package-data没有影响。两边都写上也行但要知道package-data才是控制 wheel 内容的主力。zip-safe false是为了告诉安装器不要把包当作 zip 形式加载——有些代码用__file__拼路径去读资源zip 导入模式下会失效虽然现代pip默认都是解压安装写上去是个保险。3.3 构建、检查产物的完整命令链路配置写完之后构建本身只要一条命令python -m build --wheel前提是装了build包。如果离线环境没有可以退回到pip wheelpip wheel . -w dist --no-deps--no-deps很重要否则pip会把依赖也一起下载下来放进dist/对离线交付其实是有用的可以省掉单独准备依赖的活但在调试阶段会让构建变慢很多先关掉。产物出来之后第一件事是验货。我习惯用两条命令交叉检查# 列出 wheel 内全部文件 python -m zipfile -l dist/*.whl # 检查 dist-info 里的元数据确认依赖声明符合预期 unzip -p dist/*.whl */METADATA | head -40验货的检查清单我固定为四条入口__init__.py在不在videohelpersuite/下的.py是不是全在web/下的所有层级文件在不在video_formats/的 JSON 在不在。这四条全过构建这步就算完成了。第一次做的时候我漏了第二条和第三条都是靠这个清单抓出来的。3.4 本地安装验证先绕开 ComfyUI单独确认包本身没问题装之前先做一个隔离验证——把 wheel 装进一个干净的虚拟环境只测import不牵扯 ComfyUIpython -m venv /tmp/vhs-test source /tmp/vhs-test/bin/activate pip install dist/*.whl --no-deps python -c import vhs_plugin as m print(classes:, len(m.NODE_CLASS_MAPPINGS)) print(web:, getattr(m, WEB_DIRECTORY, None)) 这一步的价值在于隔离变量。如果这一步就失败问题一定出在打包本身文件缺失、导入路径错、元数据写错跟 ComfyUI 毫无关系。如果这一步过了但 ComfyUI 里不出现节点那问题一定出在加载环节方向立刻清晰。我前面几次排查之所以绕远路就是因为没做这个隔离验证一上来就在 ComfyUI 里试两个层面混在一起排查效率极低。--no-deps在这个隔离环境里是必须的否则pip会去下Pillow、psutil浪费时间且可能失败。隔离验证只关心包的结构和导入是否正常不关心依赖是否齐全。当然如果你想顺便测依赖那就得装个带依赖的完整环境但那样验证的就不只是打包了。4. 装进 site-packages 之后ComfyUI 为什么还是看不见它4.1 扫描器的判定条件决定了三条硬性门槛回到第 1 章那个结论加载器只认custom_nodes下的物理目录。具体到判定条件我实测下来大致是三条第一路径必须是custom_nodes的子目录site-packages里的任何东西都不在扫描范围内。第二目录里必须有__init__.py只有.py文件没有入口的不算。第三目录名不能以.开头、不能以.disabled结尾、不能是__pycache__。这三条同时满足才会被加载。所以 wheel 装完之后模块确实在sys.path里了import vhs_plugin完全没问题但扫描器压根不会走到那个路径。这不是 bug是设计使然。要让 ComfyUI 看见它必须在这两者之间架一座桥。桥怎么架有几种思路各有代价。4.2 方案一custom_nodes 下放一个 shim 目录重新导出这是我最推荐的做法也是最终采用的。思路很简单在custom_nodes下建一个普通目录名字就叫comfyui-videohelpersuite和原来一样这样前端扩展的 URL 路径也保持一致里面只放一个__init__.py内容是把真正实现模块的东西重新导出# custom_nodes/comfyui-videohelpersuite/__init__.py import os import vhs_plugin as _impl NODE_CLASS_MAPPINGS _impl.NODE_CLASS_MAPPINGS NODE_DISPLAY_NAME_MAPPINGS _impl.NODE_DISPLAY_NAME_MAPPINGS # 关键WEB_DIRECTORY 要指向 site-packages 里的真实目录 WEB_DIRECTORY os.path.join(os.path.dirname(_impl.__file__), web) __all__ [NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, WEB_DIRECTORY]这个写法的要点在WEB_DIRECTORY那一行。原插件里它是相对路径./web如果 shim 里照抄ComfyUI 会去拼 shim 目录下的web/那里面空空如也前端扩展就挂了。所以必须改成绝对路径指向 wheel 安装位置里的真实web/目录。os.path.dirname(_impl.__file__)拿到的是site-packages/vhs_plugin拼上web正好是对的位置。这里有个细节要实测确认拼接逻辑里如果用的是os.path.join(模块目录, WEB_DIRECTORY)当第二个参数是绝对路径时join会直接返回第二个参数丢掉前面的模块目录。所以传绝对路径是能工作的。不同版本的实现可能有差异装完看一眼浏览器开发者工具里/extensions/下的资源能不能 200比看代码可靠得多。shim 方案还有个额外好处它保留了原目录名所以工作流里已经保存的节点标识、前端扩展路径全部不变迁移过程用户无感知。4.3 方案二.pth 文件把路径加进 sys.path某些版本或某些管理器扩展支持读取custom_nodes下的.pth文件把里面的路径追加进sys.path。写一行路径就能让site-packages下的目录被扫描到/home/user/.venv/lib/python3.10/site-packages/vhs_plugin这个方案看起来最省事但我不太推荐原因有两个。一是.pth会把整个路径加进sys.path等于把那个目录当成一个顶层模块池里面所有可导入的东西都会被暴露理论上可能和别的插件发生名字冲突。二是它依赖 ComfyUI 版本或第三方管理器的支持换个环境可能就不生效可移植性差。如果你确定环境版本、且不想维护 shim 文件用这个也行但要写死绝对路径机器一换就失效。4.4 方案三干脆放弃 site-packages装到自定义目录再软链第三条路是折中pip install --target把包装到一个你自己指定的目录然后在custom_nodes下建软链接指过去。这在 Linux 上很自然Windows 上要么开开发者模式要么用目录联接。pip install dist/*.whl --no-deps --target /opt/comfy-plugins ln -s /opt/comfy-plugins/vhs_plugin /path/to/ComfyUI/custom_nodes/comfyui-videohelpersuite这个方案的本质是让插件目录物理上就在custom_nodes下所以不需要 shimWEB_DIRECTORY的相对路径也天然成立。比起 shim它少了一层间接调试时__file__指向清晰。缺点是跨平台支持不一致而且软链在某些备份工具、同步工具下会被展开成实体拷贝导致后续更新不同步。三种方案我列个对照表你可以按自己的约束挑方案前端资源路径跨平台维护成本适用场景shim 重导出需改绝对路径好低一个文件通用推荐.pth注入天然正确好低但依赖版本支持环境固定、图省事--target 软链天然正确一般中Linux 服务器4.5 还有一层容易漏的server 路由与主程序模块的导入依赖前面三个方案解决的问题是节点能不能被扫到。但 VideoHelperSuite 这类插件还有另一件事它在server.py里会向 ComfyUI 的 Web 服务注册一些 HTTP 路由用于前端查视频信息、取缩略图之类的。这些注册代码在模块被 import 时执行靠的是from server import PromptServer然后往PromptServer.instance.routes上挂。server是 ComfyUI 根目录下的模块插件能 import 到它是因为 ComfyUI 启动时工作目录就在根目录sys.path里包含当前目录。装进site-packages之后这个前提依然成立所以正常情况没问题。但如果你用--base-directory之类的方式改变工作目录或者从别的地方以模块方式启动import server就会失败表现为插件加载时报ModuleNotFoundError: No module named server。同样的问题也会出现在folder_paths上。这类对主程序内部模块的依赖是插件与宿主之间的隐式契约打包时无法通过dependencies表达因为 ComfyUI 本身不是个可安装的包。我的处理方式是不处理但要知道它的存在。一旦日志里出现这两个名字的ModuleNotFoundError先去看启动方式和工作目录而不是怀疑打包配错了。5. 从构建成功到节点不出现完整排障链路实录5.1 症状一构建成功导入报 ModuleNotFoundError第一次构建出来的 wheel 装进隔离环境import vhs_plugin直接抛ModuleNotFoundError: No module named vhs_plugin.videohelpersuite。构建过程没有任何报错这个失败来得很突然。排查顺序是这样的。先解压 wheel 看文件清单发现vhs_plugin/下只有__init__.py和web/、video_formats/几个目录videohelpersuite/整个不见了。这就定位到packages配置——我只写了[vhs_plugin]子包没列。setuptools 不会自动把子目录当成包除非用find自动发现。补上vhs_plugin.videohelpersuite之后重新构建问题消失。顺带踩了第二个变种某个子目录里没有__init__.py原仓库里也没写setuptools 认不出来它是包。补一个空的__init__.py就解决了。这个文件对运行没影响但对打包是必需的。还有一个变种值得提前说如果原代码里用的是绝对导入from videohelpersuite.nodes import X而你把子包搬进了vhs_plugin/下面那么即便文件打包进去了import 时也会报找不到videohelpersuite因为顶层没有这个名字了。改成from .videohelpersuite.nodes import X即可。这个错误和上面的文件缺失长得一模一样但根因完全不同——一个是文件没进去一个是进去了但路径找错。区分方法是解压 wheel 看文件在不在在就是导入路径问题不在就是packages配置问题。5.2 症状二节点列表里少了一大半第二次构建装进去import通过了len(NODE_CLASS_MAPPINGS)打印出来只有三个而原插件应该有十几个。这说明入口文件在组装映射表时某些导入失败了但失败被吞掉了。去看入口文件的写法果然每个节点类的导入都包在try/except里失败就print一句然后跳过。这种写法在开发期很常见为了让插件在缺少某个可选依赖时还能部分工作但对打包后的调试极不友好——错误被降级成了日志。排查办法是把except里的异常栈临时打出来或者干脆在隔离环境里手动 import 每一个子模块逐个确认。我这次的原因是videohelpersuite/utils.py里有一个顶层import cv2而隔离虚拟环境里我没装opencv。装上之后节点数量恢复正常。这个经历带给我的经验是--no-deps装完之后的验证必须区分包结构问题和依赖缺失问题。前者是打包的事后者是环境的事。用try/except静默跳过导入的插件会把后者伪装成前者让你在打包配置上白费功夫。我的做法是临时把隔离环境的依赖装齐哪怕用pip install opencv-python装个大的确认节点数量对得上再回头检查打包是否完整。5.3 症状三装完之后别的插件坏了这个问题最凶险因为它不是不工作而是把原来好的搞坏了。换到主环境装 wheel 的时候我一开始没加--no-depspip开始解析依赖把环境里的numpy从 1.26 升到了 2.x结果另一个依赖旧版 numpy ABI 的插件直接崩了报的是_ARRAY_API not found那类错误。根因很清楚wheel 的dependencies里写了numpy哪怕没锁版本pip就会认为它需要确保这个依赖被满足于是拿环境里现有的版本去比对不满足就换。而pip的满足标准只看着它自己解析出来的那棵树完全不知道 ComfyUI 主环境里其他东西对 numpy 有什么要求。修复方案是分两步。第一步把numpy、opencv-python、torch这些重型依赖从dependencies里全部删掉只留轻量的纯 Python 包。第二步安装时强制--no-deps把依赖管理的责任完全交给环境预装流程。pip install dist/comfyui_videohelpersuite_pack-1.7.0-py3-none-any.whl --no-deps代价是装完必须手动确认依赖齐全。我的做法是写一个检查脚本把插件真正 import 到的第三方包列出来逐个importlib.util.find_spec检查缺哪个补哪个。这比让pip乱改环境安全得多。这个教训值多少钱那天为了把 numpy 降回去、再确认其他插件恢复花了一个多小时。从那以后任何进主环境的轮子我都先--no-deps --dry-run看一眼它想动什么。5.4 症状四后端正常前端一片空白节点出现在列表里、能连、能执行但原本该有的视频预览、帧数滑块全没了浏览器控制台里能看到/extensions/下某个 js 请求 404。这个症状指向WEB_DIRECTORY。两个可能。一是 shim 里我图省事写了相对路径./web指向了 shim 目录下的空文件夹——改成绝对路径就对了。二是package_data的通配符没覆盖到web/js/这一层文件压根没进 wheel——解压确认果然web/下面只有一个文件。这两个原因的表现完全一样都得靠解压看文件在不在来区分。还有个更隐蔽的第三可能文件名大小写。前端资源在某些系统上对大小写敏感代码里引用的是VideoPreview.js实际文件是videoPreview.jsLinux 上就 404Windows 上没事。这类问题只在特定机器上复现特别容易误判成打包丢文件。我现在的习惯是构建完在解压出来的目录里跑一遍大小写比对虽然土但有效。6. 这套方案值不值得长期用边界与取舍6.1 明确适合 wheel 化的三类场景走完一遍之后我对适用边界有了比较清楚的判断。第一类是多机同步尤其是团队里几台机器要跑同一套工作流。一个 wheel 加一个 shim 文件新机器上五分钟配好而且版本号写在文件名里谁装的是哪版一目了然。第二类是离线交付把插件 wheel 和依赖 wheel 一起--find-links装过程可重复、可脚本化。第三类是需要回滚的环境pip install 版本号比切 commit 再重新装依赖干净太多尤其是依赖也会跟着变的时候。反过来说有三种情况我会劝人别折腾。一是插件还在频繁改动你自己在改代码那每次改完都要重新构建、重装、重启远不如直接在custom_nodes里改文件来得快。二是插件依赖 ComfyUI 主程序的内部 API 且该 API 变动频繁打包成 wheel 会让你产生这是个稳定依赖的错觉实际上每次主程序升级都可能要跟着改。三是环境里只有一个 ComfyUI、只装三五个插件——git clone加手动装依赖的总成本可能比搭这套打包流程还低。6.2 依赖声明策略上我做的第二次修正第一版我把requirements.txt原样搬进了dependencies结果就是 5.3 节那场事故。第二版全删只留Pillow和psutil安全了但带来新问题换台机器装的时候因为没有任何依赖约束缺了imageio也能装上跑起来才在运行时炸。最终我采用的是分两档的写法在dependencies里只声明缺了必然不能 import的轻量包其余重型包在安装后用一个显式的检查清单核对。检查清单不写在pyproject.toml里而是单独一个文本文件放在交付包里配上一条pip install -r runtime-requirements.txt的人工步骤。这样pip不会因为解析这个 wheel 而动手改环境同时人也不会漏装。# 交付包目录结构 dist/ ├── comfyui_videohelpersuite_pack-1.7.0-py3-none-any.whl ├── runtime-requirements.txt # 重型依赖人工确认后安装 └── install.md # 安装步骤与 shim 文件内容这个结构的另一个好处是审计友好。别人拿到这个目录打开install.md就知道要做什么不需要去猜哪个依赖是必须的、哪个是可选的。我在某次交接时深切体会到这一点——当时的交付物只有一堆 whl接手的人花了半天才搞清楚哪些是插件本体、哪些是依赖。6.3 一个更彻底的思路私有索引加锁文件如果环境数量继续增长手动拷 wheel 也会变成负担。这时候更彻底的做法是搭一个内网的文件索引就是个静态目录pip支持--find-links指向本地或内网路径把所有插件 wheel 和依赖 wheel 都放进去然后在每台机器上维护一份constraints.txt锁死版本。pip install comfyui-videohelpersuite-pack1.7.0 \ --find-links /mnt/package-repo \ --constraint constraints.txt \ --no-deps这个组合的威力在于--find-links解决了从哪拿--constraint解决了拿哪个版本--no-deps保证了不动别的。三件事各司其职出问题时定位也清楚——装不上就是仓库里没这个版本版本不对就是约束文件写错了环境被改坏基本不可能发生。我没有一步做到位是从手动拷 whl到拷一个目录再到指一个内网路径慢慢演进的。如果你现在的规模还在个位数机器停在第二步就够了别为了架构而架构。这套东西的价值只有在重复劳动量足够大时才显现。6.4 几个我反复验证过的小细节最后分享几处实测下来容易忽略但影响不小的点。shim 目录的名字要和原插件名一致这样前端扩展的 URL 路径不变已经保存的工作流不需要任何调整换成别的名字虽然节点还能用但前端资源路径会变某些硬编码了路径的自定义脚本会失效。每次改完打包配置都要清掉build/和*.egg-infosetuptools 有缓存行为不清的话可能拿旧的文件清单去构建表现为明明改了package_data但 wheel 里还是老样子。另外装完 wheel 之后记得把custom_nodes下原来那个git clone的目录删掉或者改名加.disabled后缀。两个同名目录同时存在时加载器可能两个都扫到NODE_CLASS_MAPPINGS里的键会冲突表现为节点重复或者某个节点的行为诡异。我第一次迁移时就忘了这茬排查了半天以为是打包打重了。还有个更省事的验证手段在 ComfyUI 启动日志里搜插件目录名如果能看到加载记录说明扫描到了完全搜不到就回去检查目录位置和 shim 里的导入语句。日志比节点列表诚实得多节点列表会被try/except骗日志不会。我个人在实际操作中的体会是这条路最大的价值不在装得快而在装得可复现。wheel 文件本身就是一份带着版本号的快照谁装的、装的是哪版、装完长什么样全都能对上。至于省下来的那点敲命令的时间其实是最不重要的部分。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。