diff --git a/json_loader.py b/json_loader.py index eed69fb..d780a52 100644 --- a/json_loader.py +++ b/json_loader.py @@ -75,6 +75,8 @@ if PromptServer is not None: except (ValueError, TypeError): seq = 1 data = read_json_data(json_path) + if not data: + return web.json_response({"keys": [], "types": [], "error": "file_not_found"}) target = get_batch_item(data, seq) keys = [] types = [] diff --git a/web/json_dynamic.js b/web/json_dynamic.js index 81e11f1..22ca527 100644 --- a/web/json_dynamic.js +++ b/web/json_dynamic.js @@ -17,17 +17,31 @@ app.registerExtension({ if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; } } - // Remove all 32 default outputs from Python RETURN_TYPES - while (this.outputs.length > 0) { - this.removeOutput(0); - } + // Do NOT remove default outputs synchronously here. + // During graph loading, ComfyUI creates all nodes (firing onNodeCreated) + // before configuring them. Other nodes (e.g. Kijai Set/Get) may resolve + // links to our outputs during their configure step. If we remove outputs + // here, those nodes find no output slot and error out. + // + // Instead, defer cleanup: for loaded workflows onConfigure sets _configured + // before this runs; for new nodes the defaults are cleaned up. + this._configured = false; // Add Refresh button this.addWidget("button", "Refresh Outputs", null, () => { this.refreshDynamicOutputs(); }); - this.setSize(this.computeSize()); + queueMicrotask(() => { + if (!this._configured) { + // New node (not loading) — remove the 32 Python default outputs + while (this.outputs.length > 0) { + this.removeOutput(0); + } + this.setSize(this.computeSize()); + app.graph?.setDirtyCanvas(true, true); + } + }); }; nodeType.prototype.refreshDynamicOutputs = async function () { @@ -39,7 +53,14 @@ app.registerExtension({ const resp = await api.fetchApi( `/json_manager/get_keys?path=${encodeURIComponent(pathWidget.value)}&sequence_number=${seqWidget?.value || 1}` ); - const { keys, types } = await resp.json(); + const data = await resp.json(); + const { keys, types } = data; + + // If the file wasn't found, keep existing outputs and links intact + if (data.error === "file_not_found") { + console.warn("[JSONLoaderDynamic] File not found, keeping existing outputs:", pathWidget.value); + return; + } // Store keys and types in hidden widgets for persistence const okWidget = this.widgets?.find(w => w.name === "output_keys"); @@ -82,7 +103,6 @@ app.registerExtension({ // Reassign the outputs array and fix link slot indices this.outputs = newOutputs; - // Update link origin_slot to match new positions if (this.graph) { for (let i = 0; i < this.outputs.length; i++) { const links = this.outputs[i].links; @@ -105,6 +125,7 @@ app.registerExtension({ const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function (info) { origOnConfigure?.apply(this, arguments); + this._configured = true; // Hide internal widgets for (const name of ["output_keys", "output_types"]) { @@ -122,16 +143,23 @@ app.registerExtension({ ? otWidget.value.split(",") : []; - // On load, LiteGraph already restored serialized outputs with links. - // Rename and set types to match stored state (preserves links). - for (let i = 0; i < this.outputs.length && i < keys.length; i++) { - this.outputs[i].name = keys[i].trim(); - if (types[i]) this.outputs[i].type = types[i]; - } + if (keys.length > 0) { + // On load, LiteGraph already restored serialized outputs with links. + // Rename and set types to match stored state (preserves links). + for (let i = 0; i < this.outputs.length && i < keys.length; i++) { + this.outputs[i].name = keys[i].trim(); + if (types[i]) this.outputs[i].type = types[i]; + } - // Remove any extra outputs beyond the key count - while (this.outputs.length > keys.length) { - this.removeOutput(this.outputs.length - 1); + // Remove any extra outputs beyond the key count + while (this.outputs.length > keys.length) { + this.removeOutput(this.outputs.length - 1); + } + } else if (this.outputs.length > 0) { + // Widget values empty but serialized outputs exist — sync widgets + // from the outputs LiteGraph already restored (fallback). + if (okWidget) okWidget.value = this.outputs.map(o => o.name).join(","); + if (otWidget) otWidget.value = this.outputs.map(o => o.type).join(","); } this.setSize(this.computeSize()); diff --git a/web/project_dynamic.js b/web/project_dynamic.js index 9d58b78..9346f3d 100644 --- a/web/project_dynamic.js +++ b/web/project_dynamic.js @@ -17,17 +17,31 @@ app.registerExtension({ if (w) { w.type = "hidden"; w.computeSize = () => [0, -4]; } } - // Remove all 32 default outputs from Python RETURN_TYPES - while (this.outputs.length > 0) { - this.removeOutput(0); - } + // Do NOT remove default outputs synchronously here. + // During graph loading, ComfyUI creates all nodes (firing onNodeCreated) + // before configuring them. Other nodes (e.g. Kijai Set/Get) may resolve + // links to our outputs during their configure step. If we remove outputs + // here, those nodes find no output slot and error out. + // + // Instead, defer cleanup: for loaded workflows onConfigure sets _configured + // before this runs; for new nodes the defaults are cleaned up. + this._configured = false; // Add Refresh button this.addWidget("button", "Refresh Outputs", null, () => { this.refreshDynamicOutputs(); }); - this.setSize(this.computeSize()); + queueMicrotask(() => { + if (!this._configured) { + // New node (not loading) — remove the 32 Python default outputs + while (this.outputs.length > 0) { + this.removeOutput(0); + } + this.setSize(this.computeSize()); + app.graph?.setDirtyCanvas(true, true); + } + }); }; nodeType.prototype.refreshDynamicOutputs = async function () { @@ -42,7 +56,14 @@ app.registerExtension({ const resp = await api.fetchApi( `/json_manager/get_project_keys?url=${encodeURIComponent(urlWidget.value)}&project=${encodeURIComponent(projectWidget.value)}&file=${encodeURIComponent(fileWidget.value)}&seq=${seqWidget?.value || 1}` ); - const { keys, types } = await resp.json(); + const data = await resp.json(); + const { keys, types } = data; + + // If the API returned an error, keep existing outputs and links intact + if (data.error) { + console.warn("[ProjectLoaderDynamic] API error, keeping existing outputs:", data.error); + return; + } // Store keys and types in hidden widgets for persistence const okWidget = this.widgets?.find(w => w.name === "output_keys"); @@ -105,6 +126,7 @@ app.registerExtension({ const origOnConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function (info) { origOnConfigure?.apply(this, arguments); + this._configured = true; // Hide internal widgets for (const name of ["output_keys", "output_types"]) { @@ -122,15 +144,23 @@ app.registerExtension({ ? otWidget.value.split(",") : []; - // Rename and set types to match stored state (preserves links) - for (let i = 0; i < this.outputs.length && i < keys.length; i++) { - this.outputs[i].name = keys[i].trim(); - if (types[i]) this.outputs[i].type = types[i]; - } + if (keys.length > 0) { + // On load, LiteGraph already restored serialized outputs with links. + // Rename and set types to match stored state (preserves links). + for (let i = 0; i < this.outputs.length && i < keys.length; i++) { + this.outputs[i].name = keys[i].trim(); + if (types[i]) this.outputs[i].type = types[i]; + } - // Remove any extra outputs beyond the key count - while (this.outputs.length > keys.length) { - this.removeOutput(this.outputs.length - 1); + // Remove any extra outputs beyond the key count + while (this.outputs.length > keys.length) { + this.removeOutput(this.outputs.length - 1); + } + } else if (this.outputs.length > 0) { + // Widget values empty but serialized outputs exist — sync widgets + // from the outputs LiteGraph already restored (fallback). + if (okWidget) okWidget.value = this.outputs.map(o => o.name).join(","); + if (otWidget) otWidget.value = this.outputs.map(o => o.type).join(","); } this.setSize(this.computeSize());