[1.24.x] Cherry-pick post-1.24.2 fixes including subgraph improvements (#4672)

Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
This commit is contained in:
Christian Byrne
2025-08-04 09:49:54 -07:00
committed by GitHub
parent 309a5b8c9a
commit 6eb5a2e010
88 changed files with 6218 additions and 554 deletions

View File

@@ -34,15 +34,14 @@ import type {
ComfyWorkflowJSON,
NodeId
} from '@/schemas/comfyWorkflowSchema'
import {
type ComfyNodeDef,
validateComfyNodeDef
} from '@/schemas/nodeDefSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
interface QueuePromptRequestBody {
client_id: string
prompt: ComfyApiWorkflow
partial_execution_targets?: NodeExecutionId[]
extra_data: {
extra_pnginfo: {
workflow: ComfyWorkflowJSON
@@ -83,6 +82,18 @@ interface QueuePromptRequestBody {
number?: number
}
/**
* Options for queuePrompt method
*/
interface QueuePromptOptions {
/**
* Optional list of node execution IDs to execute (partial execution).
* Each ID represents a node's position in nested subgraphs.
* Format: Colon-separated path of node IDs (e.g., "123:456:789")
*/
partialExecutionTargets?: NodeExecutionId[]
}
/** Dictionary of Frontend-generated API calls */
interface FrontendApiCalls {
graphChanged: ComfyWorkflowJSON
@@ -605,48 +616,31 @@ export class ComfyApi extends EventTarget {
* Loads node object definitions for the graph
* @returns The node definitions
*/
async getNodeDefs({ validate = false }: { validate?: boolean } = {}): Promise<
Record<string, ComfyNodeDef>
> {
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
const resp = await this.fetchApi('/object_info', { cache: 'no-store' })
const objectInfoUnsafe = await resp.json()
if (!validate) {
return objectInfoUnsafe
}
// Validate node definitions against zod schema. (slow)
const objectInfo: Record<string, ComfyNodeDef> = {}
for (const key in objectInfoUnsafe) {
const validatedDef = validateComfyNodeDef(
objectInfoUnsafe[key],
/* onError=*/ (errorMessage: string) => {
console.warn(
`Skipping invalid node definition: ${key}. See debug log for more information.`
)
console.debug(errorMessage)
}
)
if (validatedDef !== null) {
objectInfo[key] = validatedDef
}
}
return objectInfo
return await resp.json()
}
/**
* Queues a prompt to be executed
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
* @param {object} prompt The prompt data to queue
* @param {object} data The prompt data to queue
* @param {QueuePromptOptions} options Optional execution options
* @throws {PromptExecutionError} If the prompt fails to execute
*/
async queuePrompt(
number: number,
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON }
data: { output: ComfyApiWorkflow; workflow: ComfyWorkflowJSON },
options?: QueuePromptOptions
): Promise<PromptResponse> {
const { output: prompt, workflow } = data
const body: QueuePromptRequestBody = {
client_id: this.clientId ?? '', // TODO: Unify clientId access
prompt,
...(options?.partialExecutionTargets && {
partial_execution_targets: options.partialExecutionTargets
}),
extra_data: {
auth_token_comfy_org: this.authToken,
api_key_comfy_org: this.apiKey,

View File

@@ -23,7 +23,8 @@ import {
ComfyApiWorkflow,
type ComfyWorkflowJSON,
type ModelFile,
type NodeId
type NodeId,
isSubgraphDefinition
} from '@/schemas/comfyWorkflowSchema'
import {
type ComfyNodeDef as ComfyNodeDefV1,
@@ -59,6 +60,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
import { ExtensionManager } from '@/types/extensionTypes'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
import { graphToPrompt } from '@/utils/executionUtil'
import {
@@ -124,7 +126,7 @@ export class ComfyApp {
#queueItems: {
number: number
batchCount: number
queueNodeIds?: NodeId[]
queueNodeIds?: NodeExecutionId[]
}[] = []
/**
* If the queue is currently being processed
@@ -720,16 +722,12 @@ export class ComfyApp {
fixLinkInputSlots(this)
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
for (const node of graph.nodes) {
node.onGraphConfigured?.()
}
triggerCallbackOnAllNodes(this, 'onGraphConfigured')
const r = onConfigure?.apply(this, args)
// Fire after onConfigure, used by primitives to generate widget using input nodes config
for (const node of graph.nodes) {
node.onAfterGraphConfigured?.()
}
triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured')
return r
}
@@ -855,26 +853,33 @@ export class ComfyApp {
private updateVueAppNodeDefs(defs: Record<string, ComfyNodeDefV1>) {
// Frontend only nodes registered by custom nodes.
// Example: https://github.com/rgthree/rgthree-comfy/blob/dd534e5384be8cf0c0fa35865afe2126ba75ac55/src_web/comfyui/fast_groups_bypasser.ts#L10
const rawDefs: Record<string, ComfyNodeDefV1> = Object.fromEntries(
Object.entries(LiteGraph.registered_node_types).map(([name, node]) => [
// Only create frontend_only definitions for nodes that don't have backend definitions
const frontendOnlyDefs: Record<string, ComfyNodeDefV1> = {}
for (const [name, node] of Object.entries(
LiteGraph.registered_node_types
)) {
// Skip if we already have a backend definition or system definition
if (name in defs || name in SYSTEM_NODE_DEFS) {
continue
}
frontendOnlyDefs[name] = {
name,
{
name,
display_name: name,
category: node.category || '__frontend_only__',
input: { required: {}, optional: {} },
output: [],
output_name: [],
output_is_list: [],
output_node: false,
python_module: 'custom_nodes.frontend_only',
description: `Frontend only node for ${name}`
} as ComfyNodeDefV1
])
)
display_name: name,
category: node.category || '__frontend_only__',
input: { required: {}, optional: {} },
output: [],
output_name: [],
output_is_list: [],
output_node: false,
python_module: 'custom_nodes.frontend_only',
description: `Frontend only node for ${name}`
} as ComfyNodeDefV1
}
const allNodeDefs = {
...rawDefs,
...frontendOnlyDefs,
...defs,
...SYSTEM_NODE_DEFS
}
@@ -905,12 +910,7 @@ export class ComfyApp {
.join('/')
})
return _.mapValues(
await api.getNodeDefs({
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
}),
(def) => translateNodeDef(def)
)
return _.mapValues(await api.getNodeDefs(), (def) => translateNodeDef(def))
}
/**
@@ -1061,23 +1061,51 @@ export class ComfyApp {
const embeddedModels: ModelFile[] = []
for (let n of graphData.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
const collectMissingNodesAndModels = (
nodes: ComfyWorkflowJSON['nodes'],
path: string = ''
) => {
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
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
missingNodeTypes.push(n.type)
n.type = sanitizeNodeName(n.type)
// 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)
}
n.type = sanitizeNodeName(n.type)
}
// Collect models metadata from node
const selectedModels = getSelectedModelsMetadata(n)
if (selectedModels?.length) {
embeddedModels.push(...selectedModels)
}
}
}
// Collect models metadata from node
const selectedModels = getSelectedModelsMetadata(n)
if (selectedModels?.length) {
embeddedModels.push(...selectedModels)
// Process nodes at the top level
collectMissingNodesAndModels(graphData.nodes)
// Process nodes in subgraphs
if (graphData.definitions?.subgraphs) {
for (const subgraph of graphData.definitions.subgraphs) {
if (isSubgraphDefinition(subgraph)) {
collectMissingNodesAndModels(
subgraph.nodes,
subgraph.name || subgraph.id
)
}
}
}
@@ -1209,20 +1237,16 @@ export class ComfyApp {
})
}
async graphToPrompt(
graph = this.graph,
options: { queueNodeIds?: NodeId[] } = {}
) {
async graphToPrompt(graph = this.graph) {
return graphToPrompt(graph, {
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave'),
queueNodeIds: options.queueNodeIds
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
})
}
async queuePrompt(
number: number,
batchCount: number = 1,
queueNodeIds?: NodeId[]
queueNodeIds?: NodeExecutionId[]
): Promise<boolean> {
this.#queueItems.push({ number, batchCount, queueNodeIds })
@@ -1251,11 +1275,13 @@ export class ComfyApp {
executeWidgetsCallback(subgraph.nodes, 'beforeQueued')
}
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
const p = await this.graphToPrompt(this.graph)
try {
api.authToken = comfyOrgAuthToken
api.apiKey = comfyOrgApiKey ?? undefined
const res = await api.queuePrompt(number, p)
const res = await api.queuePrompt(number, p, {
partialExecutionTargets: queueNodeIds
})
delete api.authToken
delete api.apiKey
executionStore.lastNodeErrors = res.node_errors ?? null

View File

@@ -73,7 +73,8 @@ export class ChangeTracker {
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
}
const navigation = useSubgraphNavigationStore().exportState()
this.subgraphState = navigation.length ? { navigation } : undefined
// Always store the navigation state, even if empty (root level)
this.subgraphState = { navigation }
}
restore() {
@@ -90,8 +91,14 @@ export class ChangeTracker {
const activeId = navigation.at(-1)
if (activeId) {
// Navigate to the saved subgraph
const subgraph = app.graph.subgraphs.get(activeId)
if (subgraph) app.canvas.setGraph(subgraph)
if (subgraph) {
app.canvas.setGraph(subgraph)
}
} else {
// Empty navigation array means root level
app.canvas.setGraph(app.graph)
}
}
}

View File

@@ -186,6 +186,9 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
options: this.options
})
cloned.value = this.value
// Preserve the Y position from the original widget to maintain proper positioning
// when widgets are promoted through subgraph nesting
cloned.y = this.y
return cloned
}
}
@@ -217,6 +220,9 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
options: this.options
})
cloned.value = this.value
// Preserve the Y position from the original widget to maintain proper positioning
// when widgets are promoted through subgraph nesting
cloned.y = this.y
return cloned
}

View File

@@ -21,7 +21,7 @@ export function clone<T>(obj: T): T {
* There are external callers to this function, so we need to keep it for now
*/
export function applyTextReplacements(app: ComfyApp, value: string): string {
return _applyTextReplacements(app.graph.nodes, value)
return _applyTextReplacements(app.graph, value)
}
export async function addStylesheet(