feat: module control — switch a module off per project, and a module that fails stays off across runs until repaired or turned back on (Settings > Modules)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-03 02:59:57 +03:00
co-authored by Claude Sonnet 5.5
parent 79a7baa2ec
commit 1724e94c2d
22 changed files with 525 additions and 18 deletions
+19 -3
View File
@@ -33,7 +33,7 @@ import json
from comfy_api.latest import io
from ..._core import (log, patching, registry as registry_mod,
from ..._core import (control as control_mod, log, patching, registry as registry_mod,
relations as relations_mod, run as run_mod,
schema as schema_mod)
@@ -120,6 +120,11 @@ class FunPackModifierSettings(io.ComfyNode):
specs = registry_mod.current().specs
problems = []
for module_id, values in raw.items():
if module_id in control_mod.RESERVED:
said = control_mod.bad_off(values)
if said:
problems.append(said)
continue
spec = specs.get(module_id)
if spec is None:
problems.append(f"no module named {module_id!r} is installed")
@@ -152,6 +157,13 @@ class FunPackModifierSettings(io.ComfyNode):
guards_off = cls._guards_off(raw)
problems, checked = [], {}
for module_id, values in raw.items():
if module_id in control_mod.RESERVED:
said = control_mod.bad_off(values)
if said:
problems.append(said)
else:
checked[module_id] = values
continue
spec = specs.get(module_id)
if spec is None:
# Silently ignoring it is how v4 accumulated settings that meant
@@ -192,8 +204,9 @@ class FunPackLoadModifiers(io.ComfyNode):
@classmethod
def fingerprint_inputs(cls, **_):
return "|".join(f"{spec.id}={answer()}"
for spec, answer in registry_mod.current().providers(CACHE_KEY))
return "|".join([f"{spec.id}={answer()}"
for spec, answer in registry_mod.current().providers(CACHE_KEY)]
+ ["quarantine=" + control_mod.fingerprint()])
@classmethod
def execute(cls, model, settings=None) -> io.NodeOutput:
@@ -214,6 +227,7 @@ class FunPackLoadModifiers(io.ComfyNode):
offering = [spec for spec in specs if spec.provides.get(CAPABILITY)]
compatible, incompatible = traits_mod.split(offering, available)
compatible, switched = control_mod.partition(compatible, settings)
ordered, rejected = order(compatible)
patched = model.clone()
@@ -241,6 +255,7 @@ class FunPackLoadModifiers(io.ComfyNode):
"these settings are yours.")
if stripped:
notes.append(f"replaced {stripped} modifier(s) already on this model")
notes.extend(switched)
# Checked HERE, not only where the payload was made. A FUNPACK_SETTINGS
# socket is typed by a string tag, so a dict can reach this node without
@@ -270,6 +285,7 @@ class FunPackLoadModifiers(io.ComfyNode):
# Absent, and said out loud. A modifier that half-installed and
# carried on is how a run silently stops meaning what it says.
log.broke(f"{spec.id}.{CAPABILITY}", exc, "installing itself")
control_mod.start_failed(spec, exc)
notes.append(f"{spec.id}: failed to install -- {type(exc).__name__}: {exc}")
continue
if note is None: