Files
ComfyUI-Lora-Manager/vue-widgets/src/mode-change-handler.ts
T
Will Miao 067e605e75 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.
2026-09-25 18:12:01 +08:00

176 lines
5.8 KiB
TypeScript

/**
* Mode change handler for LoRA provider nodes.
*
* Provides common mode change logic for nodes that provide LoRA configurations:
* - Lora Stacker (LoraManager)
* - Lora Randomizer (LoraManager)
* - Lora Cycler (LoraManager)
*/
// @ts-ignore
import { interceptModeChange } from '../../web/comfyui/utils.js'
/**
* List of node types that act as LoRA providers in the workflow chain.
* These nodes can be traversed when collecting active LoRAs and can trigger
* downstream loader updates when their mode or active LoRAs change.
*/
export const LORA_PROVIDER_NODE_TYPES = [
"Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)",
"Lora Cycler (LoraManager)",
"Create Hook LoRA (LoraManager)",
] as const;
/**
* Nodes that do not own LoRA state themselves, but merge or forward LORA_STACK
* inputs so downstream trigger-word updates must traverse through them.
*/
export const LORA_STACK_AGGREGATOR_NODE_TYPES = [
"Lora Stack Combiner (LoraManager)",
] as const;
export const LORA_CHAIN_NODE_TYPES = [
...LORA_PROVIDER_NODE_TYPES,
...LORA_STACK_AGGREGATOR_NODE_TYPES,
] as const;
export type LoraProviderNodeType = typeof LORA_PROVIDER_NODE_TYPES[number];
export type LoraStackAggregatorNodeType = typeof LORA_STACK_AGGREGATOR_NODE_TYPES[number];
export type LoraChainNodeType = typeof LORA_CHAIN_NODE_TYPES[number];
/**
* Check if a node class is a LoRA provider node.
*/
export function isLoraProviderNode(comfyClass: string): comfyClass is LoraProviderNodeType {
return LORA_PROVIDER_NODE_TYPES.includes(comfyClass as LoraProviderNodeType);
}
export function isLoraStackAggregatorNode(
comfyClass: string
): comfyClass is LoraStackAggregatorNodeType {
return LORA_STACK_AGGREGATOR_NODE_TYPES.includes(comfyClass as LoraStackAggregatorNodeType);
}
export function isLoraChainNode(comfyClass: string): comfyClass is LoraChainNodeType {
return LORA_CHAIN_NODE_TYPES.includes(comfyClass as LoraChainNodeType);
}
/**
* Extract active LoRA filenames from a node based on its type.
*
* For Lora Stacker and Lora Randomizer: extracts from lorasWidget (array of loras, filtered by active)
* For Lora Cycler: extracts from cycler_config widget (single current_lora_filename)
*/
export function getActiveLorasFromNodeByType(node: any): Set<string> {
const comfyClass = node?.comfyClass;
if (comfyClass === "Lora Cycler (LoraManager)") {
return extractFromCyclerConfig(node);
}
if (isLoraStackAggregatorNode(comfyClass)) {
return new Set<string>();
}
// Default: use lorasWidget (works for Stacker and Randomizer)
return extractFromLorasWidget(node);
}
/**
* Extract active LoRAs from a node's lorasWidget.
* Used by Lora Stacker and Lora Randomizer.
*/
function extractFromLorasWidget(node: any): Set<string> {
const activeLoraNames = new Set<string>();
const lorasWidget = node.lorasWidget || node.widgets?.find((w: any) => w.name === 'loras');
if (lorasWidget?.value) {
lorasWidget.value.forEach((lora: any) => {
if (lora.active) {
activeLoraNames.add(lora.name);
}
});
}
return activeLoraNames;
}
/**
* Extract the active LoRA from a Lora Cycler node.
* The Cycler has only one active LoRA at a time, stored in cycler_config.current_lora_filename.
*/
function extractFromCyclerConfig(node: any): Set<string> {
const activeLoraNames = new Set<string>();
const cyclerWidget = node.widgets?.find((w: any) => w.name === 'cycler_config');
if (cyclerWidget?.value?.current_lora_filename) {
activeLoraNames.add(cyclerWidget.value.current_lora_filename);
}
return activeLoraNames;
}
/**
* Check if a mode value represents an active node.
* Active modes: 0 (Always), 3 (On Trigger)
* Inactive modes: 2 (Never), 4 (Bypass)
*/
export function isNodeActive(mode: number | undefined): boolean {
return mode === undefined || mode === 0 || mode === 3;
}
/**
* Setup a mode change handler for a node.
*
* Delegates to `interceptModeChange`, which observes the mode property
* without shadowing the frontend's own `mode` accessor. Since ComfyUI
* frontend 1.53, `mode` is backed by shell state (`node._state.mode`) that
* serialization reads directly — redefining the property on the instance
* would silently revert bypass/mute on save/reload.
*
* @param node - The node to set up the handler for
* @param onModeChange - Callback function called when mode changes (receives newMode and oldMode)
*/
export function setupModeChangeHandler(
node: any,
onModeChange: (newMode: number, oldMode: number) => void
): void {
interceptModeChange(node, onModeChange);
}
/**
* Create a mode change callback that updates downstream loaders.
*
* This is the standard callback used by all LoRA provider nodes.
* When mode changes:
* 1. Determine if the node is active (mode 0 or 3)
* 2. Get active LoRAs (empty set if inactive)
* 3. Call the optional node-specific callback (e.g., updateConnectedTriggerWords for Stacker)
* 4. Update downstream loaders
*
* @param node - The node instance
* @param updateDownstreamLoaders - Function to update downstream loaders (from utils.js)
* @param nodeSpecificCallback - Optional callback for node-specific behavior
*/
export function createModeChangeCallback(
node: any,
updateDownstreamLoaders: (node: any) => void,
nodeSpecificCallback?: (activeLoraNames: Set<string>) => void
): (newMode: number, oldMode: number) => void {
return (newMode: number, _oldMode: number) => {
const isNodeCurrentlyActive = isNodeActive(newMode);
const activeLoraNames = isNodeCurrentlyActive
? getActiveLorasFromNodeByType(node)
: new Set<string>();
// Node-specific handling (e.g., Lora Stacker's direct trigger toggle updates)
if (nodeSpecificCallback) {
nodeSpecificCallback(activeLoraNames);
}
// Always update downstream loaders
updateDownstreamLoaders(node);
};
}