关于 comfyui-node-advanced
面向 ComfyUI V3 自定义节点开发者的进阶模式指南,介绍动态输入、类型约束、异步执行和节点扩展等设计方式。适合需要构建灵活节点与工作流逻辑的开发者。
主要功能
- 01MatchType 泛型连接
- 02MultiType 多类型输入
- 03Autogrow 动态扩展输入
- 04DynamicCombo 与 DynamicSlot
- 05惰性输入及循环节点
- 06节点扩展与替换
适用场景
- 01创建输入输出类型联动的节点
- 02构建可增长的多槽输入
- 03按选项或连接显示子输入
- 04扩展执行图或迁移旧节点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jtydhr88/ comfyui-custom-node-skills --skill comfyui-node-advanced安装后在 Claude Code 中输入/comfyui-node-advanced确认已加载。
详细介绍
ComfyUI 高级节点模式(V3)
V3 提供高级输入模式,可用于设计动态、类型安全且灵活的节点。
MatchType - 泛型类型连接
MatchType 可确保使用同一模板的输入和输出在连接时具有相同类型,类似于类型化语言中的泛型。
class PassThrough(io.ComfyNode):
@classmethod
def define_schema(cls):
# Template(template_id, allowed_types=AnyType) - optional type constraint
template = io.MatchType.Template("T")
return io.Schema(
node_id="PassThrough",
display_name="Pass Through",
category="utils",
inputs=[
io.MatchType.Input("value", template=template),
],
outputs=[
io.MatchType.Output(template=template, display_name="output"),
],
)
@classmethod
def execute(cls, value):
return io.NodeOutput(value)
用户将 IMAGE 连接到输入后,输出会自动变为 IMAGE 类型。
Switch 节点模式
Core Switch(自 v0.35 起)会让两个分支都变为可选,因此任一分支都可以保持未连接状态。未连接的可选输入不会出现在 kwargs 中;已连接但尚未求值的惰性输入则为 None。使用哨兵值来区分两者:
MISSING = object()
class Switch(io.ComfyNode):
@classmethod
def define_schema(cls):
template = io.MatchType.Template("switch")
return io.Schema(
node_id="Switch",
display_name="Switch",
category="logic",
inputs=[
io.Boolean.Input("switch"),
io.MatchType.Input("on_false", template=template, lazy=True, optional=True),
io.MatchType.Input("on_true", template=template, lazy=True, optional=True),
],
outputs=[
io.MatchType.Output(template=template, display_name="output"),
],
)
@classmethod
def check_lazy_status(cls, switch, on_false=MISSING, on_true=MISSING):
if switch and on_true is None: # connected, not yet evaluated
return ["on_true"]
if not switch and on_false is None:
return ["on_false"]
@classmethod
def execute(cls, switch, on_true=MISSING, on_false=MISSING):
selected = on_true if switch else on_false
return io.NodeOutput(None if selected is MISSING else selected)
MultiType - 接受多种类型
接受多种不同类型的单个输入:
io.MultiType.Input("data",
types=[io.Image, io.Mask, io.Latent],
optional=True,
)
Autogrow - 动态增长输入
用户连接输入时,会自动添加更多插槽。支持两种模板模式:
TemplatePrefix(编号插槽)
class ConcatImages(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ConcatImages",
display_name="Concat Images",
category="image",
inputs=[
io.Autogrow.Input("images",
template=io.Autogrow.TemplatePrefix(
input=io.Image.Input("img"), # template for each slot
prefix="image_", # slot names: image_0, image_1, ...
min=2, # minimum visible slots (default 1)
max=16, # maximum slots (default 10, hard limit 100)
),
),
],
outputs=[io.Image.Output("IMAGE")],
)
@classmethod
def execute(cls, images: io.Autogrow.Type):
# images is a dict: {"image_0": tensor, "image_1": tensor, ...}
tensors = [v for v in images.values() if v is not None]
return io.NodeOutput(torch.cat(tensors, dim=0))
TemplateNames(命名插槽)
io.Autogrow.Input("inputs",
template=io.Autogrow.TemplateNames(
input=io.Float.Input("val"),
names=["red", "green", "blue", "alpha"], # specific slot names
min=3, # first 3 are required
),
)
# Creates slots: "red" (required), "green" (required), "blue" (required), "alpha" (optional)
主要行为:
- 模板中的 Widget 输入会被强制设为仅连接(
force_input=True) - 少于
min的插槽为必需;高于min的插槽为可选 - 名称总数最多为 100 个
DynamicCombo - 条件输入
根据下拉菜单中的不同选项显示不同的子输入:
class ProcessNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ProcessNode",
display_name="Process Node",
category="processing",
is_output_node=True,
inputs=[
io.DynamicCombo.Input("mode", options=[
io.DynamicCombo.Option("resize", [
io.Int.Input("width", default=512, min=1, max=8192),
io.Int.Input("height", default=512, min=1, max=8192),
]),
io.DynamicCombo.Option("blur", [
io.Float.Input("radius", default=5.0, min=0.1, max=100.0),
]),
io.DynamicCombo.Option("sharpen", [
io.Float.Input("amount", default=1.0, min=0.0, max=10.0),
]),
]),
io.Image.Input("image"),
],
outputs=[io.Image.Output("IMAGE")],
)
@classmethod
def execute(cls, mode: io.DynamicCombo.Type, image, **kwargs):
# mode is a dict with the combo value + sub-inputs
# key for selected option matches the DynamicCombo input ID
if mode["mode"] == "resize":
width = mode["width"]
height = mode["height"]
# ... resize logic
return io.NodeOutput(image)
嵌套 DynamicCombo:
io.DynamicCombo.Input("outer", options=[
io.DynamicCombo.Option("option1", [
io.DynamicCombo.Input("inner", options=[
io.DynamicCombo.Option("sub1", [io.Float.Input("val")]),
io.DynamicCombo.Option("sub2", [io.Int.Input("count")]),
])
]),
])
DynamicSlot - 由连接触发的输入
与 DynamicCombo 类似,但子输入会在插槽建立连接时显示,而不是在选择下拉选项时显示:
io.DynamicSlot.Input(
slot=io.Image.Input("image"), # the trigger slot (widget inputs are forced to connection-only)
inputs=[ # revealed when the slot is connected
io.Float.Input("blend", default=0.5),
io.Boolean.Input("invert", default=False),
],
)
# Value type: dict containing the slot value + sub-input values
注意:
DynamicSlot已在comfy_api.latest中注册为基础设施,但尚未被任何核心节点使用;请将其视为实验性功能。
_DynamicGroup(COMFY_DYNAMICGROUP_V3,一组重复的 Widget 行,由核心 LoRA 堆栈节点使用)是内部且不稳定的:它不会从io导出;请勿从comfy_api.latest._io导入。前端支持从 frontend v1.57.0 开始提供。
动态输入中的惰性输入
Autogrow / DynamicCombo 的子输入可以设为 lazy=True(调度问题已在 v0.40 修复)。仅在 check_lazy_status 中,每个嵌套值都会以 (value, original_key) 元组的形式传入;返回 original_key 即可请求该值。execute 接收的是普通值。
io.Autogrow.Input("rows", template=io.Autogrow.TemplatePrefix(
io.Image.Input("img", lazy=True), prefix="item", min=1))
@classmethod
def check_lazy_status(cls, rows):
value, key = rows["item0"] # key == "rows.item0"
return [key] if value is None else []
循环(核心 StartLoop / EndLoop)
自 v0.36 起,核心提供 StartLoop / EndLoop(utilities/looping;简单 / For / List 模式,传递值并累积)。优先使用它们,而非自定义循环节点。对开发者有影响的事项:
- 循环体会通过节点展开按迭代次数克隆,因此循环内的节点每次迭代都会运行一次,且
cls.hidden.unique_id是展开后的 ID(显示 ID 会映射回原始节点)。 io.Schema(loop_boundary="start" | "end")用于标记循环边界,以便验证提示词(配对、循环体不可逃逸、accumulate不从循环体接收输入)。它与核心循环实现绑定;请将其视为实验性功能。
节点展开 - 子图注入
节点可以返回一个子图,在执行期间替换自身:
from comfy_execution.graph_utils import GraphBuilder
class RepeatNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RepeatNode",
display_name="Repeat KSampler",
category="sampling",
enable_expand=True,
inputs=[
io.Model.Input("model"),
io.Int.Input("repeat_count", default=2, min=1, max=10),
io.Latent.Input("latent"),
],
outputs=[io.Latent.Output("LATENT")],
)
@classmethod
def execute(cls, model, repeat_count, latent):
graph = GraphBuilder()
current_latent = latent
for i in range(repeat_count):
sampler = graph.node("KSampler",
model=model,
latent_image=current_latent,
# ... other params
)
current_latent = sampler.out(0)
return io.NodeOutput(current_latent, expand=graph.finalize())
节点展开规则:
- 在 Schema 中设置
enable_expand=True - 使用
GraphBuilder安全地构建子图 - 返回
io.NodeOutput(output_ref, expand=graph.finalize()) - 子图中的节点 ID 必须确定且唯一
- 每个子节点都会单独缓存
接受所有输入
接受 Schema 中未定义的任意输入:
class FlexibleNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="FlexibleNode",
display_name="Flexible Node",
category="utils",
accept_all_inputs=True,
inputs=[io.Combo.Input("mode", options=["a", "b"])],
outputs=[io.String.Output()],
)
@classmethod
def validate_inputs(cls, mode, **kwargs):
return True # skip validation for dynamic inputs
@classmethod
def execute(cls, mode, **kwargs):
# kwargs contains all dynamic inputs
return io.NodeOutput(str(kwargs))
阻止执行
有条件地阻止下游执行:
class GateNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="GateNode",
display_name="Gate",
category="logic",
inputs=[
io.Boolean.Input("allow"),
io.Image.Input("image"),
],
outputs=[io.Image.Output("IMAGE")],
)
@classmethod
def execute(cls, allow, image):
if not allow:
return io.NodeOutput(block_execution="Gate is closed")
return io.NodeOutput(image)
异步执行
V3 原生支持异步执行:
class AsyncNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="AsyncNode",
display_name="Async Node",
category="utils",
inputs=[io.String.Input("url")],
outputs=[io.String.Output()],
)
@classmethod
async def execute(cls, url):
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
text = await response.text()
return io.NodeOutput(text)
进度报告
报告长时间操作期间的进度:
from comfy_api.latest import ComfyAPISync # sync version; use ComfyAPI + await for async execute
class SlowNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="SlowNode",
display_name="Slow Node",
category="utils",
inputs=[io.Int.Input("steps", default=100)],
outputs=[io.String.Output()],
)
@classmethod
def execute(cls, steps):
api = ComfyAPISync()
for i in range(steps):
# ... do work ...
api.execution.set_progress(i + 1, steps)
return io.NodeOutput("done")
NodeReplace - 节点迁移
注册替换规则,使旧工作流自动迁移到新节点:
from typing_extensions import override
from comfy_api.latest import ComfyAPI, ComfyExtension, io
class MyExtension(ComfyExtension):
@override
async def on_load(self):
api = ComfyAPI()
await api.node_replacement.register(io.NodeReplace(
new_node_id="MyNewNode_v2",
old_node_id="MyOldNode",
old_widget_ids=["width", "height", "mode"], # positional widget order
input_mapping=[
{"new_id": "image_in", "old_id": "image"}, # rename input
{"new_id": "size", "set_value": 512}, # set fixed value
],
output_mapping=[
{"new_idx": 0, "old_idx": 0}, # index-based, not name-based
],
))
@override
async def get_node_list(self):
return [MyNewNodeV2]
InputMap 类型:
InputMapOldId:{"new_id": str, "old_id": str}— 将旧输入映射到新输入InputMapSetValue:{"new_id": str, "set_value": Any}— 为新输入设置固定值- 对自动增长输入使用点号表示法:
{"new_id": "images.image0", "old_id": "image1"}
OutputMap(基于索引,而非名称):
{"new_idx": int, "old_idx": int}— 将旧输出索引映射到新输出索引
old_widget_ids:必填,因为工作流 JSON 按位置而非 ID 存储控件值。此列表将位置索引映射到输入 ID,以确保正确迁移。
ComfyAPI - 运行时 API
from comfy_api.latest import ComfyAPI, ComfyAPISync
# In sync execute(): use ComfyAPISync (no await)
api = ComfyAPISync()
api.execution.set_progress(value=50, max_value=100)
api.execution.set_progress(
value=50, max_value=100,
node_id=None, # optional: defaults to current node
preview_image=pil_image, # PIL Image or ImageInput tensor
ignore_size_limit=False,
)
# In async execute(): use ComfyAPI (with await)
api = ComfyAPI()
await api.execution.set_progress(value=50, max_value=100)
# Node replacement registration (in async on_load)
await api.node_replacement.register(io.NodeReplace(...))
另请参阅
comfyui-node-basics- 节点基础知识comfyui-node-inputs- 基本输入类型comfyui-node-lifecycle- 执行生命周期和缓存comfyui-node-outputs- 输出类型和 UI 辅助工具