Replace graph reconfigure with targeted widget updates so applying resolved models does not re-trigger node context analysis or duplicate dialog refreshes. Add workflow refresh suppression state, defer ComfyUI model catalog refresh until after widget linking, and track missing-workflow signatures separately from general workflow changes.
426 lines
16 KiB
JavaScript
426 lines
16 KiB
JavaScript
import { app } from "../../../../../scripts/app.js";
|
|
import { api } from "../../../../../scripts/api.js";
|
|
import { $el } from "../../../../../scripts/ui.js";
|
|
import { getSvgIcon } from "../../utils/icon_utils.js";
|
|
import { getModelCardUrl } from "../utils/url_utils.js";
|
|
export const workflowUpdateMethods = {
|
|
isWorkflowRefreshSuppressed() {
|
|
return Number(this._workflowRefreshSuppressionDepth || 0) > 0;
|
|
},
|
|
|
|
async runWithWorkflowRefreshSuppressed(callback) {
|
|
this._workflowRefreshSuppressionDepth = Number(this._workflowRefreshSuppressionDepth || 0) + 1;
|
|
try {
|
|
return await callback();
|
|
} finally {
|
|
this._workflowRefreshSuppressionDepth = Math.max(
|
|
0,
|
|
Number(this._workflowRefreshSuppressionDepth || 0) - 1
|
|
);
|
|
}
|
|
},
|
|
|
|
scheduleComfyModelCatalogRefreshAfterApply(workflow = null, resolutions = []) {
|
|
setTimeout(() => {
|
|
this.runWithWorkflowRefreshSuppressed(
|
|
() => this.refreshComfyModelCatalogAfterApply(workflow, resolutions)
|
|
).then(refreshed => {
|
|
if (refreshed) {
|
|
this.scheduleActiveWorkflowRefresh?.('node-widget-change');
|
|
}
|
|
}).catch(error => {
|
|
console.warn('Model Resolver: deferred ComfyUI model catalog refresh failed:', error);
|
|
});
|
|
}, 0);
|
|
},
|
|
|
|
async refreshComfyModelCatalogAfterApply(workflow = null, resolutions = []) {
|
|
if (!this.isAutoRefreshComfyModelsAfterApplyEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
if (!this.shouldRefreshComfyModelCatalogForApply(workflow, resolutions)) {
|
|
return false;
|
|
}
|
|
|
|
if (this._comfyModelCatalogRefreshPromise) {
|
|
return this._comfyModelCatalogRefreshPromise;
|
|
}
|
|
|
|
this._comfyModelCatalogRefreshPromise = (async () => {
|
|
try {
|
|
if (typeof app?.refreshComboInNodes === 'function') {
|
|
await app.refreshComboInNodes();
|
|
return true;
|
|
}
|
|
|
|
const nodeDefs = await this.fetchComfyNodeDefs();
|
|
this.applyComfyNodeDefs(nodeDefs);
|
|
this.refreshGraphComboWidgetsFromNodeDefs(nodeDefs);
|
|
app?.graph?.setDirtyCanvas?.(true, true);
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('Model Resolver: could not refresh ComfyUI model catalog:', error);
|
|
return false;
|
|
} finally {
|
|
this._comfyModelCatalogRefreshPromise = null;
|
|
}
|
|
})();
|
|
|
|
return this._comfyModelCatalogRefreshPromise;
|
|
},
|
|
|
|
isAutoRefreshComfyModelsAfterApplyEnabled() {
|
|
const tokens = this.getStoredTokens?.();
|
|
return tokens?.auto_refresh_comfy_models_after_apply !== false;
|
|
},
|
|
|
|
async fetchComfyNodeDefs() {
|
|
return await this.fetchJson(`/object_info?model_resolver_refresh=${Date.now()}`, {}, 'Fetch ComfyUI node definitions');
|
|
},
|
|
|
|
shouldRefreshComfyModelCatalogForApply(workflow = null, resolutions = []) {
|
|
const targets = this.getComfyCatalogApplyTargets(workflow, resolutions);
|
|
if (!targets.length) return false;
|
|
|
|
return targets.some(target => !this.isComfyCatalogTargetAvailable(target));
|
|
},
|
|
|
|
getComfyCatalogApplyTargets(workflow = null, resolutions = []) {
|
|
if (!workflow || !Array.isArray(resolutions) || !resolutions.length) return [];
|
|
|
|
const targets = [];
|
|
for (const resolution of resolutions) {
|
|
const node = this.findWorkflowNodeForResolution(workflow, resolution);
|
|
if (!node) continue;
|
|
|
|
const widgetIndex = Number(resolution?.widget_index);
|
|
if (!Number.isInteger(widgetIndex)) continue;
|
|
|
|
const value = this.getAppliedWidgetValueFromNode(node, widgetIndex, resolution);
|
|
if (!value) continue;
|
|
|
|
const nodeType = node.comfyClass || node.type || resolution.node_type || '';
|
|
const widgetName = this.getWorkflowNodeWidgetName(node, widgetIndex)
|
|
|| this.getGraphNodeWidgetName(node, widgetIndex, resolution)
|
|
|| this.getComfyWidgetNameByIndex(nodeType, widgetIndex);
|
|
if (!nodeType || !widgetName) continue;
|
|
|
|
targets.push({ nodeType, widgetName, value });
|
|
}
|
|
|
|
return targets;
|
|
},
|
|
|
|
findWorkflowNodeForResolution(workflow = {}, resolution = {}) {
|
|
const nodeId = String(resolution?.node_id ?? '');
|
|
if (!nodeId) return null;
|
|
|
|
const isTopLevel = resolution?.is_top_level !== false;
|
|
if (isTopLevel) {
|
|
return (workflow.nodes || []).find(node => String(node?.id) === nodeId) || null;
|
|
}
|
|
|
|
const subgraphId = String(resolution?.subgraph_id || '');
|
|
const subgraphs = workflow.definitions?.subgraphs || [];
|
|
for (const subgraph of subgraphs) {
|
|
if (subgraphId && String(subgraph?.id) !== subgraphId) continue;
|
|
const node = (subgraph?.nodes || []).find(item => String(item?.id) === nodeId);
|
|
if (node) return node;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
getAppliedWidgetValueFromNode(node = {}, widgetIndex = -1, resolution = {}) {
|
|
const widgetsValues = Array.isArray(node.widgets_values) ? node.widgets_values : [];
|
|
const rawValue = widgetsValues[widgetIndex];
|
|
const nestedKey = resolution?.nested_key;
|
|
if (nestedKey && rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
return String(rawValue[nestedKey] || '').trim();
|
|
}
|
|
|
|
if (typeof rawValue === 'string') return rawValue.trim();
|
|
return '';
|
|
},
|
|
|
|
getWorkflowNodeWidgetName(node = {}, widgetIndex = -1) {
|
|
const widget = Array.isArray(node.widgets) ? node.widgets[widgetIndex] : null;
|
|
return String(widget?.name || widget?.label || widget?.widget || '').trim();
|
|
},
|
|
|
|
getGraphNodeWidgetName(workflowNode = {}, widgetIndex = -1, resolution = {}) {
|
|
if (resolution?.is_top_level === false) return '';
|
|
|
|
const nodeId = String(resolution?.node_id ?? workflowNode?.id ?? '');
|
|
const graphNode = (app?.graph?.nodes || []).find(node => String(node?.id) === nodeId);
|
|
const widget = graphNode?.widgets?.[widgetIndex];
|
|
return String(widget?.name || widget?.label || '').trim();
|
|
},
|
|
|
|
getComfyWidgetNameByIndex(nodeType = '', widgetIndex = -1) {
|
|
const nodeDef = this.getCurrentComfyNodeDef(nodeType);
|
|
if (!nodeDef || widgetIndex < 0) return '';
|
|
|
|
const names = [];
|
|
const input = nodeDef.input || {};
|
|
for (const sectionName of ['required', 'optional']) {
|
|
const section = input[sectionName];
|
|
if (!section || typeof section !== 'object') continue;
|
|
for (const [name, spec] of Object.entries(section)) {
|
|
if (this.isComfyWidgetInputSpec(spec)) {
|
|
names.push(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
return names[widgetIndex] || '';
|
|
},
|
|
|
|
getCurrentComfyNodeDef(nodeType = '') {
|
|
if (!nodeType) return null;
|
|
|
|
return app?.nodeDefs?.[nodeType]
|
|
|| globalThis?.LiteGraph?.registered_node_types?.[nodeType]?.nodeData
|
|
|| globalThis?.LiteGraph?.registered_node_types?.[nodeType]?.prototype?.nodeData
|
|
|| null;
|
|
},
|
|
|
|
isComfyWidgetInputSpec(inputSpec) {
|
|
if (!Array.isArray(inputSpec)) return false;
|
|
if (Array.isArray(inputSpec[0])) return true;
|
|
const type = String(inputSpec[0] || '').toUpperCase();
|
|
return ['STRING', 'INT', 'FLOAT', 'BOOLEAN'].includes(type);
|
|
},
|
|
|
|
isComfyCatalogTargetAvailable(target = {}) {
|
|
const values = this.getCurrentComfyCatalogValues(target.nodeType, target.widgetName);
|
|
if (!values) return true;
|
|
|
|
const wanted = this.normalizeComfyCatalogValue(target.value);
|
|
return values.some(value => this.normalizeComfyCatalogValue(value) === wanted);
|
|
},
|
|
|
|
getCurrentComfyCatalogValues(nodeType = '', widgetName = '') {
|
|
const nodeDef = this.getCurrentComfyNodeDef(nodeType);
|
|
const values = this.getComfyComboValuesFromSpec(
|
|
this.getComfyWidgetInputSpec(nodeDef, widgetName)
|
|
);
|
|
if (values) return values;
|
|
|
|
const graphNodes = app?.graph?.nodes || [];
|
|
for (const node of graphNodes) {
|
|
const type = node?.comfyClass || node?.type;
|
|
if (type !== nodeType || !Array.isArray(node.widgets)) continue;
|
|
|
|
const widget = node.widgets.find(item => item?.name === widgetName);
|
|
const widgetValues = widget?.options?.values;
|
|
if (Array.isArray(widgetValues)) return widgetValues;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
normalizeComfyCatalogValue(value = '') {
|
|
return this.normalizePathToForward(value);
|
|
},
|
|
|
|
applyComfyNodeDefs(nodeDefs = {}) {
|
|
if (!nodeDefs || typeof nodeDefs !== 'object') return;
|
|
|
|
try {
|
|
if (app && typeof app === 'object') {
|
|
if (!app.nodeDefs || typeof app.nodeDefs !== 'object') {
|
|
app.nodeDefs = {};
|
|
}
|
|
Object.assign(app.nodeDefs, nodeDefs);
|
|
}
|
|
} catch (error) {
|
|
console.warn('Model Resolver: could not update app.nodeDefs:', error);
|
|
}
|
|
|
|
const registeredTypes = globalThis?.LiteGraph?.registered_node_types;
|
|
if (!registeredTypes || typeof registeredTypes !== 'object') return;
|
|
|
|
for (const [nodeType, nodeDef] of Object.entries(nodeDefs)) {
|
|
const registered = registeredTypes[nodeType];
|
|
if (!registered) continue;
|
|
|
|
try {
|
|
registered.nodeData = nodeDef;
|
|
registered.prototype.nodeData = nodeDef;
|
|
} catch (error) {
|
|
console.warn(`Model Resolver: could not update node definition for ${nodeType}:`, error);
|
|
}
|
|
}
|
|
},
|
|
|
|
getComfyWidgetInputSpec(nodeDef = {}, widgetName = '') {
|
|
const input = nodeDef?.input;
|
|
if (!input || !widgetName) return null;
|
|
|
|
return input.required?.[widgetName]
|
|
|| input.optional?.[widgetName]
|
|
|| input.hidden?.[widgetName]
|
|
|| null;
|
|
},
|
|
|
|
getComfyComboValuesFromSpec(inputSpec) {
|
|
if (!Array.isArray(inputSpec)) return null;
|
|
|
|
const values = inputSpec[0];
|
|
return Array.isArray(values) ? values : null;
|
|
},
|
|
|
|
refreshGraphComboWidgetsFromNodeDefs(nodeDefs = {}) {
|
|
const graphNodes = app?.graph?.nodes;
|
|
if (!Array.isArray(graphNodes) || !nodeDefs || typeof nodeDefs !== 'object') return;
|
|
|
|
for (const node of graphNodes) {
|
|
const nodeType = node?.comfyClass || node?.type;
|
|
const nodeDef = nodeDefs[nodeType];
|
|
if (!nodeDef || !Array.isArray(node.widgets)) continue;
|
|
|
|
for (const widget of node.widgets) {
|
|
const values = this.getComfyComboValuesFromSpec(
|
|
this.getComfyWidgetInputSpec(nodeDef, widget?.name)
|
|
);
|
|
if (!values) continue;
|
|
|
|
if (!widget.options || typeof widget.options !== 'object') {
|
|
widget.options = {};
|
|
}
|
|
widget.options.values = values;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Extract model page URL from a download URL
|
|
* HuggingFace file: https://huggingface.co/Owner/Repo/resolve/main/file.safetensors -> https://huggingface.co/Owner/Repo/blob/main/file.safetensors
|
|
* CivitAI: https://civitai.com/api/download/models/123?type=Model -> https://civitai.com/models/123
|
|
*/
|
|
getModelCardUrl(downloadUrl) {
|
|
return getModelCardUrl(downloadUrl);
|
|
},
|
|
|
|
/**
|
|
* Update workflow in ComfyUI's UI/memory
|
|
* Updates the current workflow in place instead of creating a new tab
|
|
*/
|
|
cloneWorkflowWidgetValue(value) {
|
|
if (!value || typeof value !== 'object') return value;
|
|
|
|
if (typeof structuredClone === 'function') {
|
|
try {
|
|
return structuredClone(value);
|
|
} catch {
|
|
// Fall back to JSON for plain serialized workflow values.
|
|
}
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(JSON.stringify(value));
|
|
} catch {
|
|
return value;
|
|
}
|
|
},
|
|
|
|
applyWorkflowResolutionValuesToGraph(workflow, resolutions = []) {
|
|
const graph = app?.graph;
|
|
if (!graph || !workflow || !Array.isArray(resolutions) || !resolutions.length) {
|
|
return false;
|
|
}
|
|
|
|
const graphNodes = Array.isArray(graph.nodes)
|
|
? graph.nodes
|
|
: (Array.isArray(graph._nodes) ? graph._nodes : []);
|
|
const updates = new Map();
|
|
|
|
for (const resolution of resolutions) {
|
|
if (resolution?.is_top_level === false) return false;
|
|
|
|
const nodeId = String(resolution?.node_id ?? '');
|
|
const widgetIndex = Number(resolution?.widget_index);
|
|
if (!nodeId || !Number.isInteger(widgetIndex) || widgetIndex < 0) return false;
|
|
|
|
const workflowNode = this.findWorkflowNodeForResolution(workflow, resolution);
|
|
const graphNode = graph.getNodeById?.(resolution.node_id)
|
|
|| graphNodes.find(node => String(node?.id) === nodeId);
|
|
const widget = graphNode?.widgets?.[widgetIndex];
|
|
if (!workflowNode || !graphNode || !widget || !Array.isArray(workflowNode.widgets_values)) {
|
|
return false;
|
|
}
|
|
|
|
updates.set(`${nodeId}:${widgetIndex}`, {
|
|
graphNode,
|
|
widget,
|
|
widgetIndex,
|
|
value: workflowNode.widgets_values[widgetIndex]
|
|
});
|
|
}
|
|
|
|
if (!updates.size) return false;
|
|
|
|
graph.beforeChange?.();
|
|
try {
|
|
for (const { graphNode, widget, widgetIndex, value } of updates.values()) {
|
|
const oldValue = widget.value;
|
|
const nextValue = this.cloneWorkflowWidgetValue(value);
|
|
widget.value = nextValue;
|
|
|
|
if (Array.isArray(graphNode.widgets_values)) {
|
|
graphNode.widgets_values[widgetIndex] = this.cloneWorkflowWidgetValue(value);
|
|
}
|
|
|
|
try {
|
|
graphNode.onWidgetChanged?.(widget.name, nextValue, oldValue, widget);
|
|
} catch (error) {
|
|
console.warn('Model Resolver: node rejected a linked widget update:', error);
|
|
}
|
|
}
|
|
} finally {
|
|
graph.afterChange?.();
|
|
}
|
|
|
|
graph.setDirtyCanvas?.(true, true);
|
|
return true;
|
|
},
|
|
|
|
async updateWorkflowInComfyUI(workflow, resolutions = []) {
|
|
if (!app || !app.graph) {
|
|
console.warn('Model Resolver: Could not update workflow - app or app.graph not available');
|
|
return false;
|
|
}
|
|
|
|
return await this.runWithWorkflowRefreshSuppressed(async () => {
|
|
try {
|
|
if (this.applyWorkflowResolutionValuesToGraph(workflow, resolutions)) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof app.graph.configure === 'function') {
|
|
app.graph.configure(workflow);
|
|
return true;
|
|
}
|
|
|
|
if (typeof app.graph.deserialize === 'function') {
|
|
app.graph.deserialize(workflow);
|
|
return true;
|
|
}
|
|
|
|
if (app.loadGraphData) {
|
|
await app.loadGraphData(workflow, false, false, null);
|
|
return true;
|
|
}
|
|
|
|
console.warn('Model Resolver: No method available to update workflow');
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Model Resolver: Error updating workflow in ComfyUI:', error);
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
};
|