Skip to Content
Ⅳ · 扩展与边界13 · V1 / V3 节点协议

13 · V1 / V3 节点协议

节点不是“任意有 execute 的 Python 类”。它是一份同时被前端、验证器、调度器、缓存器和调用适配器读取的协议。V1 用一组类属性约定协议;V3 用 typed Schema、ComfyNode 与 NodeOutput 把同样语义结构化。

一个最小 V1 节点

class AddNumbers: @classmethod def INPUT_TYPES(cls): return { "required": { "a": ("FLOAT", {"default": 0.0}), "b": ("FLOAT", {"default": 0.0}), } } RETURN_TYPES = ("FLOAT",) RETURN_NAMES = ("sum",) FUNCTION = "add" CATEGORY = "examples/math" def add(self, a, b): return (a + b,)

模块再导出:

NODE_CLASS_MAPPINGS = {"ExampleAddNumbers": AddNumbers} NODE_DISPLAY_NAME_MAPPINGS = {"ExampleAddNumbers": "Add Numbers"}

节点类本身不注册;模块映射才给它全局 node ID。

用核心节点校准协议

CLIPTextEncode 是典型 V1 节点:

  • text 是 multiline STRING widget;
  • clip 是 CLIP 连接输入;
  • 返回 CONDITIONING;
  • FUNCTION = "encode";
  • category、description、tooltip、search aliases 服务 UI;
  • encode() 只做 tokenize + encode。
nodes.pyL56-77一个完整而克制的 V1 节点。

它没有自己解析 Prompt、查缓存或发送 WebSocket。运行时基础设施越强,业务节点越薄。

V1 属性速查

属性 / 方法消费者语义
INPUT_TYPES()前端、验证器、执行器required / optional / hidden 输入 schema
RETURN_TYPES前端、链接验证、结果合并每个输出 slot 的类型
RETURN_NAMES前端输出显示名
FUNCTION执行器实例上真正调用的方法名
CATEGORY前端Add Node 菜单路径
DESCRIPTION / tooltips前端帮助信息
OUTPUT_NODE验证器、调度器是否作为反向求值根
INPUT_IS_LIST调用适配器是否一次接收整列表
OUTPUT_IS_LIST结果合并哪些 slot 已是列表,应展平
VALIDATE_INPUTS验证器领域自定义验证
IS_CHANGED缓存外部状态 fingerprint
check_lazy_status调度器当前真正需要哪些懒输入
NOT_IDEMPOTENT缓存节点 ID 是否必须进入签名
HAS_INTERMEDIATE_OUTPUTUI/cache中间 UI 是否应在刷新后重放

属性名多为大写,因为 V1 主要通过反射和类级常量建立协议。

输入 schema 的三组

{ "required": {...}, "optional": {...}, "hidden": {...} }

required

Prompt 必须提供。既可以是 widget 常量,也可以由上游链接提供。

optional

缺失时不阻止验证。函数签名应提供默认值或能在未传参数时工作。

hidden

前端通常不显示连接口,由运行时注入。常见值:

hidden 类型用途
PROMPT原始执行 Prompt
EXTRA_PNGINFO可写入图片的附加元数据
UNIQUE_ID当前节点身份
DYNPROMPT动态图查询能力
auth / API context受控的远端服务上下文

隐藏值是运行时 capability,不是让节点从全局单例随便取数据的借口。只声明真正需要的值,有利于测试与安全审计。

输入类型与 UI 选项

基本声明:

"steps": ("INT", { "default": 20, "min": 1, "max": 10000, "step": 1, "tooltip": "Sampling steps" })

字符串还可声明 multiline、dynamic prompts;浮点可声明 round;组合输入直接把第一项写为选项列表。前端使用这些信息生成 widget,后端验证器复用 min/max/choices。

UI 选项不是纯装饰:它们构成执行协议的一部分。改变 min/max 或返回类型可能让旧 Workflow 不再合法。

输入类型是名义类型

