14 · 自定义节点发现与装载
ComfyUI 的生态不是一个受限 DSL,而是把 Python 模块导入主进程。因此“节点为什么没出现”和“节点是否可信”都要从装载链分析,而不是只看类定义。
两个扩展时点
prestartup_script.py 与节点模块 import 都能执行任意 Python,只是发生时间不同。
路径来源
folder_paths.get_folder_paths("custom_nodes") 可返回一个或多个根目录,来源包括默认目录与 extra model paths 配置。加载器对每个根执行目录枚举。
候选可以是:
- 单个
.py文件; - 包含
__init__.py的目录; - 非 Python 文件会跳过;
- 名字以
.disabled结尾会跳过; __pycache__被排除。
三层启用策略
.disabled
本地显式禁用单个扩展。它基于目录/文件名,是最直观的故障隔离手段。
--disable-all-custom-nodes + whitelist
默认全部跳过,只允许白名单。适合排障或服务部署的 allowlist 模式。
Manager policy
启用 ComfyUI Manager 时,可由 manager 判断模块是否应被阻止。它在 import 前执行,能避免已知禁用扩展代码进入进程。
三层都只能决定“是否导入”,不能把已导入代码变成沙箱。
动态 import
load_custom_node() 为文件或包创建 importlib spec,构造 module,先放入 sys.modules,再 exec_module()。
为什么先放 sys.modules
包内相对导入、循环导入和 Python module identity 依赖它。代价是 import 中途失败可能留下部分初始化模块状态;错误被记录后,不能假设重试一定像新进程一样干净。
module name 规范化
目录路径中的点会替换,文件与包走不同 spec;加载成功后 LOADED_MODULE_DIRS 保存模块名到绝对目录,供资源/诊断使用。
V1 注册分支
模块若导出非空 NODE_CLASS_MAPPINGS:
- 遍历
(node_id, class); - 不允许覆盖启动前记录的核心 node IDs;
- 写入全局
NODE_CLASS_MAPPINGS; - 给 class 写
RELATIVE_PYTHON_MODULE; - 可合并
NODE_DISPLAY_NAME_MAPPINGS; - 成功后立即返回。
核心映射可见于 nodes.py 的 KSampler、CheckpointLoader、SaveImage 等注册。
ID 冲突
加载器显式保护核心 ID;两个第三方扩展之间的同名仍可能受加载顺序影响。扩展应使用稳定命名空间,如 Acme_ImageAnnotator,而不是通用 ProcessImage。
目录枚举顺序不应被当成可靠的覆盖机制。
V3 注册分支
模块没有 V1 mapping、但有 comfy_entrypoint 时:
- 检查 entrypoint callable;
- 同步或 await 调用;
- 验证返回
ComfyExtension; await extension.on_load();await extension.get_node_list();- 验证得到 list;
- 对每个 class 调
GET_SCHEMA(); - 以
schema.node_id注册,并同步 display name。
entrypoint、on_load 和 get_node_list 都允许异步,适合读取配置或轻量初始化。但启动过程会等待它们,不适合长时间远端请求。
为什么 V1 优先
源码用 if NODE_CLASS_MAPPINGS ... elif comfy_entrypoint。一个模块同时导出两者时只走 V1。迁移扩展时应明确选择入口,不要期待两套节点自动合并。
Web 扩展目录
模块可用两种方式声明前端资源:
pyproject.toml的 Comfy 配置指定 web 目录和 project name;- 传统模块常量
WEB_DIRECTORY。
存在且为目录时,路径写入 EXTENSION_WEB_DIRS,PromptServer.add_routes() 再以扩展静态路径暴露给前端。
前端 JS 与 Python 节点同属受信任扩展:它在 ComfyUI 页面 origin 执行,可访问页面上下文与后端 API。部署审核不能只扫 .py,还要看 web assets。
prestartup 脚本
主启动链在 torch/comfy runtime 之前寻找并执行 prestartup_script.py,记录耗时与失败。
适合的用途:
- 调整扩展自己的 import path;
- 检查/迁移扩展配置;
- 与 Manager 协作准备依赖;
- 做必须早于核心 import 的兼容设置。
不理想的用途:
- 每次启动无条件在线安装未锁定依赖;
- 修改全局 site-packages 而不记录;
- 吞掉失败继续让节点以半安装状态导入;
- 读取/上传用户数据。
加载失败如何降级
模块 import、V3 entrypoint 或 schema 失败时,加载器记录 traceback/warning 并返回 false。扫描完成后按耗时排序打印:
0.1 seconds: custom_nodes/fast_node
4.8 seconds (IMPORT FAILED): custom_nodes/broken_node服务通常继续启动,成功节点仍可用。这适合桌面生态,却要求 UI/诊断明确展示缺失节点;不能只把“首页打开”当健康检查。
一个节点为什么没有出现:定位树
内建 extras 与外部 custom nodes
comfy_extras 也通过模块加载机制加入大量内建功能,但其路径和发布与核心仓库绑定。外部 custom nodes 来自用户目录,信任与兼容等级不同。
API/Partner nodes 位于 comfy_api_nodes,很多依赖 api.comfy.org 或第三方服务。它们可能标记 is_api_node、价格 badge,并要求敏感 API key。关闭 API nodes 时服务还会调整 CSP/路由能力。
API key 的边界
/prompt 会把 auth_token_comfy_org、api_key_comfy_org 等敏感 extra data 与普通历史字段拆开,执行前恢复。节点应通过受支持的 hidden/context 获取,不要:
- 写进 UI output;
- 放入图片 metadata;
- 打进异常 traceback / repr;
- 作为普通 cache signature 内容;
- 传给无关第三方域名。
敏感字段分离只是减少意外传播,不代替网络访问控制和 secret rotation。
自定义节点的进程权限
一旦 import,节点可以:
- 读写进程有权访问的文件;
- 发起网络请求;
- 读取环境变量与密钥;
- 修改全局 Python 对象;
- 占用 GPU/RAM;
- 注册路由或前端脚本(视扩展机制而定)。
因此核心 schema 验证只验证 Prompt 输入,不隔离插件实现。真正需要多租户隔离时,应在 OS/container/process 边界运行不同信任级别的实例。
发布一个可靠扩展
建议最小结构:
custom_node_project/
├── __init__.py V1 mapping 或 V3 entrypoint
├── nodes/ 业务节点
├── pyproject.toml 元数据、依赖、可选 web 配置
├── web/ 可选前端扩展
├── tests/ schema、执行、cache、路径测试
├── README.md 兼容版本与模型依赖
└── requirements/lock 可复现依赖策略测试不只“能 import”
至少覆盖:
- node IDs 唯一且 schema finalize 成功;
- required/optional/hidden 与执行签名一致;
- 每个 output slot 类型和 tuple 数量正确;
- list mapping / batch 边界;
- fingerprint 在外部状态变更时变化;
- async 异常与取消不重复副作用;
- expand ID 唯一、无环、父缓存失效正确;
- 路径输入不能逃逸允许目录;
- Web assets 不泄露 token 或引入不必要远端脚本。
可迁移的设计方法
- 插件发现先做 allow/deny policy,再 import;import 后已经拥有权限。
- 核心扩展点保留 ID 命名空间,拒绝插件覆盖基础协议。
- 插件初始化失败可降级,但必须提供结构化可观测性。
- 后端插件和同源前端资源按同一信任等级审核。
- 新旧插件协议共享注册表和执行器,避免生态一分为二。