feat: classify missing nodes by replacement availability (#8483)

## 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)
This commit is contained in:
Jin Yi
2026-02-15 11:26:46 +09:00
committed by GitHub
parent 58182ddda7
commit 96b9e886ea
6 changed files with 42 additions and 30 deletions

View File

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