mcpField
C
comfyui-node-advanced

comfyui-node-advanced

github.com/jtydhr88开发308137 次安装2 周前更新
代码仓库

关于 comfyui-node-advanced

面向 ComfyUI V3 自定义节点开发者的进阶模式指南,介绍动态输入、类型约束、异步执行和节点扩展等设计方式。适合需要构建灵活节点与工作流逻辑的开发者。

主要功能

  1. 01MatchType 泛型连接
  2. 02MultiType 多类型输入
  3. 03Autogrow 动态扩展输入
  4. 04DynamicCombo 与 DynamicSlot
  5. 05惰性输入及循环节点
  6. 06节点扩展与替换

适用场景

  1. 01创建输入输出类型联动的节点
  2. 02构建可增长的多槽输入
  3. 03按选项或连接显示子输入
  4. 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 辅助工具

评论

支持 Markdown