From 96b9e886ea7c075f2a693e3e93f4fbcb8dbf0969 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Sun, 15 Feb 2026 11:26:46 +0900 Subject: [PATCH] feat: classify missing nodes by replacement availability (#8483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Extend `MissingNodeType` with `isReplaceable` and `replacement` fields - Classify missing nodes by checking `nodeReplacementStore.getReplacementFor()` during graph load - Wrap hardcoded node patches (T2IAdapterLoader, ConditioningAverage, etc.) in `if (!isEnabled)` guard so they only run when node replacement setting is disabled - Change `useNodeReplacementStore().load()` from fire-and-forget (`void`) to `await` so replacement data is available before missing node detection - Fix guard condition order in `nodeReplacementStore.load()`: check `isEnabled` before `isLoaded` - Align `InputMap` types with actual API response (flat `old_id`/`set_value` fields instead of nested `assign` wrapper) ## Test plan - [x] Load workflow with deprecated nodes (T2IAdapterLoader, Load3DAnimation, SDV_img2vid_Conditioning) - [x] Verify missing nodes are classified with `isReplaceable: true` and `replacement` object - [x] Verify hardcoded patches only run when node replacement setting is OFF - [x] Verify `nodeReplacementStore.load()` is not called when setting is disabled - [x] Unit tests pass (`nodeReplacementStore.test.ts` - 16 tests) - [x] Typecheck passes 🤖 Generated with [Claude Code](https://claude.ai/code) --- .storybook/main.ts | 3 +- .../nodeReplacementStore.test.ts | 10 +++++ .../nodeReplacement/nodeReplacementStore.ts | 3 +- src/platform/nodeReplacement/types.ts | 15 +++----- src/scripts/app.ts | 38 ++++++++++--------- src/types/comfy.ts | 3 ++ 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 0c576b0bde..68e22b2831 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -100,8 +100,7 @@ const config: StorybookConfig = { rolldownOptions: { treeshake: false, output: { - keepNames: true, - strictExecutionOrder: true + keepNames: true }, onwarn: (warning, warn) => { // Suppress specific warnings diff --git a/src/platform/nodeReplacement/nodeReplacementStore.test.ts b/src/platform/nodeReplacement/nodeReplacementStore.test.ts index 3ade67622c..a661d86ee8 100644 --- a/src/platform/nodeReplacement/nodeReplacementStore.test.ts +++ b/src/platform/nodeReplacement/nodeReplacementStore.test.ts @@ -247,5 +247,15 @@ describe('useNodeReplacementStore', () => { expect(fetchNodeReplacements).toHaveBeenCalledOnce() }) + + it('should not call API when setting is disabled', async () => { + vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements) + store = createStore(false) + + await store.load() + + expect(fetchNodeReplacements).not.toHaveBeenCalled() + expect(store.isLoaded).toBe(false) + }) }) }) diff --git a/src/platform/nodeReplacement/nodeReplacementStore.ts b/src/platform/nodeReplacement/nodeReplacementStore.ts index d2ee2520f1..2f749947f2 100644 --- a/src/platform/nodeReplacement/nodeReplacementStore.ts +++ b/src/platform/nodeReplacement/nodeReplacementStore.ts @@ -15,8 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => { ) async function load() { - if (isLoaded.value || !isEnabled.value) return - + if (!isEnabled.value || isLoaded.value) return try { replacements.value = await fetchNodeReplacements() isLoaded.value = true diff --git a/src/platform/nodeReplacement/types.ts b/src/platform/nodeReplacement/types.ts index efbfbba70e..e82ed5bcdf 100644 --- a/src/platform/nodeReplacement/types.ts +++ b/src/platform/nodeReplacement/types.ts @@ -1,17 +1,14 @@ -interface InputAssignOldId { - assign_type: 'old_id' +interface InputMapOldId { + new_id: string old_id: string } -interface InputAssignSetValue { - assign_type: 'set_value' - value: unknown +interface InputMapSetValue { + new_id: string + set_value: unknown } -interface InputMap { - new_id: string - assign: InputAssignOldId | InputAssignSetValue -} +type InputMap = InputMapOldId | InputMapSetValue interface OutputMap { new_idx: number diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 26dc66ea2b..f85be55073 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -795,7 +795,7 @@ export class ComfyApp { await useWorkspaceStore().workflow.syncWorkflows() //Doesn't need to block. Blueprints will load async void useSubgraphStore().fetchSubgraphs() - void useNodeReplacementStore().load() + await useNodeReplacementStore().load() await useExtensionService().loadExtensions() this.addProcessKeyHandler() @@ -1148,6 +1148,8 @@ export class ComfyApp { const embeddedModels: ModelFile[] = [] + const nodeReplacementStore = useNodeReplacementStore() + const collectMissingNodesAndModels = ( nodes: ComfyWorkflowJSON['nodes'], path: string = '' @@ -1160,25 +1162,27 @@ export class ComfyApp { return } for (let n of nodes) { - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader' - if (n.type == 'ConditioningAverage ') n.type = 'ConditioningAverage' //typo fix - if (n.type == 'SDV_img2vid_Conditioning') - n.type = 'SVD_img2vid_Conditioning' //typo fix - if (n.type == 'Load3DAnimation') n.type = 'Load3D' // Animation node merged into Load3D - if (n.type == 'Preview3DAnimation') n.type = 'Preview3D' // Animation node merged into Load3D + // When node replacement is disabled, fall back to hardcoded patches + if (!nodeReplacementStore.isEnabled) { + if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader' + if (n.type == 'ConditioningAverage ') n.type = 'ConditioningAverage' + if (n.type == 'SDV_img2vid_Conditioning') + n.type = 'SVD_img2vid_Conditioning' + if (n.type == 'Load3DAnimation') n.type = 'Load3D' + if (n.type == 'Preview3DAnimation') n.type = 'Preview3D' + } // Find missing node types if (!(n.type in LiteGraph.registered_node_types)) { - // Include context about subgraph location if applicable - if (path) { - missingNodeTypes.push({ - type: n.type, - hint: `in subgraph '${path}'` - }) - } else { - missingNodeTypes.push(n.type) - } + const replacement = nodeReplacementStore.getReplacementFor(n.type) + + missingNodeTypes.push({ + type: n.type, + ...(path && { hint: `in subgraph '${path}'` }), + isReplaceable: replacement !== null, + replacement: replacement ?? undefined + }) + n.type = sanitizeNodeName(n.type) } diff --git a/src/types/comfy.ts b/src/types/comfy.ts index 273824f1bd..c9aa5d1dac 100644 --- a/src/types/comfy.ts +++ b/src/types/comfy.ts @@ -3,6 +3,7 @@ import type { Positionable } from '@/lib/litegraph/src/interfaces' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { NodeReplacement } from '@/platform/nodeReplacement/types' import type { SettingParams } from '@/platform/settings/types' import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import type { Keybinding } from '@/platform/keybindings/types' @@ -93,6 +94,8 @@ export type MissingNodeType = text: string callback: () => void } + isReplaceable?: boolean + replacement?: NodeReplacement } export interface ComfyExtension {