fix: keep bypass/mute state on frontend 1.53+ node shell state (#1123)

ComfyUI frontend 1.53 turned LGraphNode.mode into a prototype accessor
backed by node._state, and serialize() now reads that state directly.
Redefining mode on the instance shadowed the setter, so bypass/mute
never reached the serialized workflow and silently reverted to Always
on save/reload or workflow tab switch.

Add interceptModeChange() in web/comfyui/utils.js: it delegates to the
prototype accessor when one exists (observing changes only), and falls
back to the legacy closure accessor on older frontends. Use it in
lora_loader.js and in the Vue widgets' setupModeChangeHandler, which
covers the LoRA provider/aggregator nodes with the same latent bug.
This commit is contained in:
Will Miao
2026-09-25 18:12:01 +08:00
parent dae18b3d1d
commit 067e605e75
8 changed files with 360 additions and 120 deletions
+10 -16
View File
@@ -10,6 +10,7 @@ import {
collectActiveLorasFromChain,
updateConnectedTriggerWords,
chainCallback,
interceptModeChange,
mergeLoras,
getAllGraphNodes,
getNodeFromGraph,
@@ -189,24 +190,17 @@ app.registerExtension({
let isUpdating = false;
let isSyncingInput = false;
// Mechanism: Property descriptor to listen for mode changes
// Mechanism: Observe mode changes without shadowing the frontend's
// own `mode` accessor (frontend >= 1.53 serializes from shell state,
// so redefining `mode` here would silently revert bypass/mute).
const self = this;
let _mode = this.mode;
Object.defineProperty(this, 'mode', {
get() {
return _mode;
},
set(value) {
const oldValue = _mode;
_mode = value;
// Trigger mode change handler
if (self.onModeChange) {
self.onModeChange(value, oldValue);
}
console.log(`[Lora Loader] Node mode changed from ${oldValue} to ${value}`);
interceptModeChange(this, (newMode, oldMode) => {
// Trigger mode change handler
if (self.onModeChange) {
self.onModeChange(newMode, oldMode);
}
console.log(`[Lora Loader] Node mode changed from ${oldMode} to ${newMode}`);
});
// Define the mode change handler
+68
View File
@@ -188,6 +188,74 @@ export function chainCallback(object, property, callback) {
}
}
/**
* Find a `mode` accessor (getter/setter) on the node's prototype chain.
* Since ComfyUI frontend 1.53, `LGraphNode.mode` is a prototype accessor
* backed by the node's shell state (`node._state.mode`); on legacy frontends
* `mode` is a plain data property on the instance and no accessor exists.
*/
function findModeAccessor(node) {
let proto = Object.getPrototypeOf(node);
while (proto && proto !== Object.prototype) {
const descriptor = Object.getOwnPropertyDescriptor(proto, "mode");
if (descriptor && (descriptor.get || descriptor.set)) {
return descriptor;
}
proto = Object.getPrototypeOf(proto);
}
return null;
}
/**
* Observe node mode changes (Always/Never/On Trigger/Bypass) without
* breaking the frontend's own `mode` property.
*
* Since ComfyUI frontend 1.53, `mode` is a prototype accessor backed by
* shell state and serialization reads that state directly. Redefining `mode`
* on the instance would shadow the prototype setter, so bypass/mute never
* reaches the serialized workflow and silently reverts to Always on reload.
* When a prototype accessor exists we delegate to it and only observe the
* change; on legacy frontends we keep the value in a closure as before.
*
* @param {Object} node - The litegraph node instance
* @param {(newMode: number, oldMode: number) => void} onModeChange - Called when the mode actually changes
*/
export function interceptModeChange(node, onModeChange) {
const delegate = findModeAccessor(node);
if (delegate && typeof delegate.get === "function" && typeof delegate.set === "function") {
Object.defineProperty(node, "mode", {
configurable: true,
get() {
return delegate.get.call(this);
},
set(value) {
const oldValue = delegate.get.call(this);
delegate.set.call(this, value);
if (oldValue !== value) {
onModeChange(value, oldValue);
}
},
});
return;
}
let currentMode = node.mode;
Object.defineProperty(node, "mode", {
configurable: true,
get() {
return currentMode;
},
set(value) {
const oldValue = currentMode;
currentMode = value;
if (oldValue !== value) {
onModeChange(value, oldValue);
}
},
});
}
/**
* Show a toast notification
* @param {Object|string} options - Toast options object or message string for backward compatibility
+118 -86
View File
@@ -15715,82 +15715,6 @@ function createVueWidgetCleanup(vueApp, onCleanup) {
onCleanup == null ? void 0 : onCleanup();
};
}
const LORA_PROVIDER_NODE_TYPES$1 = [
"Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)",
"Lora Cycler (LoraManager)",
"Create Hook LoRA (LoraManager)"
];
const LORA_STACK_AGGREGATOR_NODE_TYPES$1 = [
"Lora Stack Combiner (LoraManager)"
];
const LORA_CHAIN_NODE_TYPES$1 = [
...LORA_PROVIDER_NODE_TYPES$1,
...LORA_STACK_AGGREGATOR_NODE_TYPES$1
];
function isLoraStackAggregatorNode$1(comfyClass) {
return LORA_STACK_AGGREGATOR_NODE_TYPES$1.includes(comfyClass);
}
function getActiveLorasFromNodeByType(node) {
const comfyClass = node == null ? void 0 : node.comfyClass;
if (comfyClass === "Lora Cycler (LoraManager)") {
return extractFromCyclerConfig(node);
}
if (isLoraStackAggregatorNode$1(comfyClass)) {
return /* @__PURE__ */ new Set();
}
return extractFromLorasWidget(node);
}
function extractFromLorasWidget(node) {
var _a2;
const activeLoraNames = /* @__PURE__ */ new Set();
const lorasWidget = node.lorasWidget || ((_a2 = node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "loras"));
if (lorasWidget == null ? void 0 : lorasWidget.value) {
lorasWidget.value.forEach((lora) => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
});
}
return activeLoraNames;
}
function extractFromCyclerConfig(node) {
var _a2, _b;
const activeLoraNames = /* @__PURE__ */ new Set();
const cyclerWidget = (_a2 = node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "cycler_config");
if ((_b = cyclerWidget == null ? void 0 : cyclerWidget.value) == null ? void 0 : _b.current_lora_filename) {
activeLoraNames.add(cyclerWidget.value.current_lora_filename);
}
return activeLoraNames;
}
function isNodeActive(mode) {
return mode === void 0 || mode === 0 || mode === 3;
}
function setupModeChangeHandler(node, onModeChange) {
let _mode = node.mode;
Object.defineProperty(node, "mode", {
get() {
return _mode;
},
set(value) {
const oldValue = _mode;
_mode = value;
if (oldValue !== value) {
onModeChange(value, oldValue);
}
}
});
}
function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCallback) {
return (newMode, _oldMode) => {
const isNodeCurrentlyActive = isNodeActive(newMode);
const activeLoraNames = isNodeCurrentlyActive ? getActiveLorasFromNodeByType(node) : /* @__PURE__ */ new Set();
if (nodeSpecificCallback) {
nodeSpecificCallback(activeLoraNames);
}
updateDownstreamLoaders2(node);
};
}
let _loraSyntaxFormatCache = null;
let _loraSyntaxFormatRefreshPromise = null;
async function _fetchLoraSyntaxFormat() {
@@ -15913,24 +15837,24 @@ function lmUrl(path) {
return `${getComfyUIBasePath()}${path}`;
}
const ROOT_GRAPH_ID = "root";
const LORA_PROVIDER_NODE_TYPES = [
const LORA_PROVIDER_NODE_TYPES$1 = [
"Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)",
"Lora Cycler (LoraManager)",
"Create Hook LoRA (LoraManager)"
];
const LORA_STACK_AGGREGATOR_NODE_TYPES = [
const LORA_STACK_AGGREGATOR_NODE_TYPES$1 = [
"Lora Stack Combiner (LoraManager)"
];
const LORA_CHAIN_NODE_TYPES = [
...LORA_PROVIDER_NODE_TYPES,
...LORA_STACK_AGGREGATOR_NODE_TYPES
const LORA_CHAIN_NODE_TYPES$1 = [
...LORA_PROVIDER_NODE_TYPES$1,
...LORA_STACK_AGGREGATOR_NODE_TYPES$1
];
function isLoraStackAggregatorNode(comfyClass) {
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass);
function isLoraStackAggregatorNode$1(comfyClass) {
return LORA_STACK_AGGREGATOR_NODE_TYPES$1.includes(comfyClass);
}
function isLoraChainNode(comfyClass) {
return LORA_CHAIN_NODE_TYPES.includes(comfyClass);
return LORA_CHAIN_NODE_TYPES$1.includes(comfyClass);
}
function isMapLike(collection) {
return collection && typeof collection.entries === "function" && typeof collection.values === "function";
@@ -15968,6 +15892,50 @@ function getLinkFromGraph(graph, linkId) {
}
return graph.links[linkId] || null;
}
function findModeAccessor(node) {
let proto = Object.getPrototypeOf(node);
while (proto && proto !== Object.prototype) {
const descriptor = Object.getOwnPropertyDescriptor(proto, "mode");
if (descriptor && (descriptor.get || descriptor.set)) {
return descriptor;
}
proto = Object.getPrototypeOf(proto);
}
return null;
}
function interceptModeChange(node, onModeChange) {
const delegate = findModeAccessor(node);
if (delegate && typeof delegate.get === "function" && typeof delegate.set === "function") {
Object.defineProperty(node, "mode", {
configurable: true,
get() {
return delegate.get.call(this);
},
set(value) {
const oldValue = delegate.get.call(this);
delegate.set.call(this, value);
if (oldValue !== value) {
onModeChange(value, oldValue);
}
}
});
return;
}
let currentMode = node.mode;
Object.defineProperty(node, "mode", {
configurable: true,
get() {
return currentMode;
},
set(value) {
const oldValue = currentMode;
currentMode = value;
if (oldValue !== value) {
onModeChange(value, oldValue);
}
}
});
}
function isLoraStackInput(input) {
return (input == null ? void 0 : input.type) === "LORA_STACK";
}
@@ -16025,7 +15993,7 @@ function getActiveLorasFromNode(node) {
}
return activeLoraNames;
}
if (isLoraStackAggregatorNode(node.comfyClass)) {
if (isLoraStackAggregatorNode$1(node.comfyClass)) {
return activeLoraNames;
}
let lorasWidget = node.lorasWidget;
@@ -16136,6 +16104,70 @@ function updateDownstreamLoaders(startNode, visited = /* @__PURE__ */ new Set())
}
}
}
const LORA_PROVIDER_NODE_TYPES = [
"Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)",
"Lora Cycler (LoraManager)",
"Create Hook LoRA (LoraManager)"
];
const LORA_STACK_AGGREGATOR_NODE_TYPES = [
"Lora Stack Combiner (LoraManager)"
];
const LORA_CHAIN_NODE_TYPES = [
...LORA_PROVIDER_NODE_TYPES,
...LORA_STACK_AGGREGATOR_NODE_TYPES
];
function isLoraStackAggregatorNode(comfyClass) {
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass);
}
function getActiveLorasFromNodeByType(node) {
const comfyClass = node == null ? void 0 : node.comfyClass;
if (comfyClass === "Lora Cycler (LoraManager)") {
return extractFromCyclerConfig(node);
}
if (isLoraStackAggregatorNode(comfyClass)) {
return /* @__PURE__ */ new Set();
}
return extractFromLorasWidget(node);
}
function extractFromLorasWidget(node) {
var _a2;
const activeLoraNames = /* @__PURE__ */ new Set();
const lorasWidget = node.lorasWidget || ((_a2 = node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "loras"));
if (lorasWidget == null ? void 0 : lorasWidget.value) {
lorasWidget.value.forEach((lora) => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
});
}
return activeLoraNames;
}
function extractFromCyclerConfig(node) {
var _a2, _b;
const activeLoraNames = /* @__PURE__ */ new Set();
const cyclerWidget = (_a2 = node.widgets) == null ? void 0 : _a2.find((w2) => w2.name === "cycler_config");
if ((_b = cyclerWidget == null ? void 0 : cyclerWidget.value) == null ? void 0 : _b.current_lora_filename) {
activeLoraNames.add(cyclerWidget.value.current_lora_filename);
}
return activeLoraNames;
}
function isNodeActive(mode) {
return mode === void 0 || mode === 0 || mode === 3;
}
function setupModeChangeHandler(node, onModeChange) {
interceptModeChange(node, onModeChange);
}
function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCallback) {
return (newMode, _oldMode) => {
const isNodeCurrentlyActive = isNodeActive(newMode);
const activeLoraNames = isNodeCurrentlyActive ? getActiveLorasFromNodeByType(node) : /* @__PURE__ */ new Set();
if (nodeSpecificCallback) {
nodeSpecificCallback(activeLoraNames);
}
updateDownstreamLoaders2(node);
};
}
const LORA_POOL_WIDGET_MIN_WIDTH = 500;
const LORA_POOL_WIDGET_MIN_HEIGHT = 520;
const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500;
@@ -16853,7 +16885,7 @@ app.registerExtension({
return originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments);
};
}
if (LORA_CHAIN_NODE_TYPES$1.includes(comfyClass)) {
if (LORA_CHAIN_NODE_TYPES.includes(comfyClass)) {
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
originalOnNodeCreated == null ? void 0 : originalOnNodeCreated.apply(this, arguments);
File diff suppressed because one or more lines are too long