ComfyUI 生产环境中的 100 个自定义节点:它们做什么,以及如何交付
作者: Anastasiia Butova, ComfyUI 与扩散模型工程师,贝尔格莱德 · 发布于 2026-09-23
译自英文版: English
我用 16 个包写了 100 个自定义 ComfyUI 节点。这个数字是 2026 年 9 月 15 日根据每个包的注册代码统计出来的,并不是凭记忆。它们大多属于三类之一:把模型包装成可以在图中运行的形式,图必须做出的某个决策,或者现有的包都无法按我们流水线所需方式完成的图像操作。下面给出每一类的真实例子,然后是通常会被略过的部分:节点如何进入生产镜像,以及错误的节点被部署到服务器上时会发生什么。
这个数字从何而来
一个脚本读取每个包的 Python 源代码,收集 ComfyUI 接受的三种注册形式中的节点 id。……被两个包同时注册;如果有任何源文件无法读取,脚本会以退出码 2 退出,并且不发布这个数字。最大的包是一个有 32 个节点的通用包(路由、门控、蒙版、噪声、颜色、接触表),一个 SAM 3 宝石包有 20 个,一个分块与频率包有 19 个。另外十个小包各有一到三个节点,大多是单模型的包装器。完整表格在 /comfyui/ 上。
这些仓库是私有的,其中有几个是在客户和雇主的工作中写成的(最大的那个仓库的描述里仍写着 "by Glam AI"),因此我描述它们,而不链接。在这次统计之前,网站上写的是"100+ 节点,开源"。这个数字经得起核对;"开源"则经不起。
模型,包装成在图中运行的形式
NAFNet、SwinIR 和 HYPIR 负责修复,SAM 3 切分汽车零件和宝石,DINOv3 测量汽车并进行分类,YOLO26 负责检测。作为节点,它们与生成在同一个任务中、在同一块 GPU 上运行,不需要绕道去另一个服务。
包装器中调用模型是容易的部分。其余的工作是让它在图的作者从未见过的硬件上正常运行。SwinIR 节点有若干 GPU 配置档案,用来设置分块大小和批次;而工作流 JSON 中保存的是其作者选择的任何配置,比如 H200(141 GB)。在 RunPod 无服务器上,调度器分配的是端点池中现有的任何硬件,从几种 H100 变体到 RTX PRO 6000 Blackwell。配置不对就意味着显存不足错误、重试,以及任务超过 30 分钟的超时。从 1.6.0 版本开始,节点会自己检测显卡,并忽略那个设置。
它过去还会在显存不足时回退到 CPU,而一个分块在 CPU 上大约需要 40 秒,整个任务就要花上数小时。我在 1.5.8 版本中移除了这个回退:现在节点会带着显存摘要大声失败。一次没有人注意到的缓慢成功,比一个会被人修好的错误更糟。
图必须做出的决策
ComfyUI 的图里没有 if。我们的图通过下面的方式分支。一个路由节点把真实图像放到它所选的输出上,而在其他每个输出上放一张 1×1 的黑色占位图。每个分支的开头都有一个门控:
def run(self, image):
from comfy_execution.graph_utils import ExecutionBlocker
if not torch.is_tensor(image):
print(f"[HappyinImageGate] NOT a tensor → BLOCK")
return (ExecutionBlocker(None),)
h, w = image.shape[1], image.shape[2]
if h <= 1 or w <= 1:
print(f"[HappyinImageGate] {w}x{h} → BLOCK")
return (ExecutionBlocker(None),)
print(f"[HappyinImageGate] {w}x{h} → PASS")
return (image,)
ExecutionBlocker 告诉 ComfyUI 不要运行它下游的任何东西,因此未被选中的分支不消耗任何 GPU。在每个分支的末端,一个带有惰性输入(lazy input)的占位节点,要么请求该分支的结果,要么在其分支未被选中时直接传递 1×1 图像而不去请求;然后一个合并节点交出第一张真实图像。如果没有惰性输入,阻断器就会传到合并节点,并阻断其后的一切。
其中三个路由器:
- NSFW 与质量退化。 一个 NSFW 标记,加上根据像素指标得出的质量判定(来自 DCT 的 JPEG 块效应、色彩条带、色度噪声、振铃和模糊),以及一个基于 CLIP 的审美评分。四个图像输出,从
nsfw_bad_quality到safe_good_quality,因此质量退化的帧可以进入放大分支,而干净的帧则进入较轻的分支。 - 特写。 人脸覆盖率用两种方式测量:MediaPipe 人脸皮肤分割,以及人脸检测器的框;信号较强者胜出。
- Florence 提示词路由。 在一个珠宝修图工作流中,Florence-2 为画面生成描述文字;如果描述中提到了宝石,就使用宝石的提示词。
Florence 路由器是我从中学到最多的一个。在 30 个真实客户画面上,Florence 在所有 18 个有宝石的画面中都说出了宝石,而宝石分支却只被选中了 17 次。三个原因全部出在匹配上,没有一个出在模型上。复数:diamonds 不匹配 \bdiamond\b。否定:“There are 0 gemstones”中含有这个名词。形状:“diamond-shaped design”描述的是图案,而不是宝石。显而易见的修复,即加一个可选的 s,反而更糟,它在全部 30 个画面上都触发了,因为被否定的句子里同样含有这个名词。顺序很重要:先去掉否定,再去掉形状用法,然后再带着复数形式做匹配。而且 "cut" 不能放进形状列表,因为 "brilliant-cut stone" 指的正是一颗真实的宝石。
在同一组 22 条描述文字上,旧的匹配答对了 13 条,第一个拟议修复答对 17 条,最终版本答对 22 条。我在任何路由器中都会保留的部分是:匹配逻辑被移出节点,变成一个普通函数,并在保存的描述文字上测试,既不需要 Florence,也不需要 GPU。正如提交信息所说,"回归现在只需要一秒,而不是一次图运行"。
现有包不以我们的方式做的图像操作
第三类是普通的图像处理,现有的节点虽然做的事情接近,但并非流水线所需要的:
- 噪声。 一个估计器(在 64×64 小块中最平滑的十分之一上计算拉普拉斯算子加 MAD,再减去一个小的下限,使干净的图像不会被读成有噪声),以及一个颗粒匹配器,把参考图像的颗粒搬到目标图像上。
- 对齐。 带有形变回退的光流对齐,然后做频率混合,使同一帧的两个版本在混合之前对齐。
- 尺寸。 一个潜空间尺寸吸附器:把一帧拉伸到所选模型表中最接近的尺寸,每条边都是 16 的倍数,而不是裁剪它。
- 分块。 针对 6K–10K 像素产品照片的分块切分与合并以及频率分解;分块带来的接缝见 单独的文章。
- 汽车。 用于 mashinki,一个节点会倾斜背景底图,以匹配驾驶舱摄像头的侧倾。
节点如何进入生产镜像
对于 OIS.Gold,生产镜像从一个内置的包集合开始:21 个包,其中 14 个公开、7 个私有,拷贝进来时不带它们的 .git 文件夹。构建过程会把该集合的提交写入镜像中的 .vendor-sha,因此镜像可以说明它是从哪个集合构建的。更新是有意为之的:刷新各个包,针对新状态运行工作流,提交,然后重新构建。对于内置的包,别人主分支上的改动不可能在一夜之间进入生产。
这个集合是在 4 月 23 日整理出来的,起因是一次损失。四天前,一台 GPU 主机被终止,而它的容器快照保存了外层镜像,却没有保存自定义节点。其中一个节点,也就是上面的 Florence 路由器,随之丢失。我于 4 月 27 日根据生产工作流 JSON 中记录的输入和输出把它重新做了出来。
内置规则有一个例外,我宁愿直接说明,而不是把它说得圆滑。仍在积极开发的包(我自己的三个,以及 Blackwell 镜像中的几个第三方包),在构建时会从各自的主分支重新克隆到集合之上,并可以用提交号固定,以便分支出现回归时使用。对于这些包来说,主分支就是生产环境,包括别人的代码。这就是为什么我最大的包在 8 月获得了 CI,而工作流文件用一行话说明了这一点:
任何合入 master 的内容都会进入下一次 worker 重建,所以 master 必须保持绿色。
测试刻意不安装 torch。它们覆盖的是不需要 Florence 也不需要 GPU 就能运行的部分,而路由器的错误恰恰就出在那里。
测试并不等于评判。新的检查点或节点改动,要在一张对比样张上与之前的版本对比来评判,其中每个格子都使用相同的冻结提示词和相同的种子。wowfaceaibot 页面 上展示了两张这样的图表,来自我自己的角色 LoRA 运行。
包在导入时就会运行
我最大的包中有两个模块从来不是节点。它们没有输入,也没有注册,只为导入时的副作用而存在:一个包装了 ComfyUI 的 PromptExecutor.execute,把每个新提示词的 PNG 和工作流 JSON 保存下来;另一个则给保存路径打补丁,用容器名作为路径前缀。在开发机上它们很有用。但在无服务器的生产环境中,临时的 worker 写下的历史没人会去读;而包在执行器外面的 try/except 可能会隐藏真正的工作流错误。5 月,它们被移到了一个独立的仓库,生产构建从不克隆该仓库;此后任何常开的副作用都放在那里。
同一个 Florence 路由器也教会了我另一半的道理。它以 trust_remote_code=True 加载 Florence-2,因此在一个全新的 worker 上,transformers 库会从 Hugging Face Hub 下载模型的 Python 代码并运行它。5 月,这段代码在已安装的 transformers 下不再能工作。路由器捕获了错误并返回了空的描述文字,而整条级联最终让 worker 交付了一张扁平的 PNG,而不是修图师使用的分层 PSD。修复针对的是我们自己的节点:加载一个本地副本,使用 local_files_only,并固定 transformers 版本。一个全局的 HF_HUB_OFFLINE=1 只维持了一天,因为它也阻断了一个社区节点的正当下载。8 月,在一台装有 transformers 5 的机器上,节点改为使用 ComfyUI-Florence2 包中的模型代码,那个包从不走远程代码的路径。第二次故障在空描述文字的掩盖下隐藏了好几天。
自定义节点就是拥有服务器权限的 Python
这就是 /comfyui/ 上安全条款背后的事故。2026 年 2 月,我参与维护的一台 H100 服务器上的 ComfyUI 容器正在用 XMRig 的一个变体挖掘门罗币;宿主机本身是干净的。
容器的 ComfyUI 端口未经认证就对互联网开放。有人向它发送了一个工作流,其中使用了 srl-nodes 中的 SRL Eval,这是一个公开的包。该节点的行为正如其文档字符串所说:它会执行你给它的任何 Python,而其源码中的一条注释还补充道 "ComfyUI isn't secure to begin with"(ComfyUI 本身就不安全)。在一台封闭的机器上,这是一种便利。而在开放的端口上,它就是一个远程 shell。
攻击者的代码改写了 /opt/ComfyUI/execution.py:干净的文件大约 1,282 行,被感染的副本则达到 1,700–2,100 行甚至更多。每次运行工作流时,被注入的代码都会暂停挖矿程序,让生成任务获得 CPU,然后再恢复它;如果二进制文件被删除,它会重新下载。这些二进制文件藏在大约 27 个目录里,名字看起来像系统服务,每次感染都不同:2 月 13、17 和 18 日,五天内共三波。删除这个包并无帮助,因为 ComfyUI-Manager 保存了一份已安装节点的快照,重启时发现 srl-nodes 缺失,便把它又克隆了回来。几分钟之内就再次被感染。
我编写了检测与清除的运行手册,附带一个检查脚本和一个清理脚本。顺序本身就是关键:
- 先从外部封锁端口,即在
DOCKER-USERiptables 链中设置规则,否则下面所做的一切几分钟内就会被抵消。除非保存,否则这些规则在 Docker 守护进程重启后不会保留。 - 杀掉容器中除 ComfyUI 本身之外的所有进程。
- 删除该包,以及所有列出它的 Manager 快照。
- 删除二进制文件,然后在已知的工具目录之外寻找任何残留的可执行文件。
- 从 git 恢复
execution.py,这是唯一可靠的方法,然后检查其行数,并确认挖矿程序的标记为零命中。 - 重启,确认该包没有回来,然后提交干净的镜像。
检查脚本会查看五件事:execution.py 中的挖矿字符串、多余的进程、意外的可执行文件、与矿池的连接,以及该包是否又回来了。运行手册也列出了不能删除的东西:那个容器中有一个不熟悉的包,实际上是合法的 GPU 显存管理组件。
为了这篇文章重读检查脚本时,我发现了一个缺陷。如果 SSH 无法连接,每个计数都会是空的,每项测试都会默认通过,脚本就会打印 RESULT: CLEAN。一个无法运行的检查必须说出来,因此它需要第三种结果。
2026 年 4 月,Censys 发布了一份分析,描述了针对暴露在互联网上的 ComfyUI 的一场攻击活动。SrlEval 在它列出的被利用节点之中;当没有安装任何易受攻击的节点时,扫描器会通过 ComfyUI-Manager 自行安装一个。同一个节点,同样由 Manager 充当安装者。
这对编写和交付节点意味着什么
- 节点的输入就是一个 API。如果一个节点根据其输入执行代码、读取路径或下载东西,那么控制图的人就控制了这些行为。
- ComfyUI 不应暴露在公共互联网上,应放在一个狭窄的服务之后;本站汽车工作室是如何运作的,见 配套文章。
- 节点在构建时到达,来源是 git 中的一个列表,固定提交号胜过跟随分支。运行时到达的代码,比如重新安装某个包的快照,或者由
trust_remote_code去获取的 Python,都是没有人有意交付的代码。 - 当生产环境从某个分支构建时,那个分支就应该有 CI,其中包含放在包内、不需要 GPU 的测试。
如果你需要一个现有包都没有覆盖的步骤所用的节点,或者需要一条可以安全交付的流水线,我的工作方式见 /comfyui/。