mcpField
C
comfyui-node-migration

comfyui-node-migration

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

关于 comfyui-node-migration

将现有 ComfyUI V1 节点迁移到 V3 API 的指南技能。它通过映射表、示例和常见问题说明,将旧版节点的架构、方法、输入、输出及注册方式转换为 V3 模式。

主要功能

  1. 01将 V1 属性映射到 V3 架构字段
  2. 02把输入类型转换为 V3 类型化输入
  3. 03更新执行和验证方法
  4. 04将元组返回值改为 NodeOutput
  5. 05将节点注册迁移到 ComfyExtension
  6. 06了解 V3 行为及迁移常见问题

适用场景

  1. 01将 V1 自定义节点移植到 V3 API
  2. 02升级时转换节点输入和输出
  3. 03更新执行、验证及隐藏输入的访问方式
  4. 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 注册。

迁移清单

  1. 将基类改为 io.ComfyNode
  2. 将 INPUT_TYPES() 替换为返回 io.Schema 的 define_schema()
  3. 将执行函数重命名为 execute,并将其设为 @classmethod
  4. 将返回元组替换为 io.NodeOutput(...)
  5. 将 IS_CHANGED 替换为 fingerprint_inputs
  6. 将 VALIDATE_INPUTS 替换为 validate_inputs
  7. 将 check_lazy_status 转换为 @classmethod
  8. 将 NODE_CLASS_MAPPINGS 替换为 ComfyExtension + comfy_entrypoint()
  9. 通过 cls.hidden 而非 kwargs 访问隐藏输入
  10. 移除 __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 = Trueio.Schema(is_output_node=True)
DEPRECATED = Trueio.Schema(is_deprecated=True)
EXPERIMENTAL = Trueio.Schema(is_experimental=True)
API_NODE = Trueio.Schema(is_api_node=True)
NOT_IDEMPOTENT = Trueio.Schema(not_idempotent=True)
DESCRIPTION = "..."io.Schema(description="...")
SEARCH_ALIASES = [...]io.Schema(search_aliases=[...])
INPUT_IS_LIST = Trueio.Schema(is_input_list=True)
OUTPUT_IS_LIST = (True,)io.Image.Output(is_output_list=True)
DEV_ONLY = Trueio.Schema(is_dev_only=True)
ESSENTIALS_CATEGORY = "Basic"io.Schema(essentials_category="Basic")

输入类型映射

V1 InputV3 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)

关键注意事项

  1. 没有实例状态:V3 的 execute 是类方法。不要在 self 上存储状态。如有需要,请使用外部存储。
  2. 固定的方法名称:始终使用 execute,不能使用自定义名称。
  3. 隐藏输入的访问方式已更改:使用 cls.hidden.prompt,而不是函数参数。
  4. 返回类型已更改:使用 io.NodeOutput(val),而不是 (val,)。
  5. 可选输入:在 execute 参数中使用 =None 默认值,而不是单独的 "optional" 字典。
  6. 支持异步:V3 的 execute 可以定义为 async def execute(cls, ...)。

另请参阅

  • comfyui-node-basics - V3 节点基础知识
  • comfyui-node-packaging - 项目结构
  • comfyui-node-lifecycle - 执行生命周期差异

评论

支持 Markdown