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。
它没有自己解析 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_OUTPUT | UI/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 的列表结构 |
MODEL | ModelPatcher |
CLIP | 文本编码器包装器 |
VAE | VAE 包装对象 |
这些 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)模块提供 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_id | NODE_CLASS_MAPPINGS 的 key |
display_name | NODE_DISPLAY_NAME_MAPPINGS |
category | CATEGORY |
inputs / outputs / hidden | INPUT_TYPES / RETURN_TYPES |
is_input_list | INPUT_IS_LIST |
is_output_node | OUTPUT_NODE |
not_idempotent | NOT_IDEMPOTENT |
enable_expand | 显式声明 NodeOutput 可带 expand |
has_intermediate_output | HAS_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() 返回该类。
这个例子还展示错误应该在最接近领域输入处给出:颜色格式不对就抛具体 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 对照
| 需求 | V1 | V3 |
|---|---|---|
| 声明节点 ID | module mapping key | Schema.node_id |
| 声明输入 | tuple dict | typed Input objects |
| 执行方法 | FUNCTION 指向实例方法 | execute classmethod |
| 返回 | tuple / dict | NodeOutput |
| 自定义验证 | VALIDATE_INPUTS | validate_inputs |
| cache fingerprint | IS_CHANGED | fingerprint_inputs |
| lazy | check_lazy_status | 同名 classmethod |
| extension 初始化 | module import side effect | ComfyExtension.on_load |
| 动态子图 | dict expand | NodeOutput(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 恢复、缓存和错误写测试。
节点设计检查表
- node ID 是否带项目命名空间,且准备长期稳定?
- 输入能否精确区分 required / optional / hidden?
- 默认值、范围、tooltip 和 choices 是否同时服务 UI 与验证?
- 输出 tuple/NodeOutput 是否与声明 slot 数一致?
- 函数是否对 list mapping 与 batch tensor 的区别有清楚假设?
- 外部文件/远端状态是否进入 fingerprint?
- 副作用是否声明 not idempotent,并支持幂等重试?
- 重量级资源是否交给统一 model/resource manager?
- 异常消息能否指向用户可修改的输入?
- 是否有 V1/V3 协议级单元测试,而非只手点 UI?
可迁移的设计方法
- 插件协议同时服务发现、验证、执行、缓存和 UI,不能只设计函数签名。
- 新协议通过投影兼容旧 runtime,可渐进迁移而不复制调度器。
- typed schema 把错误前移到注册期;运行时仍保留防守性检查。
- 节点副作用和外部依赖必须成为显式协议字段。
- 普通扩展保持薄,把列表、异步、缓存和传输复杂度收拢到统一适配层。