关于 comfyui-node-migration
将现有 ComfyUI V1 节点迁移到 V3 API 的指南技能。它通过映射表、示例和常见问题说明,将旧版节点的架构、方法、输入、输出及注册方式转换为 V3 模式。
主要功能
- 01将 V1 属性映射到 V3 架构字段
- 02把输入类型转换为 V3 类型化输入
- 03更新执行和验证方法
- 04将元组返回值改为 NodeOutput
- 05将节点注册迁移到 ComfyExtension
- 06了解 V3 行为及迁移常见问题
适用场景
- 01将 V1 自定义节点移植到 V3 API
- 02升级时转换节点输入和输出
- 03更新执行、验证及隐藏输入的访问方式
- 04迁移前检查节点注册方式的变化
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jtydhr88/ comfyui-custom-node-skills --skill comfyui-node-migration安装后在 Claude Code 中输入/comfyui-node-migration确认已加载。
详细介绍
ComfyUI V1 → V3 迁移指南
将现有的 V1 节点迁移到现代 V3 API。V3 使用类方法、类型化输入/输出和 ComfyExtension 注册。
迁移清单
- 将基类改为
io.ComfyNode - 将
INPUT_TYPES()替换为返回io.Schema的define_schema() - 将执行函数重命名为
execute,并将其设为@classmethod - 将返回元组替换为
io.NodeOutput(...) - 将
IS_CHANGED替换为fingerprint_inputs - 将
VALIDATE_INPUTS替换为validate_inputs - 将
check_lazy_status转换为@classmethod - 将
NODE_CLASS_MAPPINGS替换为ComfyExtension+comfy_entrypoint() - 通过
cls.hidden而非 kwargs 访问隐藏输入 - 移除
__init__方法(V3 中不使用实例状态)
并排对比
V1(之前)
import torch
class ImageInvertV1:
CATEGORY = "image"
FUNCTION = "invert"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
OUTPUT_TOOLTIPS = ("The inverted image",)
DESCRIPTION = "Inverts image colors"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"strength": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
}),
},
"optional": {
"mask": ("MASK",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
@classmethod
def IS_CHANGED(s, image, strength, mask=None, unique_id=None):
return strength
@classmethod
def VALIDATE_INPUTS(s, image, strength, mask=None, unique_id=None):
if strength < 0:
return "Strength must be non-negative"
return True
def invert(self, image, strength, mask=None, unique_id=None):
inverted = 1.0 - image
result = image * (1 - strength) + inverted * strength
if mask is not None:
result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
return (result,)
NODE_CLASS_MAPPINGS = {"ImageInvertV1": ImageInvertV1}
NODE_DISPLAY_NAME_MAPPINGS = {"ImageInvertV1": "Invert Image"}
V3(之后)
import torch
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class ImageInvertV3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageInvertV3",
display_name="Invert Image",
description="Inverts image colors",
category="image",
inputs=[
io.Image.Input("image"),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),
io.Mask.Input("mask", optional=True),
],
outputs=[
io.Image.Output("IMAGE", tooltip="The inverted image"),
],
hidden=[io.Hidden.unique_id],
)
@classmethod
def fingerprint_inputs(cls, image, strength, mask=None):
return strength
@classmethod
def validate_inputs(cls, image, strength, mask=None):
if strength < 0:
return "Strength must be non-negative"
return True
@classmethod
def execute(cls, image, strength, mask=None):
node_id = cls.hidden.unique_id # access hidden via cls.hidden
inverted = 1.0 - image
result = image * (1 - strength) + inverted * strength
if mask is not None:
result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
return io.NodeOutput(result)
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [ImageInvertV3]
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
属性映射
| V1 属性 | V3 对应项 |
|---|---|
CATEGORY = "image" | io.Schema(category="image") |
FUNCTION = "my_func" | 始终为 execute(固定名称) |
RETURN_TYPES = ("IMAGE",) | outputs=[io.Image.Output()] |
RETURN_NAMES = ("image",) | outputs=[io.Image.Output(display_name="image")] |
OUTPUT_TOOLTIPS = ("tip",) | outputs=[io.Image.Output(tooltip="tip")] |
OUTPUT_NODE = True | io.Schema(is_output_node=True) |
DEPRECATED = True | io.Schema(is_deprecated=True) |
EXPERIMENTAL = True | io.Schema(is_experimental=True) |
API_NODE = True | io.Schema(is_api_node=True) |
NOT_IDEMPOTENT = True | io.Schema(not_idempotent=True) |
DESCRIPTION = "..." | io.Schema(description="...") |
SEARCH_ALIASES = [...] | io.Schema(search_aliases=[...]) |
INPUT_IS_LIST = True | io.Schema(is_input_list=True) |
OUTPUT_IS_LIST = (True,) | io.Image.Output(is_output_list=True) |
DEV_ONLY = True | io.Schema(is_dev_only=True) |
ESSENTIALS_CATEGORY = "Basic" | io.Schema(essentials_category="Basic") |
输入类型映射
| V1 Input | V3 Input |
|---|---|
("IMAGE",) | io.Image.Input("id") |
("MASK",) | io.Mask.Input("id") |
("LATENT",) | io.Latent.Input("id") |
("MODEL",) | io.Model.Input("id") |
("CLIP",) | io.Clip.Input("id") |
("VAE",) | io.Vae.Input("id") |
("CONDITIONING",) | io.Conditioning.Input("id") |
("INT", {"default": 0, ...}) | io.Int.Input("id", default=0, ...) |
("FLOAT", {"default": 1.0, ...}) | io.Float.Input("id", default=1.0, ...) |
("STRING", {"multiline": True}) | io.String.Input("id", multiline=True) |
("BOOLEAN", {"default": True}) | io.Boolean.Input("id", default=True) |
(["opt1", "opt2"],) | io.Combo.Input("id", options=["opt1", "opt2"]) |
("CONTROL_NET",) | io.ControlNet.Input("id") |
("CLIP_VISION",) | io.ClipVision.Input("id") |
("CLIP_VISION_OUTPUT",) | io.ClipVisionOutput.Input("id") |
("STYLE_MODEL",) | io.StyleModel.Input("id") |
("GLIGEN",) | io.Gligen.Input("id") |
("UPSCALE_MODEL",) | io.UpscaleModel.Input("id") |
("AUDIO",) | io.Audio.Input("id") |
("VIDEO",) | io.Video.Input("id") |
("SAMPLER",) | io.Sampler.Input("id") |
("SIGMAS",) | io.Sigmas.Input("id") |
("NOISE",) | io.Noise.Input("id") |
("GUIDER",) | io.Guider.Input("id") |
("HOOKS",) | io.Hooks.Input("id") |
("LORA_MODEL",) | io.LoraModel.Input("id") |
("MESH",) | io.Mesh.Input("id") |
("VOXEL",) | io.Voxel.Input("id") |
("FILE_3D",) | io.File3DAny.Input("id") |
("FILE_3D_GLB",) | io.File3DGLB.Input("id") |
("SVG",) | io.SVG.Input("id") |
("COLOR",) | io.Color.Input("id") |
("BOUNDING_BOX",) | io.BoundingBox.Input("id") |
("CURVE",) | io.Curve.Input("id") |
("LATENT_UPSCALE_MODEL",) | io.LatentUpscaleModel.Input("id") |
("MODEL_PATCH",) | io.ModelPatch.Input("id") |
("HOOK_KEYFRAMES",) | io.HookKeyframes.Input("id") |
("AUDIO_ENCODER",) | io.AudioEncoder.Input("id") |
("AUDIO_ENCODER_OUTPUT",) | io.AudioEncoderOutput.Input("id") |
("TRACKS",) | io.Tracks.Input("id") |
("LOSS_MAP",) | io.LossMap.Input("id") |
("TIMESTEPS_RANGE",) | io.TimestepsRange.Input("id") |
("LATENT_OPERATION",) | io.LatentOperation.Input("id") |
("WEBCAM",) | io.Webcam.Input("id") |
("PHOTOMAKER",) | io.Photomaker.Input("id") |
("WAN_CAMERA_EMBEDDING",) | io.WanCameraEmbedding.Input("id") |
("LOAD_3D",) | io.Load3D.Input("id") |
("LOAD_3D_ANIMATION",) | io.Load3DAnimation.Input("id") |
("LOAD3D_CAMERA",) | io.Load3DCamera.Input("id") |
("FILE_3D_GLTF",) | io.File3DGLTF.Input("id") |
("FILE_3D_FBX",) | io.File3DFBX.Input("id") |
("FILE_3D_OBJ",) | io.File3DOBJ.Input("id") |
("FILE_3D_STL",) | io.File3DSTL.Input("id") |
("FILE_3D_USDZ",) | io.File3DUSDZ.Input("id") |
("FILE_3D_PLY",) | io.File3DPLY.Input("id") |
("FILE_3D_SPLAT",) | io.File3DSPLAT.Input("id") |
("FILE_3D_SPZ",) | io.File3DSPZ.Input("id") |
("FILE_3D_KSPLAT",) | io.File3DKSPLAT.Input("id") |
("FILE_3D_SPLAT_ANY",) | io.File3DSplatAny.Input("id") |
("FILE_3D_POINT_CLOUD_ANY",) | io.File3DPointCloudAny.Input("id") |
("SPLAT",) | io.Splat.Input("id") |
("LOAD3D_MODEL_INFO",) | io.Load3DModelInfo.Input("id") |
("BACKGROUND_REMOVAL",) | io.BackgroundRemoval.Input("id") |
("DICT",) | io.Dict.Input("id") |
("ARRAY",) | io.Array.Input("id") |
("COLORS",) | io.Colors.Input("id") |
("BOUNDING_BOXES",) | io.BoundingBoxes.Input("id") |
("RANGE",) | io.Range.Input("id") |
("HISTOGRAM",) | io.Histogram.Input("id") |
("POINT",) | io.Point.Input("id") |
("FACE_ANALYSIS",) | io.FaceAnalysis.Input("id") |
("BBOX",) | io.BBOX.Input("id") |
("SEGS",) | io.SEGS.Input("id") |
("IMAGECOMPARE",) | io.ImageCompare.Input("id") |
("LAYERS",) | io.Layers.Input("id") |
("COMPOSITOR",) | io.Compositor.Input("id") |
("CAMERA_INFO_STATE",) | io.CameraInfoState.Input("id") |
("VIDEO_EDIT",) | io.VideoEdit.Input("id", features=["trim", "crop"]) |
("RESOLUTION_PREVIEW",) | io.ResolutionPreview.Input("id") |
("*",) | io.AnyType.Input("id") or io.MultiType.Input("id", types=[...]) |
方法迁移
Execute 方法
# V1: instance method with custom name
class V1Node:
FUNCTION = "process"
def process(self, image, value):
return (result,)
# V3: classmethod named "execute", returns NodeOutput
class V3Node(io.ComfyNode):
@classmethod
def execute(cls, image, value):
return io.NodeOutput(result)
IS_CHANGED → fingerprint_inputs
# V1
@classmethod
def IS_CHANGED(s, **kwargs):
return float("NaN") # always re-execute
# V3
@classmethod
def fingerprint_inputs(cls, **kwargs):
import time
return time.time() # always re-execute
VALIDATE_INPUTS → validate_inputs
# V1
@classmethod
def VALIDATE_INPUTS(s, input_types=None, **kwargs):
return True
# V3
@classmethod
def validate_inputs(cls, input_types=None, **kwargs):
return True
check_lazy_status
# V1: instance method
def check_lazy_status(self, **kwargs):
return ["input_name"]
# V3: classmethod
@classmethod
def check_lazy_status(cls, **kwargs):
return ["input_name"]
隐藏输入
# V1: received as kwargs
def execute(self, image, unique_id=None, prompt=None):
node_id = unique_id
# V3: accessed via cls.hidden
@classmethod
def execute(cls, image):
node_id = cls.hidden.unique_id
prompt = cls.hidden.prompt
注册迁移
# V1
NODE_CLASS_MAPPINGS = {
"Node1": Node1Class,
"Node2": Node2Class,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Node1": "Node One",
"Node2": "Node Two",
}
WEB_DIRECTORY = "./js"
# V3
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
class MyExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [Node1Class, Node2Class]
@override
async def on_load(self):
# Optional: initialization logic
pass
async def comfy_entrypoint() -> MyExtension:
return MyExtension()
# WEB_DIRECTORY still works the same way for JS extensions
WEB_DIRECTORY = "./js"
输出节点迁移
# V1
class V1SaveNode:
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "save"
def save(self, images, prefix):
# ... save logic ...
return {"ui": {"images": results}}
# V3
from comfy_api.latest import io, ui
class V3SaveNode(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="V3SaveNode",
display_name="Save",
category="image",
is_output_node=True,
inputs=[
io.Image.Input("images"),
io.String.Input("prefix", default="output"),
],
outputs=[],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
)
@classmethod
def execute(cls, images, prefix):
saved = ui.ImageSaveHelper.get_save_images_ui(images, prefix, cls=cls)
return io.NodeOutput(ui=saved)
关键注意事项
- 没有实例状态:V3 的 execute 是类方法。不要在
self上存储状态。如有需要,请使用外部存储。 - 固定的方法名称:始终使用
execute,不能使用自定义名称。 - 隐藏输入的访问方式已更改:使用
cls.hidden.prompt,而不是函数参数。 - 返回类型已更改:使用
io.NodeOutput(val),而不是(val,)。 - 可选输入:在 execute 参数中使用
=None默认值,而不是单独的"optional"字典。 - 支持异步:V3 的 execute 可以定义为
async def execute(cls, ...)。
另请参阅
comfyui-node-basics- V3 节点基础知识comfyui-node-packaging- 项目结构comfyui-node-lifecycle- 执行生命周期差异
评论
支持 Markdown