[backport core/1.39] feat: classify missing nodes by replacement availability (#8931)

Backport of #8483 to `core/1.39`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8931-backport-core-1-39-feat-classify-missing-nodes-by-replacement-availability-30a6d73d365081cbb1cdef5d62b4687d)
by [Unito](https://www.unito.io)

Co-authored-by: Jin Yi <jin12cc@gmail.com>
This commit is contained in:
Comfy Org PR Bot
2026-02-17 16:45:47 +09:00
committed by GitHub
parent 0b952a8b21
commit bbae1a0c6e
5 changed files with 41 additions and 28 deletions

View File

@@ -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)
})
})
})

View File

@@ -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

View File

@@ -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

View File

@@ -786,7 +786,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()
@@ -1134,6 +1134,8 @@ export class ComfyApp {
const embeddedModels: ModelFile[] = []
const nodeReplacementStore = useNodeReplacementStore()
const collectMissingNodesAndModels = (
nodes: ComfyWorkflowJSON['nodes'],
path: string = ''
@@ -1146,25 +1148,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)
}

View File

@@ -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 {