IMAGE、MASK、MODEL、CLIP 等是字符串/枚举化标识。执行器不序列化它们,只在链接验证时对比;实际 Python 值可以是 tensor、dict、Patcher 或领域对象。

典型运行时形状:

类型常见 Python 表示
IMAGE[B,H,W,C] float tensor
MASK[B,H,W] tensor
LATENT至少含 samples 的 dict
CONDITIONING条件 tensor 与 metadata 的列表结构
MODELModelPatcher
CLIP文本编码器包装器
VAEVAE 包装对象

这些 shape 是约定,不由字符串类型系统完全证明。节点实现仍要保留合理的运行时检查。

输出必须是 tuple 语义

即使只有一个 output,V1 普通结果也应返回 (value,)。执行器按 RETURN_TYPES slot 合并列表调用结果。

V1 也可返回 dict:

{ "result": (value,), "ui": {"text": ["done"]}, "expand": {...} }

result 供下游节点,ui 供客户端显示,expand 供运行时增图。三者生命周期不同,执行器负责归一化。

输出节点与中间输出

OUTPUT_NODE = True

使节点成为执行根。SaveImage、PreviewImage 等通常如此。它不必有数据输出;副作用本身就是需求。

HAS_INTERMEDIATE_OUTPUT = True

中间 UI 输出可被缓存并在页面刷新后重发,但节点不会因此自动成为执行根。只有位于真实输出依赖路径上才运行。

两者解决不同问题:一个决定“需不需要算”,一个决定“UI 是否值得重放”。

节点对象生命周期

执行器通过 objects cache 获取实例;没有则 class_def() 并缓存。结果 cache 命中时可能根本不实例化/调用节点。cache 模式关闭或签名变化时实例也可能重建。

因此:

  • 不要在 __init__ 里做不可逆业务副作用;
  • 不要把 correctness 依赖于实例永远不销毁;
  • 需要影响输出的内部/外部状态必须进入 fingerprint;
  • 大模型不要藏在私有字段绕过 ModelPatcher 与资源管理。

V3:同一语义的类型化表达

V3 节点继承 io.ComfyNode,实现两个 classmethod:

