ComfyUI V3 架构引入了一种更有序的节点定义方式,今后的节点功能扩展只会在 V3 架构中进行。本指南将帮助你将现有的 V1 节点迁移到新的 V3 架构。
核心概念
V3 架构基于新的版本化 Comfy API,这意味着未来的架构更新都将向后兼容。comfy_api.latest 指向正在开发中的最新版本 API,而 latest 之前的版本可以认为是“稳定版”。目前版本 v0_0_2 是第一个 API 版本,之后可能还会有不兼容的更改。当它稳定后,会创建新的 v0_0_3 版本供 latest 指向。
# 使用最新的 ComfyUI API
from comfy_api.latest import ComfyExtension, io, ui
# 使用特定版本的 ComfyUI API
from comfy_api.v0_0_2 import ComfyExtension, io, ui
V1 与 V3 架构
V3 架构的主要变化包括:
- 输入和输出使用对象定义,而不是字典。
- 执行方法统一命名为 ‘execute’,并且是类方法。
- 使用
def comfy_entrypoint() 函数返回 ComfyExtension 对象来定义节点,取代 NODE_CLASS_MAPPINGS/NODE_DISPLAY_NAME_MAPPINGS
- 节点对象不保存状态 -
def __init__(self) 不会影响节点函数的暴露内容,因为所有方法都是类方法。节点类在执行前也会被清理。
V1 (旧版)
class MyNode:
@classmethod
def INPUT_TYPES(s):
return {"required": {...}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "execute"
CATEGORY = "my_category"
def execute(self, ...):
return (result,)
NODE_CLASS_MAPPINGS = {"MyNode": MyNode}
V3 (现代版)
from comfy_api.latest import ComfyExtension, io
class MyNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="MyNode",
display_name="My Node",
category="my_category",
inputs=[...],
outputs=[...]
)
@classmethod
def execute(cls, ...) -> io.NodeOutput:
return io.NodeOutput(result)
class MyExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [MyNode]
async def comfy_entrypoint() -> ComfyExtension:
return MyExtension()
迁移步骤
从 V1 迁移到 V3 在大多数情况下都很简单,主要是语法的调整。
步骤 1: 更改基类
所有 V3 节点都必须继承自 ComfyNode。支持多层继承,只要继承链的顶层有 ComfyNode 父类即可。
V1:
class Example:
def __init__(self):
pass
V3:
from comfy_api.latest import io
class Example(io.ComfyNode):
# 不需要 __init__
原来分散在代码不同位置(如字典和类属性)的节点属性(节点 ID、显示名称、类别等)现在都通过 Schema 类统一管理。
define_schema(cls) 函数需要返回一个 Schema 对象,工作方式与 V1 中的 INPUT_TYPES(s) 类似。
支持的核心输入/输出类型存储在 comfy_api/{version} 的 _io.py 文件中,默认以 io 作为命名空间。由于输入/输出现在由类定义而不是字典或字符串,自定义类型可以通过编写自己的类或使用 io 中的 Custom 辅助函数来实现。
自定义类型在下面的章节中有详细说明。
类型类包含以下属性:
class Input 用于定义输入(如 Model.Input(...))
class Output 用于定义输出(如 Model.Output(...))。注意,不是所有类型都支持作为输出。
Type 用于获取类型提示(如 Model.Type)。有些类型提示可能只是 any,未来会进一步完善。这些类型提示不会被强制执行,仅作为文档参考。
V1:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"int_field": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 64,
"display": "number"
}),
"string_field": ("STRING", {
"multiline": False,
"default": "Hello"
}),
# V1 处理任意类型
"custom_field": ("MY_CUSTOM_TYPE",),
},
"optional": {
"mask": ("MASK",)
}
}
V3:
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="Example",
display_name="Example Node",
category="examples",
description="Node description here",
inputs=[
io.Image.Input("image"),
io.Int.Input("int_field",
default=0,
min=0,
max=4096,
step=64,
display_mode=io.NumberDisplay.number
),
io.String.Input("string_field",
default="Hello",
multiline=False
),
# V3 处理任意类型
io.Custom("my_custom_type").Input("custom_input"),
io.Mask.Input("mask", optional=True)
],
outputs=[
io.Image.Output()
]
)
步骤 3: 更新执行方法
V3 中所有的执行函数都命名为 execute 并且必须是类方法。
V1:
def test(self, image, string_field, int_field):
# Process
image = 1.0 - image
return (image,)
V3:
@classmethod
def execute(cls, image, string_field, int_field) -> io.NodeOutput:
# Process
image = 1.0 - image
# Return with optional UI preview
return io.NodeOutput(image, ui=ui.PreviewImage(image, cls=cls))
步骤 4: 转换节点属性
以下是一些属性名称的对照表,更多详细信息请查看 comfy_api.latest._io 中的源代码。
| V1 属性 | V3 规范字段 | 备注 |
|---|
RETURN_TYPES | Schema 中的 outputs | 输出对象列表 |
RETURN_NAMES | 输出中的 display_name | 每个输出的显示名称 |
FUNCTION | 始终为 execute | 方法名称标准化 |
CATEGORY | Schema 中的 category | 字符串值 |
OUTPUT_NODE | Schema 中的 is_output_node | 布尔标志 |
DEPRECATED | Schema 中的 is_deprecated | 布尔标志 |
EXPERIMENTAL | Schema 中的 is_experimental | 布尔标志 |
步骤 5: 处理特殊方法
V3 支持与 V1 相同的特殊方法,但方法名改为小写或重新命名以更加清晰。使用方式保持不变。
验证 (V1 → V3)
输入验证函数重命名为 validate_inputs。
V1:
@classmethod
def VALIDATE_INPUTS(s, **kwargs):
# Validation logic
return True
V3:
@classmethod
def validate_inputs(cls, **kwargs) -> bool | str:
# Return True if valid, error string if not
if error_condition:
return "Error message"
return True
惰性求值 (V1 → V3)
check_lazy_status 函数改为类方法,其他部分保持不变。
V1:
def check_lazy_status(self, image, string_field, ...):
if condition:
return ["string_field"]
return []
V3:
@classmethod
def check_lazy_status(cls, image, string_field, ...):
if condition:
return ["string_field"]
return []
缓存控制 (V1 → V3)
缓存控制的功能与 V1 相同,但原来的函数名容易误导。
V1 的 IS_CHANGED 函数的逗辑是:如果返回值与上次执行时相同,则不重新执行节点。
因此函数 IS_CHANGED 被重命名为 fingerprint_inputs。开发者常见的错误是认为返回 True 就会让节点总是重新执行。但由于总是返回 True,反而会导致节点只执行一次然后重用缓存。
一个常见的使用场景是 LoadImage 节点。它返回所选文件的哈希值,这样文件变化时节点就会重新执行。
V1:
@classmethod
def IS_CHANGED(s, **kwargs):
return "unique_value"
V3:
@classmethod
def fingerprint_inputs(cls, **kwargs):
return "unique_value"
步骤 6: 创建扩展和入口点
不再使用字典来映射节点 ID 到节点类/显示名称,现在需要定义 ComfyExtension 类和 comfy_entrypoint 函数。
将来可能会在 ComfyExtension 中添加更多函数,通过 get_node_list 注册节点以外的其他内容。
comfy_entrypoint 可以是同步或异步函数,但 get_node_list 必须声明为异步。
V1:
NODE_CLASS_MAPPINGS = {
"Example": Example
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Example": "Example Node"
}
V3:
from comfy_api.latest import ComfyExtension
class MyExtension(ComfyExtension):
# 必须声明为异步
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
Example,
# 在这里添加更多节点
]
# 可以声明为异步或不是,两者都可以工作
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
输入类型参考
虽然在步骤 2 中已经介绍过,但这里再提供一些 V1 与 V3 类型的对照表。完整的类型声明请查看 comfy_api.latest._io。
基本类型
| V1 类型 | V3 类型 | 示例 |
|---|
"INT" | io.Int.Input() | io.Int.Input("count", default=1, min=0, max=100) |
"FLOAT" | io.Float.Input() | io.Float.Input("strength", default=1.0, min=0.0, max=10.0) |
"STRING" | io.String.Input() | io.String.Input("text", multiline=True) |
"BOOLEAN" | io.Boolean.Input() | io.Boolean.Input("enabled", default=True) |
control_after_generate
Int 和 Combo 输入支持 control_after_generate 参数,用于添加一个控制小部件,在每次生成后自动更改值。在 V1 中这是一个普通的 bool;在 V3 中你可以使用 io.ControlAfterGenerate 枚举进行显式控制。传递 True 等同于 io.ControlAfterGenerate.randomize。
| 值 | 行为 |
|---|
io.ControlAfterGenerate.fixed | 每次生成后值保持不变。 |
io.ControlAfterGenerate.increment | 每次生成后值按步长递增。 |
io.ControlAfterGenerate.decrement | 每次生成后值按步长递减。 |
io.ControlAfterGenerate.randomize | 每次生成后值随机化。 |
# 启用控制小部件(用户在 UI 中选择模式)
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFFFFFFFFFF, control_after_generate=True)
# 设置特定的默认模式
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFFFFFFFFFF,
control_after_generate=io.ControlAfterGenerate.randomize)
ComfyUI 类型
| V1 类型 | V3 类型 | 示例 |
|---|
"IMAGE" | io.Image.Input() | io.Image.Input("image", tooltip="Input image") |
"MASK" | io.Mask.Input() | io.Mask.Input("mask", optional=True) |
"LATENT" | io.Latent.Input() | io.Latent.Input("latent") |
"CONDITIONING" | io.Conditioning.Input() | io.Conditioning.Input("positive") |
"MODEL" | io.Model.Input() | io.Model.Input("model") |
"VAE" | io.VAE.Input() | io.VAE.Input("vae") |
"CLIP" | io.CLIP.Input() | io.CLIP.Input("clip") |
组合类型(下拉框/选择列表)
V3 中的组合类型需要显式定义。
V1:
"mode": (["option1", "option2", "option3"],)
V3:
io.Combo.Input("mode", options=["option1", "option2", "option3"])
Schema 参考
Schema 数据类定义了 V3 节点的所有属性。以下是所有可用字段的完整参考:
| 字段 | 类型 | 默认值 | 描述 |
|---|
node_id | str | 必需 | 节点的全局唯一 ID。自定义节点应添加前缀/后缀以避免冲突。 |
display_name | str | None | 在 UI 中显示的名称。如果未设置,则回退到 node_id。 |
category | str | "sd" | 在”添加节点”菜单中的类别(例如 "image/transform")。 |
description | str | "" | 悬停在节点上时显示的工具提示。 |
inputs | list[Input] | [] | 输入定义列表。 |
outputs | list[Output] | [] | 输出定义列表。 |
hidden | list[Hidden] | [] | 要请求的隐藏输入列表(参见隐藏输入)。 |
search_aliases | list[str] | [] | 搜索的备用名称。适用于同义词或重命名后的旧名称。 |
is_output_node | bool | False | 将节点标记为输出节点,使其及其依赖项被执行。 |
is_input_list | bool | False | 当为 True 时,无论传入多少项,所有输入都变为 list[type]。 |
is_deprecated | bool | False | 将节点标记为已弃用,提示用户寻找替代方案。 |
is_experimental | bool | False | 将节点标记为实验性,警告用户它可能会更改。 |
is_dev_only | bool | False | 除非启用开发模式,否则从搜索/菜单中隐藏节点。 |
is_api_node | bool | False | 将节点标记为 Comfy API 服务的 API 节点。 |
not_idempotent | bool | False | 当为 True 时,节点将始终重新运行,永远不会重用图中另一个相同节点的缓存输出。 |
enable_expand | bool | False | 允许 NodeOutput 包含用于节点扩展的 expand 属性。 |
accept_all_inputs | bool | False | 当为 True 时,来自 prompt 的所有输入都将作为 kwargs 传递,即使未在 schema 中定义。 |
通用输入参数
所有输入类型共享这些基本参数:
| 参数 | 类型 | 默认值 | 描述 |
|---|
id | str | 必需 | 输入的唯一标识符,用作 execute 中的 kwarg 名称。 |
display_name | str | None | 在 UI 中显示的标签。默认为 id。 |
optional | bool | False | 输入是否可选。 |
tooltip | str | None | 悬停时的工具提示文本。 |
lazy | bool | None | 标记输入为惰性求值(参见惰性求值)。 |
raw_link | bool | None | 当为 True 时,传递原始链接信息而不是解析后的值。 |
advanced | bool | None | 当为 True 时,输入隐藏在 UI 中的”高级”切换后面。 |
小部件输入(Int、Float、String、Boolean、Combo)还支持:
| 参数 | 类型 | 默认值 | 描述 |
|---|
default | 不定 | None | 小部件的默认值。 |
socketless | bool | None | 当为 True 时,隐藏输入插槽(仅小部件,无传入连接)。 |
force_input | bool | None | 当为 True 时,强制小部件显示为插槽输入。 |
高级功能
隐藏输入
隐藏输入提供对执行上下文的访问,如 prompt 元数据、节点 ID 和其他内部值。它们在 UI 中不可见。
在 V1 中,隐藏输入在 INPUT_TYPES 中声明为 "hidden" 键。在 V3 中,它们通过 Schema 上的 hidden 参数声明,其值通过 cls.hidden 访问。
V1:
@classmethod
def INPUT_TYPES(s):
return {
"required": {...},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
def execute(self, unique_id, prompt, extra_pnginfo, ...):
# 隐藏值作为普通参数传递
print(unique_id)
V3:
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="MyNode",
inputs=[...],
hidden=[io.Hidden.unique_id, io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, ...) -> io.NodeOutput:
# 通过 cls.hidden 访问隐藏值
print(cls.hidden.unique_id)
print(cls.hidden.prompt)
print(cls.hidden.extra_pnginfo)
可用的隐藏值:
| Hidden 枚举 | 描述 |
|---|
io.Hidden.unique_id | 节点的唯一标识符,与客户端上的 id 匹配。 |
io.Hidden.prompt | 客户端发送的完整 prompt。 |
io.Hidden.extra_pnginfo | 复制到保存的 .png 文件元数据中的字典。 |
io.Hidden.dynprompt | 可能在执行期间变化的 DynamicPrompt 实例。 |
io.Hidden.auth_token_comfy_org | 从前端登录 ComfyOrg 账户获取的令牌。 |
io.Hidden.api_key_comfy_org | ComfyOrg 生成的 API 密钥,允许跳过前端登录。 |
某些隐藏值会根据 Schema 标志自动添加。输出节点(is_output_node=True)自动接收 prompt 和 extra_pnginfo。API 节点(is_api_node=True)自动接收认证令牌。
UI 辅助函数
V3 在 ui 模块中提供内置的 UI 辅助函数来处理常见模式,如预览和保存文件。通过 ui 参数将它们传递给 io.NodeOutput。
预览辅助函数
预览辅助函数保存临时文件并返回用于节点内显示的 UI 数据。
from comfy_api.latest import ui
# 在节点中预览图像
return io.NodeOutput(images, ui=ui.PreviewImage(images, cls=cls))
# 预览遮罩(自动转换为 3 通道以便显示)
return io.NodeOutput(mask, ui=ui.PreviewMask(mask, cls=cls))
# 预览音频
return io.NodeOutput(audio, ui=ui.PreviewAudio(audio, cls=cls))
# 预览文本
return io.NodeOutput(ui=ui.PreviewText("Some text value"))
# 预览 3D 模型
return io.NodeOutput(ui=ui.PreviewUI3D(model_file, camera_info))
保存辅助函数
保存辅助函数提供将文件保存到输出目录并正确嵌入元数据的方法。它们通常用于输出节点。
from comfy_api.latest import ui, io
# 保存图像并返回 UI 数据(最常见的模式)
return io.NodeOutput(
ui=ui.ImageSaveHelper.get_save_images_ui(
images=images,
filename_prefix=filename_prefix,
cls=cls, # 传递隐藏的 prompt/extra_pnginfo 用于元数据
)
)
# 保存动画 PNG
return io.NodeOutput(
ui=ui.ImageSaveHelper.get_save_animated_png_ui(
images=images,
filename_prefix=filename_prefix,
cls=cls,
fps=6.0,
compress_level=4,
)
)
# 保存动画 WebP
return io.NodeOutput(
ui=ui.ImageSaveHelper.get_save_animated_webp_ui(
images=images,
filename_prefix=filename_prefix,
cls=cls,
fps=6.0,
lossless=True,
quality=80,
method=4,
)
)
# 保存音频(支持 flac、mp3、opus)
return io.NodeOutput(
ui=ui.AudioSaveHelper.get_save_audio_ui(
audio=audio,
filename_prefix=filename_prefix,
cls=cls,
format="flac",
)
)
将 cls=cls 传递给保存/预览辅助函数允许它们自动在保存的文件中嵌入工作流元数据(prompt、extra_pnginfo)。确保在你的 schema 的 hidden 列表中包含 io.Hidden.prompt 和 io.Hidden.extra_pnginfo,或设置 is_output_node=True 会自动添加它们。
返回原始 UI 字典
如果你需要返回没有辅助函数的 UI 数据,可以直接传递字典:
return io.NodeOutput(ui={"images": results})
输出节点
适用于产生副作用的节点(如保存文件)。与 V1 一样,将节点标记为输出节点后,会在节点的上下文菜单中显示 run 播放按钮,允许部分执行流程图。
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="SaveNode",
inputs=[...],
outputs=[], # 不需要为空。
is_output_node=True # 标记为输出节点
)
自定义类型
可以通过编写类或使用 Custom 辅助函数来创建自定义输入/输出类型。
from comfy_api.latest import io
# 方法 1: 使用装饰器定义类
@io.comfytype(io_type="MY_CUSTOM_TYPE")
class MyCustomType:
Type = torch.Tensor # Python 类型注释
class Input(io.Input):
def __init__(self, id: str, **kwargs):
super().__init__(id, **kwargs)
class Output(io.Output):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 方法 2: 使用 Custom 辅助函数
# 为了方便起见,也可以直接使用辅助函数而不先保存到变量
MyCustomType = io.Custom("MY_CUSTOM_TYPE")
MultiType 输入
MultiType 允许一个输入接受多种类型。当节点可以通过同一个输入插槽对不同数据类型进行操作时,这很有用。
如果第一个参数(id)是 Input 类的实例而不是字符串,则该输入将用于创建具有其覆盖值的小部件。否则,输入仅为插槽。
# 仅插槽的多类型输入(无小部件)
io.MultiType.Input("input", types=[io.Image, io.Mask])
# 带有小部件回退的多类型输入(未连接时显示 String 小部件)
io.MultiType.Input(
io.String.Input("model_file", default="", multiline=False),
types=[io.File3DGLB, io.File3DGLTF, io.File3DOBJ],
tooltip="3D 模型文件或路径字符串",
)
MatchType(泛型类型匹配)
MatchType 创建类型关联的输入和输出。当用户将特定类型连接到 MatchType 输入时,所有共享相同模板的其他输入和输出会自动约束为该类型。这就是 Switch 和 Create List 等节点处理任意类型的方式。
@classmethod
def define_schema(cls):
# 创建模板 - 所有共享相同模板的输入/输出将匹配类型
template = io.MatchType.Template("switch")
return io.Schema(
node_id="SwitchNode",
display_name="Switch",
category="logic",
inputs=[
io.Boolean.Input("switch"),
io.MatchType.Input("on_false", template=template, lazy=True),
io.MatchType.Input("on_true", template=template, lazy=True),
],
outputs=[
io.MatchType.Output(template=template, display_name="output"),
],
)
你还可以限制允许的类型:
# 仅允许 Image、Mask 或 Latent 类型
template = io.MatchType.Template("input_type", allowed_types=[io.Image, io.Mask, io.Latent])
动态输入
V3 引入了动态输入类型,根据用户交互改变可用的输入。V1 中没有这些功能的等效项。
Autogrow
Autogrow 创建可变数量的输入,随着用户连接更多输入会自动增长。有两种模板类型:
TemplatePrefix 生成带有编号前缀的输入(例如 image0、image1、image2…):
@classmethod
def define_schema(cls):
autogrow_template = io.Autogrow.TemplatePrefix(
input=io.Image.Input("image"), # 每个输入的模板
prefix="image", # 生成的输入名称前缀
min=2, # 显示的最小输入数
max=50, # 允许的最大输入数
)
return io.Schema(
node_id="BatchImagesNode",
display_name="Batch Images",
category="image",
inputs=[io.Autogrow.Input("images", template=autogrow_template)],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, images: io.Autogrow.Type) -> io.NodeOutput:
# 'images' 是一个将输入名称映射到其值的字典
image_list = list(images.values())
return io.NodeOutput(batch(image_list))
TemplateNames 生成具有特定名称的输入:
template = io.Autogrow.TemplateNames(
input=io.Float.Input("float"),
names=["x", "y", "z"], # 每个输入的显式名称
min=1, # 显示的最小输入数
)
Autogrow 可以与 MatchType 结合使用来创建类型匹配输入的列表:
@classmethod
def define_schema(cls):
template_matchtype = io.MatchType.Template("type")
template_autogrow = io.Autogrow.TemplatePrefix(
input=io.MatchType.Input("input", template=template_matchtype),
prefix="input",
)
return io.Schema(
node_id="CreateList",
display_name="Create List",
category="logic",
is_input_list=True,
inputs=[io.Autogrow.Input("inputs", template=template_autogrow)],
outputs=[
io.MatchType.Output(
template=template_matchtype,
is_output_list=True,
display_name="list",
),
],
)
DynamicCombo
DynamicCombo 创建一个下拉菜单,根据选择的选项显示/隐藏不同的输入。当不同模式需要不同参数时,这很有用。
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ResizeNode",
display_name="Resize",
category="transform",
inputs=[
io.Image.Input("image"),
io.DynamicCombo.Input("resize_type", options=[
io.DynamicCombo.Option("scale by dimensions", [
io.Int.Input("width", default=512, min=0, max=8192),
io.Int.Input("height", default=512, min=0, max=8192),
]),
io.DynamicCombo.Option("scale by multiplier", [
io.Float.Input("multiplier", default=1.0, min=0.01, max=8.0),
]),
io.DynamicCombo.Option("scale to megapixels", [
io.Float.Input("megapixels", default=1.0, min=0.01, max=16.0),
]),
]),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, image, resize_type: dict) -> io.NodeOutput:
# resize_type 是一个包含所选选项键及其输入的字典
selected = resize_type["resize_type"]
if selected == "scale by dimensions":
width = resize_type["width"]
height = resize_type["height"]
# ...
elif selected == "scale by multiplier":
multiplier = resize_type["multiplier"]
# ...
DynamicCombo 选项也可以嵌套:
io.DynamicCombo.Input("combo", options=[
io.DynamicCombo.Option("option1", [io.String.Input("string")]),
io.DynamicCombo.Option("option2", [
io.DynamicCombo.Input("subcombo", options=[
io.DynamicCombo.Option("sub_opt1", [io.Float.Input("x"), io.Float.Input("y")]),
io.DynamicCombo.Option("sub_opt2", [io.Mask.Input("mask", optional=True)]),
])
]),
])
异步执行
V3 支持异步 execute 方法。对于执行 I/O 操作、API 调用或其他异步工作的节点,这很有用。只需将 execute 声明为 async:
@classmethod
async def execute(cls, prompt, **kwargs) -> io.NodeOutput:
result = await some_async_operation(prompt)
return io.NodeOutput(result)
ComfyAPI
ComfyAPI 类提供对 ComfyUI 运行时服务的访问,如进度报告和节点替换注册。导入它并创建实例:
from comfy_api.latest import ComfyAPI
api = ComfyAPI()
进度报告
从节点的 execute 方法中报告执行进度。进度条显示在 ComfyUI 界面中。这取代了 V1 中使用 comfy.utils.PROGRESS_BAR_HOOK 的模式。
from comfy_api.latest import ComfyAPI
api = ComfyAPI()
@classmethod
async def execute(cls, images, **kwargs) -> io.NodeOutput:
total = len(images)
for i, image in enumerate(images):
process(image)
await api.execution.set_progress(
value=i + 1,
max_value=total,
preview_image=image, # 可选:在进度期间显示预览
)
return io.NodeOutput(result)
set_progress 可以接受 PIL Image、ImageInput 张量或 None 作为 preview_image 参数。当从 execute 内部调用时,node_id 会自动从执行上下文中确定。
节点替换
节点替换允许将旧的/已弃用的节点映射到新节点,因此现有工作流会自动升级。在扩展的 on_load 方法中使用 ComfyAPI 注册替换。
from comfy_api.latest import ComfyAPI, ComfyExtension, io
api = ComfyAPI()
class MyExtension(ComfyExtension):
async def on_load(self) -> None:
await api.node_replacement.register(io.NodeReplace(
new_node_id="MyNewNode",
old_node_id="MyOldNode",
old_widget_ids=["param1", "param2"], # 用于位置映射的有序小部件 ID
input_mapping=[
{"new_id": "image", "old_id": "input_image"}, # 重命名输入
{"new_id": "method", "set_value": "lanczos"}, # 设置固定值
],
output_mapping=[
{"new_idx": 0, "old_idx": 0}, # 按索引映射输出
],
))
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [MyNewNode]
old_widget_ids 参数很重要:工作流 JSON 按位置索引存储小部件值,而不是按名称。此列表将这些位置索引映射到输入 ID,以便替换系统在迁移期间可以正确识别小部件值。
对于使用动态输入(如 Autogrow)的节点,在映射中使用点分路径:
input_mapping=[
{"new_id": "images.image0", "old_id": "image1"},
{"new_id": "images.image1", "old_id": "image2"},
]
扩展生命周期
ComfyExtension 类支持除 get_node_list 之外的生命周期钩子:
from comfy_api.latest import ComfyExtension, io
class MyExtension(ComfyExtension):
async def on_load(self) -> None:
"""扩展加载时调用。
用于一次性初始化:注册节点替换、
设置全局资源等。
"""
pass
async def get_node_list(self) -> list[type[io.ComfyNode]]:
"""返回此扩展提供的节点类列表。"""
return [MyNode]
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
NodeOutput
NodeOutput 类是 execute 的标准化返回值。它支持多种模式:
# 返回单个输出值
return io.NodeOutput(image)
# 返回多个输出值(顺序与 schema 中的 outputs 列表匹配)
return io.NodeOutput(width, height, batch_size)
# 仅返回 UI 数据(无输出值)
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
# 返回输出值和 UI 数据
return io.NodeOutput(image, ui=ui.PreviewImage(image, cls=cls))
# 返回 None/空(对于没有输出的节点)
return io.NodeOutput()
完整示例
这是一个包含多个节点的完整 V3 扩展文件示例:
from comfy_api.latest import ComfyExtension, io, ui
class InvertImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MyPack_InvertImage", # 添加前缀以避免冲突
display_name="Invert Image",
category="my_pack/image",
description="反转图像的颜色。",
inputs=[
io.Image.Input("image"),
],
outputs=[
io.Image.Output(display_name="inverted"),
],
)
@classmethod
def execute(cls, image) -> io.NodeOutput:
inverted = 1.0 - image
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted, cls=cls))
class SaveImage(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MyPack_SaveImage",
display_name="Save Image",
category="my_pack/image",
is_output_node=True,
inputs=[
io.Image.Input("images"),
io.String.Input("filename_prefix", default="ComfyUI"),
],
outputs=[],
)
@classmethod
def execute(cls, images, filename_prefix) -> io.NodeOutput:
return io.NodeOutput(
ui=ui.ImageSaveHelper.get_save_images_ui(
images=images,
filename_prefix=filename_prefix,
cls=cls,
)
)
class MyPackExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [InvertImage, SaveImage]
async def comfy_entrypoint() -> MyPackExtension:
return MyPackExtension()