1. 先搞清楚 ComfyUI 是怎么加载插件的,再谈 wheel
把 ComfyUI 的插件构建成 wheel 文件并安装,听起来只是把pip install换个姿势,真动手之后你会发现,难点根本不在打包那几条命令,而在于 ComfyUI 对插件的发现机制和 Python 的包安装机制之间存在一道很明确的缝隙。这篇笔记就是我围绕ComfyUI-VideoHelperSuite(后面简称 VHS)做的一次探索:能不能把插件从custom_nodes目录里彻底搬出来,变成一个标准的分发单元,用 wheel 的方式装进环境。结论我先放在前面——能,而且效果比预期好,但需要额外一层入口衔接,不能直接丢一个 wheel 就不管了。
先说说为什么我会起这个念头。我在几台不同配置的机器上维护同一套 ComfyUI 工作流,这些机器的角色不太一样:有的是带显卡的工作站,跑视频合成,有的是只有 CPU 的小主机,只用来整理素材和调试参数,还有一台属于某实验室的共享设备,谁都能登进去改点东西。每次 ComfyUI 本体一更新,我那几个插件目录就开始出各种奇怪的状态——有人顺手在目录里改了行代码,有人拉了新分支,有人装依赖的时候用的是系统 Python,结果节点加载报ModuleNotFoundError。视频合成这类插件的依赖又比较重,牵涉到图像处理、视频编码、数值计算好几个方向,一旦版本错位,排查成本极高。
我真正想要的,是一套“可复现、可回滚、可离线分发”的插件管理方式。wheel 恰好是 Python 生态里最标准的那个答案:版本号写死、依赖写死、一个文件就是一份完整快照、装和卸都干净。但 ComfyUI 的插件机制并不是围绕“已安装的 Python 包”设计的,它是围绕“目录里有__init__.py”设计的。这两者之间的落差,就是这篇笔记要拆解的全部内容。
1.1 一条 import 链决定了插件的生死
ComfyUI 启动时会扫描custom_nodes目录,把里面每个子目录当作一个待加载模块。它做的事情大致是:遍历目录项,跳过以.disabled结尾的、跳过__pycache__和各种隐藏文件,然后对剩下的目录尝试用importlib加载其中的__init__.py。加载成功后,ComfyUI 会从这个模块对象上取两个约定俗成的属性:NODE_CLASS_MAPPINGS和NODE_DISPLAY_NAME_MAPPINGS。前者是节点内部名到节点类的字典,后者是内部名到界面上显示名字的字典。这两个字典缺一个,节点要么不出现,要么出现但名字是裸的内部标识。
custom_nodes目录里写入一个子目录,并在其中放置__init__.py,插件才能被可见。这个过程中,ComfyUI 完全不关心插件的代码到底是从哪来的——它只认“在这个目录下、能被 import、能吐出那两个字典”这三个条件。也正是这一点,给了 wheel 方案可乘之机:wheel 负责把代码和依赖稳稳地装进 Python 环境,剩下的事就是在这个目录里放一个足够薄的入口文件,把NODE_CLASS_MAPPINGS从 site-packages 里转出来。
这里还有一个容易被忽略的细节:ComfyUI 加载插件用的是动态模块加载,模块名是根据目录名生成的,而不是包的真实名字。所以你安装的 pip 包名、内部 import 名、custom_nodes目录名,这三者完全可以不一致。理解这一点,后面写入口文件的时候就不会被相对导入和绝对导入绕晕。很多人第一次尝试把插件挪进 site-packages,然后就卡在节点消失上,本质上就是没意识到“ComfyUI 只扫目录,不扫已安装包”这条硬规则。
1.2 直接 clone 到 custom_nodes 到底有什么不好
我得先承认,多数情况下 clone 到custom_nodes是最省事的做法,尤其是你只想跟进上游最新代码的时候。但省事的代价会在时间轴上慢慢浮现。第一是版本漂移:上游仓库的主干是流动的,今天能用的提交,两周后可能因为 ComfyUI 本体换了某个接口而报错,而你手上那套工作流并不想跟着一起折腾。第二是依赖污染:插件的requirements.txt里通常写得很宽松,pip install -r一执行,很容易把环境里已有的某个库升到不兼容的版本,视频类插件尤其危险,因为它经常和数值计算库、图像处理库共享依赖。
第三是卸载不干净。想禁用某个插件,标准做法是把目录名加个.disabled后缀,代码还在硬盘上,依赖还留在环境里,久而久之环境越来越臃肿,最后没人说得清哪一行是哪个插件留下的。第四是多机同步困难:你在工作站上验证通过的组合,搬到另一台机器上要重新 clone、重新装依赖,而 clone 下来的代码已经不是同一个快照了,出了问题只能凭记忆对比。
还有一类更麻烦的场景——共享设备。别人进来改了插件目录里的代码,甚至改了依赖,你完全无从得知。等你下次跑同样的工作流发现结果不一样,回头一查,代码早就变了。这类问题用目录方式管理几乎无解,但用版本号固定的 wheel 就天然免疫:装的是哪个版本的包,pip list里写得明明白白,谁想改就得重新装一遍,改动本身就留了痕。
1.3 wheel 能解决什么,解决不了什么
先把话说清楚,避免期望过高。wheel 能解决的:代码和依赖的版本确定性问题、离线分发问题、批量部署问题、干净卸载问题、多人环境的一致性验证问题。这些恰好是团队协作和多机维护里最疼的部分。
wheel 解决不了的:它不会让 ComfyUI 自动发现新装的包,入口还是得靠custom_nodes里的一个文件来衔接;它不会帮你处理 ComfyUI 本体接口变化带来的兼容性问题,插件代码该适配的还是得适配;它也不能阻止别人在你的环境里手动改东西,只能保证“按包安装的那份是干净的”。另外,如果插件本身带有前端静态资源、需要在浏览器端注册扩展,路径处理还要额外做一层适配,这个坑我在第三章详细说。
所以这次探索真正有价值的问题不是“能不能打成 wheel”,而是“打完 wheel 之后,怎样让它和 ComfyUI 的目录式加载机制平滑对接”。想明白这句话,剩下的都是工程细节。
2. 打包之前先做体检:这个插件适不适合做成 wheel
不是所有插件都值得做成 wheel。我见过有人花一整个下午给一个只有两百行的单节点插件写打包配置,然后发现上游一周后重构了三次。投入产出比这件事得先算清楚。我的判断维度有三个:插件是否被多台机器复用、依赖是否复杂、上游是否足够活跃以至于你不得不锁版本。三个里占两个以上,就值得动手。
2.1 插件分三类,难度天差地别
第一类:单文件、无外部依赖、只做数值变换或简单包装的节点。这类插件甚至不需要 wheel,把目录复制过去就行,打包纯属过度工程。
第二类:多模块、依赖若干纯 Python 库、带少量前端资源的插件。这类是 wheel 化收益最高的区间,因为依赖管理是它们的主要痛点,而前端资源只要路径处理对了就没问题。
第三类:依赖带二进制扩展的库、需要编译、或者依赖特定硬件后端(比如某些加速库)的插件。这类打包本身不难,难在目标机器上一装上就装,环境不匹配照样跑不起来,必须配合平台的 wheel 标签和依赖白名单一起考虑。
VHS 属于典型的第二类偏第三类:它核心是纯 Python 的多模块工程,但依赖里有带二进制的东西,还有一类依赖在不同平台上表现差异明显,得先逐一确认。
2.2 拆开 VideoHelperSuite 看依赖画像
我把 VHS 的依赖按“能不能随便装”分成三档来看,这个分档方法对绝大多数插件都通用:
| 依赖类型 | 典型代表 | 打包处理策略 | 风险点 |
|---|---|---|---|
| 纯 Python 通用库 | 图像处理主库、数值计算库 | 正常写进依赖声明 | 版本区间过宽容易顶掉环境里的版本 |
| 带二进制扩展的库 | 视频编解码封装、图像处理加速版 | 写进依赖但必须限定下限,安装时优先复用 | 不同平台装出来的东西不一样,无图形界面机器要选精简版 |
| 环境级依赖 | 特定推理框架、特定 Python 版本 | 不要写进依赖声明,改为运行时检查 | 一旦被 pip 顺带升级,整台机器的模型推理全废 |
VHS 的依赖主要是这几块:负责视频容器读写和帧编解码的库、负责图像处理的库、负责数值计算的库,以及一个把可执行文件打包进 wheel 的编码工具封装。这个组合有个好处——它们基本都是成熟库,纯安装层面不会有编译地狱。真正的隐患集中在那块“编码工具封装”上:它内部塞了一个独立的可执行文件,体积不小,安装时会解压到缓存目录。这意味着你打出来的 wheel 不该把这个库本身塞进去,而应该声明依赖让 pip 自己去装,否则你的 wheel 会膨胀到几十兆,还失去了跨平台的灵活性。
还有一个细节:图像处理库有多个发行变体,标准版带图形界面依赖,无头版不带。跑在服务器上的 ComfyUI 通常只需要无头版,但如果你的环境里已经装了标准版,pip 装 wheel 时可能会因为“已满足依赖”而不做任何事,也可能试图装第二个变体导致两个包互相覆盖同名模块——这是实际折腾过的人才会遇到的坑,后面第五章细说。
2.3 一张决策表:什么时候值得动手
我把过去几个月的判断经验整理成一张表,遇到新插件先过一遍,能省掉很多无用功:
| 判断项 | 满足时倾向于 wheel | 满足时倾向于继续 clone |
|---|---|---|
| 部署机器数量 | 3 台以上,且配置不一致 | 就一台,自己随便折腾 |
| 依赖复杂度 | 涉及编码、图像、数值等多个方向 | 只依赖标准库或一两个轻量库 |
| 上游活跃度 | 主干变动频繁,你需要固定某个可用点 | 上游长期稳定,或你就是维护者 |
| 交付对象 | 要交给别人用,对方不懂 Python 环境 | 只有自己用 |
| 网络条件 | 目标机器不能方便地访问外部资源 | 随时能拉代码装依赖 |
VHS 在这张表上几乎全中:多机部署、依赖偏重、上游活跃、还要交付给不熟悉环境的人。所以它是个合适的试验对象。如果你手上的插件只命中一项,我的建议是先别折腾,clone 加个.disabled就够用了。
3. 把插件改造成可打包的工程结构
直接对custom_nodes里的目录执行构建是行不通的。目录式插件的结构是“目录即模块”,而 wheel 要求的是“包即模块”,两者在顶层布局上差了一层。改造的核心思路只有一句话:把插件真正的代码收进一个带__init__.py的子包里,让顶层目录只承担打包元数据的职责。
3.1 目录重排与包名规范
改造前是扁平结构,所有模块文件直接躺在插件根目录下。改造后我把它变成了这样:
ComfyUI-VideoHelperSuite/ ├── pyproject.toml ├── README.md ├── LICENSE └── videohelperuite_src/ # 包目录,名字自定 ├── __init__.py # 对外暴露 NODE_CLASS_MAPPINGS ├── nodes.py ├── utils/ │ └── ... └── web/ # 前端静态资源 └── ...这里有两个命名上的取舍值得说。包名我刻意避开了和上游仓库同名的写法,因为包名一旦和某个已安装的库撞车,pip 的依赖解析会出现很别扭的行为,而 ComfyUI 又不管这些,它只看custom_nodes里的目录名。内部导入名保持简短,方便入口文件里写from 包名 import NODE_CLASS_MAPPINGS。另外,包目录里必须有__init__.py,这一点和目录式插件是一样的,因为它负责把散落在各个子模块里的节点类汇总成两个字典,是整个插件对外唯一的接口面。
原目录里的文件不需要改动内容,只需要把模块间的相对导入从“同级模块”改成“包内模块”。这一步用编辑器批量替换很快,但要注意不能无脑替换,比如from utils import xxx这种写法在包内应该变成from .utils import xxx,而from .foo import bar这种本来就对的不要动。我建议先小批量改,跑一次导入测试再继续,改完一次性启动往往会一次冒出十几个ModuleNotFoundError,反而难定位。
3.2 pyproject.toml 的关键字段逐条拆
打包元数据我用的是pyproject.toml,不再用setup.py,理由很简单:新工具链默认支持,不需要额外依赖,可读性也好。核心字段就那几个,但每个都有容易写错的地方。
[build-system] requires = ["setuptools>=64", "wheel"] build-backend = "setuptools.build_meta" [project] name = "vhs-nodes-wheel" # 分发名,和 ComfyUI 目录名无关 version = "1.0.0" description = "Video helper nodes for ComfyUI, packaged as wheel" requires-python = ">=3.9" dependencies = [ "imageio-ffmpeg>=0.4.9", "av>=10.0.0", "opencv-python-headless>=4.6.0", "numpy>=1.21,<2.0", "Pillow>=9.0.0", ] [tool.setuptools.packages.find] include = ["videohelperuite_src*"] [tool.setuptools.package-data] videohelperuite_src = ["web/**/*"]几个点单独强调。name是 pip 看到的分发名,include里的是实际导入的包名,两者可以完全不同,别被搞混。requires-python一定要写,ComfyUI 的 Python 版本在不同整合包里差别不小,写一个下限能避免装到不该装的环境里。dependencies里每个都要带上下限,尤其是数值计算库,我只写了上界不给下界或者反过来都不行——下界保证功能可用,上界防止被拖进不兼容的大版本。
package-data那一行是前端资源能不能生效的关键。默认情况下 setuptools 只把.py文件打进 wheel,web目录下的 JS 和 CSS 会被静默丢弃,装完之后节点能出来但界面上的按钮全没了。这个坑我踩过一次,表现是节点功能正常、界面扩展不加载,浏览器控制台报 404,查了半天才发现是打包时文件没进去。
提示:构建前先跑一次
python -m build --wheel然后在解压目录里翻一遍,确认web目录确实在包里,比装完再排查省事得多。
3.3 别把 torch 写进 install_requires
这条必须单独成节,因为它是整个流程里最容易把环境搞废的操作。ComfyUI 的推理框架版本和显卡驱动、CUDA 版本是强绑定的,那个包体积巨大,装一次要很久,而且不同整合包用的构建渠道还不一样。如果你的 wheel 依赖声明里写了它,或者写了某个会把它作为传递依赖拉进来的库,pip 在解析依赖时可能做两件事:顺手升级到一个“满足约束的”新版本,或者干脆下载一个和当前环境不匹配的构建。
我做过一次实验:在一台已经跑通的环境里,装一个声称“依赖推理框架”的 wheel,pip 输出的日志里出现了“已满足”的判断,看起来什么都没变,但实际上它把一个底层库换了版本,结果原本正常的模型加载开始报错。恢复的办法只有一个——按原来的版本重装。从那以后我定了一条规矩:
推理框架、CUDA 相关包、以及任何体积超过百兆的依赖,一律不写进dependencies,改为在插件的__init__.py里做运行时检查。检查逻辑很简单:尝试 import,失败就打印一条明确的提示,告诉用户需要自行安装,然后让模块加载失败但不至于把整个 ComfyUI 拖崩。这样分发出去的 wheel 是“依赖环境”的,而不是“接管环境”的。
同时安装时我固定加--no-deps,把依赖安装和使用完全拆成两步:先由我确认依赖清单,再手工装依赖,最后装 wheel。多一步操作,换来的是环境可控。
3.4 静态资源和 WEB_DIRECTORY 的处理
VHS 有前端扩展,需要在浏览器端注册。ComfyUI 的约定是在模块上暴露一个WEB_DIRECTORY属性,值是相对模块文件所在目录的路径,服务端会把那个目录挂成一个可访问的静态路径。目录式插件里,模块文件就在插件根目录,WEB_DIRECTORY = "web"指向根目录下的web,一切正常。
打包之后这个相对路径就失效了。原因很直接:入口文件在custom_nodes里,而 web 目录跑到了 site-packages 深处,相对路径指过去是空的。我试过三种写法,最后落在第三种:
第一种,在入口文件里写WEB_DIRECTORY = "../../site-packages/xxx/web"。能用,但路径依赖安装位置,换个环境就崩,直接放弃。
第二种,把 web 目录复制一份到custom_nodes的插件目录里。能用,但破坏了“一个 wheel 就是完整快照”的原则,还要多维护一份文件。
第三种,入口文件里用绝对路径。ComfyUI 处理这个属性时会做路径拼接,而拼接函数遇到绝对路径会直接采用后者,所以给绝对路径是安全的:
import os import videohelperuite_src WEB_DIRECTORY = os.path.join( os.path.dirname(videohelperuite_src.__file__), "web" )实测下来这条路最稳,目录位置怎么变都能自己算出来。唯一的代价是入口文件里多两行 import,但反正入口文件本来就要 import 包。
4. 构建、验证、安装:完整流程走一遍
前面都是准备,这一章是真正动手的部分。我按实际执行顺序记录,包含我在每一步的确认动作,你照着重现基本不会偏。
4.1 构建环境与构建命令
构建环境要和运行环境分开,这是基本纪律。我通常建一个干净的一次性虚拟环境来构建,避免构建过程把开发机上的杂七杂八带进 wheel。
python -m venv build-env source build-env/bin/activate # Windows 用 build-env\Scripts\activate python -m pip install --upgrade pip build cd ComfyUI-VideoHelperSuite python -m build --wheelpython -m build会先在一个隔离环境里装好构建依赖,再执行打包,产物落在dist/下,文件名形如vhs_nodes_wheel-1.0.0-py3-none-any.whl。看到py3-none-any这个标签就说明打出来的是纯 Python 包,任何平台都能装,这是我们想要的结果。如果你的包里有编译产物,标签会变成带平台和 Python 版本的三段式,那种包就只能装在同构环境里,分发成本高得多。
如果构建时报package-data相关警告,或者提示某个包找不到,八成是packages.find的include写错了。这个字段支持通配,但只匹配包目录(有__init__.py的目录),不会匹配普通文件夹。web 这种没有__init__.py的目录必须靠package-data才能进去。
4.2 装之前先拆开 wheel 看看
这一步很多人跳过,但它是所有排查的地基。wheel 本质就是一个 zip,看一眼文件清单只要几秒:
python -m zipfile -l dist/vhs_nodes_wheel-1.0.0-py3-none-any.whl我要确认三件事:包目录下所有.py都在;web目录下的 JS 和 CSS 都在;dist-info/METADATA里的依赖列表和预期一致。第三项尤其重要,因为依赖声明写错的话,问题会延后到安装阶段才暴露,而且报错信息往往指向别处。翻 METADATA 还能顺便看一眼Requires-Python有没有生效。
4.3 装进 ComfyUI 的 Python 环境
关键点:必须装进 ComfyUI 实际使用的那个 Python,不是你的系统 Python,不是开发机的虚拟环境。双系统、整合包、容器部署这三种情况各有各的坑。
整合包通常自带一个嵌入式 Python,路径形如python_embeded/python.exe,包管理命令要显式指向它:
python_embeded/python.exe -m pip install --no-deps /path/to/vhs_nodes_wheel-1.0.0-py3-none-any.whl容器部署要先docker exec进去,找到跑主程序的那个解释器。我见过最隐蔽的一种情况:容器里装了两个 Python,主程序用一个,shell 默认的是另一个,pip install装半天,装到了不会被执行的那个里面,节点死活不出来。
安装完成后立刻验证:
python -c "import videohelperuite_src; print(videohelperuite_src.__file__)"能打印出 site-packages 下的路径就说明装对了位置。如果提示找不到模块,那一定是环境选错,回去重新确认解释器路径。
4.4 写一个薄壳节点目录把入口接上
这是整个方案的核心。前面说过 ComfyUI 只扫custom_nodes目录,所以需要在里面放一个极薄的入口,把装好的包“接”出来。目录结构:
custom_nodes/ └── ComfyUI-VideoHelperSuite/ └── __init__.py__init__.py内容大致长这样:
import os from videohelperuite_src import ( NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, ) import videohelperuite_src WEB_DIRECTORY = os.path.join( os.path.dirname(videohelperuite_src.__file__), "web" ) __all__ = [ "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY", ]就这么几行。它的职责只有一个:被 ComfyUI 导入,然后把两个字典和静态目录路径交出去。所有实际逻辑都在 wheel 里,薄壳里不写任何业务代码。这样升级插件只需要换 wheel,入口文件一次写好后基本不用再动。
我试过另一条路:用 entry_points 声明一个入口,指望 ComfyUI 或者某个管理组件能识别并自动挂载。试下来是不行的,因为 ComfyUI 并没有定义这套插件发现协议,它不读站点元数据,只认目录。所以 entry_points 方案在当前版本里走不通,只能退回薄壳。这个结论我特意记下来,因为网上偶尔能看到类似的说法,实际验证一遍最省心。
注意:薄壳目录名决定了 ComfyUI 内部生成的模块名,如果同一个环境里同时存在目录版插件和薄壳版,模块名可能撞车,表现是节点重复、名字前缀异常、或者随机一个版本生效。切换到 wheel 方案后,务必把原来的目录版删掉或者加
.disabled后缀。
4.5 版本锁定与离线批量分发
多机部署时我会把以下几样东西打成一个压缩包:wheel 文件、依赖清单文件(用pip freeze精确导出关键依赖的版本)、薄壳目录、一份几十行的部署说明。目标机器上解压,先按清单装依赖,再装 wheel,最后把薄壳目录放到custom_nodes。整个过程不需要访问外部代码仓库,适合网络受限的环境。
版本号我习惯带一点信息,比如在1.0.0后面加构建序号,或者直接把上游的提交短哈希塞进版本号的后缀里。这样在pip list里看到的名字就能对上你打包时的那个代码状态,排查时省去大量回忆工作。
python -m pip freeze | grep -iE "av|imageio|opencv|numpy|pillow" > deps.lock5. 踩坑实录:节点不显示、依赖打架、ffmpeg 找不到
这一章是我这轮探索里真正花时间的地方。问题都不难,难在定位——报错信息经常指向完全无关的地方。
5.1 节点列表里看不到 VHS 的排查路径
节点不显示是最常见的症状,原因却有七八种。我形成一个固定的排查顺序,从便宜到贵,基本能在十分钟内定位。
第一步,看启动日志里的加载记录。ComfyUI 在加载custom_nodes时会对每个目录打印一条信息,成功的话会有导入成功的迹象,失败会带上异常类型。这一步能区分“没被扫到”和“扫到了但导入失败”。
第二步,确认目录和文件。目录有没有拼错、是不是被加上了.disabled、__init__.py是不是真的存在、文件名大小写有没有问题(在某些文件系统上大小写敏感,__Init__.py不算数)。
第三步,手动导入。进入正确环境,执行python -c "import videohelperuite_src",看能不能成功。能成功说明包本身没问题,问题在薄壳;失败说明包或依赖有问题。
第四步,模拟 ComfyUI 的加载方式,直接执行薄壳的__init__.py:
python -c "exec(open('custom_nodes/ComfyUI-VideoHelperSuite/__init__.py').read()); print(list(NODE_CLASS_MAPPINGS)[:5])"能打印出节点名就说明入口文件没问题,那就该怀疑前端缓存——节点其实加载了,但浏览器拿着老的前端清单,界面上看不到。清缓存强刷一次往往就恢复了。
我遇到过一次特别隐蔽的:前三个步骤全部通过,日志干净,导入正常,节点名也打印出来了,但界面就是没有。最后发现是包名和custom_nodes里另一个插件内部的某个模块名撞了,ComfyUI 的动态加载把其中一个缓存住了,第二个加载时拿到的其实是第一个的模块对象。改成唯一性更强的包名后解决。这类问题没有通用解法,只能靠“名字尽量独特”来预防。
5.2 依赖冲突把 torch 顶掉的现场
前面在 3.3 提过推理框架不能写进依赖,这里说一次真实翻车。我在一台机器上装 wheel 时没加--no-deps,依赖声明里有一个数值计算库的上界写得过宽,pip 解析时选择了一个新版本,那个版本要求另一个底层库升级,而底层库升级又牵动了推理框架。整个过程没有任何报错,安装“成功”,重启后模型加载全部失败,报的是内存分配相关的错误,看起来完全不像版本问题。
定位靠的是对比:把出问题前后的pip freeze拉出来做个 diff,一眼就能看到几个不该动的包被换了版本。
pip freeze > after.txt diff before.txt after.txt所以我现在的标准动作是:装之前一定先pip freeze > before.txt,装完再导一次,对比差异,确认只有预期内的包发生变化。这个习惯看起来笨,但救命。
5.3 ffmpeg 相关报错的三层定位法
视频类插件的报错里,编码工具相关问题占了大头。我的定位方法分三层,从下往上排。
第一层,可执行文件到底有没有。编码工具封装通常会自带一个静态二进制,位置在包目录内部。如果这个库没装好或者装了个不完整的版本,合成视频时就会报“找不到可执行文件”。验证方式是直接 import 那个库,调用它提供的获取路径的函数,看返回的路径是否存在、是否有执行权限。
第二层,用了哪个。有些环境里同时存在系统安装的编码工具和包内自带的,插件优先用哪个、环境变量有没有干扰,都会影响结果。我一般显式把路径固定住,不让它自己猜。
第三层,编解码器支持。能启动不代表能编码,某些构建里缺某个编码器,输出时会报“编码器不存在”。这种情况要么换一个容器格式,要么换一个可用的编码器,实测对比一下哪个更稳。
图像处理库那个坑也顺带说一下:无头版和标准版同名模块,装了两个就会互相覆盖。判断方法是在环境里打印模块文件路径,看它指向哪个包目录。如果发现两个都在,卸掉一个,统一用无头版。
5.4 问题速查表
| 症状 | 大概率原因 | 处理动作 |
|---|---|---|
| 启动日志里完全没有该插件 | 目录名错、带.disabled、放错custom_nodes | 核对路径和目录名,去掉后缀 |
| 日志有该目录但报导入异常 | 包没装进当前解释器、依赖缺失 | 用主程序解释器验证 import,按清单补依赖 |
| 导入正常、界面无节点 | 前端缓存、WEB_DIRECTORY路径失效 | 强刷缓存,检查静态目录是否可访问 |
| 节点出现但功能菜单空白 | 前端资源没打进 wheel | 核对package-data,重打重装 |
| 合成视频时报找不到编码器 | 可执行文件缺失或编码器不支持 | 三层定位,固定路径或换容器格式 |
| 安装后模型加载异常 | 依赖解析顺带升级了底层库 | 对比pip freeze,回滚被改动的包 |
| 卸载后节点还在 | 薄壳目录未删除 | 删除custom_nodes下的入口目录 |
6. 我的取舍:什么场景上 wheel,什么场景老实 clone
折腾完这一轮,我的实际做法是分场景的,不是一刀切。
需要交付给别人用的、跑在多台配置不一致机器上的、依赖偏重的插件,我全部走 wheel 加薄壳方案。VHS 就是第一个转过去的,转完之后最直观的变化是:新机器部署从“半小时的试错”变成了“十分钟的照抄”,出问题时有明确的版本号可以对,而不是靠回忆“当时装的是哪个提交”。
自己一个人用、随时要改代码、要跟上游同步的插件,我仍然用目录方式,最多加个本地 git 分支管理。改了代码立刻重启就能验证,这种反馈速度是 wheel 给不了的。每次改完打个 wheel 再装一遍,纯属自我折磨。
还有一类我干脆不碰:上游一周重构三次的那种。这种插件的接口还在剧烈变化,任何打包工作都是白做,等它稳定下来再说。
一点后续可以做的事:如果 ComfyUI 的插件发现机制将来支持读取已安装包的元数据,薄壳这一层就能去掉,整个方案会变得非常干净。在那之前,薄壳目录那几行代码就是我愿意付的成本——它把“插件代码”和“插件加载”这两件事彻底解耦了,而解耦带来的可控性,远比省下的那几行代码值钱。