class AddNumbers(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: return io.Schema( node_id="ExampleAddNumbersV3", display_name="Add Numbers", category="examples/math", inputs=[ io.Float.Input("a", default=0.0), io.Float.Input("b", default=0.0), ], outputs=[io.Float.Output(display_name="sum")], ) @classmethod def execute(cls, a: float, b: float) -> io.NodeOutput: return io.NodeOutput(a + b)
comfy_api/latest/_io.pyL2233-2305ComfyNode 的 schema/execute 扩展点与 NodeOutput。

模块提供 extension:

class ExampleExtension(ComfyExtension): async def get_node_list(self): return [AddNumbers] async def comfy_entrypoint(): return ExampleExtension()

V3 Schema

comfy_api/latest/_io.pyL1643-1745Schema 字段、验证与 finalize。

核心字段:

字段V1 对应 / 新语义
node_idNODE_CLASS_MAPPINGS 的 key
display_nameNODE_DISPLAY_NAME_MAPPINGS
categoryCATEGORY
inputs / outputs / hiddenINPUT_TYPES / RETURN_TYPES
is_input_listINPUT_IS_LIST
is_output_nodeOUTPUT_NODE
not_idempotentNOT_IDEMPOTENT
enable_expand显式声明 NodeOutput 可带 expand
has_intermediate_outputHAS_INTERMEDIATE_OUTPUT
is_api_node / price_badge远端 API / 价格展示语义
accept_all_inputs接收 schema 外动态 kwargs
deprecated / experimental / dev_only生命周期与可见性标识

Schema.validate() 检查 input/output ID 唯一性,并调用每个 typed IO 自身验证。finalize() 注入派生 hidden 信息、默认 output IDs 等,再投影成旧执行器理解的类属性。

typed Input / Output

V3 提供 io.String、io.Int、io.Float、io.Image、io.Model 等构造器,而不是手拼 tuple。它的价值不只是编辑器补全:

  • input ID 与 display name 分开;
  • option 字段拥有明确类型;
  • 嵌套输入可保留结构;
  • dynamic input 可在运行时 finalization;
  • 输出默认 ID 与 list 语义统一;
  • Schema 能在注册时主动验证。

V3 的实际样例

ColorToRGBInt 用一个 io.Color.Input,输出 Int、Color、Float,并返回 io.NodeOutput(rgb_int, color, alpha)。Extension 的 get_node_list() 返回该类。

comfy_extras/nodes_color.pyL1-55小而完整的 V3 node + extension + entrypoint。

这个例子还展示错误应该在最接近领域输入处给出:颜色格式不对就抛具体 ValueError,而不是让后续十六进制解析产生晦涩异常。

V1 与 V3 如何共存

加载器把 V3 Schema finalize 后映射到 NODE_CLASS_MAPPINGS。执行器判断 class 是否为 _ComfyNodeInternal:

  • V1:实例化对象,getattr(obj, FUNCTION);
  • V3:准备 class clone、锁定 method、重建嵌套输入,再调 classmethod;
  • 两者的验证、列表映射、lazy、async、缓存和输出解析最终共享。

所以 V3 不是另一套 scheduler;它是更强的声明层,通过 compatibility projection 使用同一个运行时。

V1 / V3 对照

需求V1V3
声明节点 IDmodule mapping keySchema.node_id
声明输入tuple dicttyped Input objects
执行方法FUNCTION 指向实例方法execute classmethod
返回tuple / dictNodeOutput
自定义验证VALIDATE_INPUTSvalidate_inputs
cache fingerprintIS_CHANGEDfingerprint_inputs
lazycheck_lazy_status同名 classmethod
extension 初始化module import side effectComfyExtension.on_load
动态子图dict expandNodeOutput(expand=...)

幂等性是协议,不是实现细节

一个“写文件”节点即使相同输入得到相同字节,也有可观察副作用。V3 not_idempotent=True / V1 NOT_IDEMPOTENT=True 让 node ID 进入 cache signature,防止不同节点被语义合并。

但同一 ID、同一输入在下一 Prompt 是否应该再次执行,还要看输出节点和 fingerprint 设计。节点作者需要明确:是在缓存一个纯函数,还是在编排一次动作。

高级能力组合规则

  • async execute 可以与 list mapping 组合,每个 index 得到一个 Task;
  • lazy status 同样经过 list mapping;
  • expand 应显式启用,并让 GraphBuilder 生成唯一 ID;
  • blocker 会按输出 slot 传播;
  • UI 输出进入 CacheEntry,可在 cache hit 时重放;
  • dynamic inputs 在 V3 调用前重建嵌套结构。

高级能力不是互斥开关,但组合越多,越需要针对 PENDING 恢复、缓存和错误写测试。

节点设计检查表

  1. node ID 是否带项目命名空间,且准备长期稳定?
  2. 输入能否精确区分 required / optional / hidden?
  3. 默认值、范围、tooltip 和 choices 是否同时服务 UI 与验证?
  4. 输出 tuple/NodeOutput 是否与声明 slot 数一致?
  5. 函数是否对 list mapping 与 batch tensor 的区别有清楚假设?
  6. 外部文件/远端状态是否进入 fingerprint?
  7. 副作用是否声明 not idempotent,并支持幂等重试?
  8. 重量级资源是否交给统一 model/resource manager?
  9. 异常消息能否指向用户可修改的输入?
  10. 是否有 V1/V3 协议级单元测试,而非只手点 UI?

可迁移的设计方法

  • 插件协议同时服务发现、验证、执行、缓存和 UI,不能只设计函数签名。
  • 新协议通过投影兼容旧 runtime,可渐进迁移而不复制调度器。
  • typed schema 把错误前移到注册期;运行时仍保留防守性检查。
  • 节点副作用和外部依赖必须成为显式协议字段。
  • 普通扩展保持薄,把列表、异步、缓存和传输复杂度收拢到统一适配层。
Last updated on