[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

@@ -1,8 +1,7 @@
import type {
ExecutableLGraphNode,
ExecutionId,
LGraph,
NodeId
LGraph
} from '@comfyorg/litegraph'
import {
ExecutableNodeDTO,
@@ -18,31 +17,6 @@ import type {
import { ExecutableGroupNodeDTO, isGroupNode } from './executableGroupNodeDto'
import { compressWidgetInputSlots } from './litegraphUtil'
/**
* Recursively target node's parent nodes to the new output.
* @param nodeId The node id to add.
* @param oldOutput The old output.
* @param newOutput The new output.
* @returns The new output.
*/
function recursiveAddNodes(
nodeId: NodeId,
oldOutput: ComfyApiWorkflow,
newOutput: ComfyApiWorkflow
) {
const currentId = String(nodeId)
const currentNode = oldOutput[currentId]!
if (newOutput[currentId] == null) {
newOutput[currentId] = currentNode
for (const inputValue of Object.values(currentNode.inputs || [])) {
if (Array.isArray(inputValue)) {
recursiveAddNodes(inputValue[0], oldOutput, newOutput)
}
}
}
return newOutput
}
/**
* Converts the current graph workflow for sending to the API.
* @note Node widgets are updated before serialization to prepare queueing.
@@ -50,14 +24,13 @@ function recursiveAddNodes(
* @param graph The graph to convert.
* @param options The options for the conversion.
* - `sortNodes`: Whether to sort the nodes by execution order.
* - `queueNodeIds`: The output nodes to execute. Execute all output nodes if not provided.
* @returns The workflow and node links
*/
export const graphToPrompt = async (
graph: LGraph,
options: { sortNodes?: boolean; queueNodeIds?: NodeId[] } = {}
options: { sortNodes?: boolean } = {}
): Promise<{ workflow: ComfyWorkflowJSON; output: ComfyApiWorkflow }> => {
const { sortNodes = false, queueNodeIds } = options
const { sortNodes = false } = options
for (const node of graph.computeExecutionOrder(false)) {
const innerNodes = node.getInnerNodes
@@ -104,7 +77,7 @@ export const graphToPrompt = async (
nodeDtoMap.set(dto.id, dto)
}
let output: ComfyApiWorkflow = {}
const output: ComfyApiWorkflow = {}
// Process nodes in order of execution
for (const node of nodeDtoMap.values()) {
// Don't serialize muted nodes
@@ -180,14 +153,5 @@ export const graphToPrompt = async (
}
}
// Partial execution
if (queueNodeIds?.length) {
const newOutput = {}
for (const queueNodeId of queueNodeIds) {
recursiveAddNodes(queueNodeId, output, newOutput)
}
output = newOutput
}
return { workflow: workflow as ComfyWorkflowJSON, output }
}

View File

@@ -1,8 +1,10 @@
import type { LGraph, LGraphNode, Subgraph } from '@comfyorg/litegraph'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
import { parseNodeLocatorId } from '@/types/nodeIdentification'
import { isSubgraphIoNode } from './typeGuardUtil'
/**
* Parses an execution ID into its component parts.
*
@@ -86,13 +88,7 @@ export function triggerCallbackOnAllNodes(
graph: LGraph | Subgraph,
callbackProperty: keyof LGraphNode
): void {
visitGraphNodes(graph, (node) => {
// Recursively process subgraphs first
if (node.isSubgraphNode?.() && node.subgraph) {
triggerCallbackOnAllNodes(node.subgraph, callbackProperty)
}
// Invoke callback if it exists on the node
forEachNode(graph, (node) => {
const callback = node[callbackProperty]
if (typeof callback === 'function') {
callback.call(node)
@@ -100,6 +96,58 @@ export function triggerCallbackOnAllNodes(
})
}
/**
* Maps a function over all nodes in a graph hierarchy (including subgraphs).
* This is a pure functional traversal that doesn't mutate the graph.
*
* @param graph - The root graph to traverse
* @param mapFn - Function to apply to each node
* @returns Array of mapped results (excluding undefined values)
*/
export function mapAllNodes<T>(
graph: LGraph | Subgraph,
mapFn: (node: LGraphNode) => T | undefined
): T[] {
const results: T[] = []
visitGraphNodes(graph, (node) => {
// Recursively map over subgraphs first
if (node.isSubgraphNode?.() && node.subgraph) {
results.push(...mapAllNodes(node.subgraph, mapFn))
}
// Apply map function to current node
const result = mapFn(node)
if (result !== undefined) {
results.push(result)
}
})
return results
}
/**
* Executes a side-effect function on all nodes in a graph hierarchy.
* This is for operations that modify nodes or perform side effects.
*
* @param graph - The root graph to traverse
* @param fn - Function to execute on each node
*/
export function forEachNode(
graph: LGraph | Subgraph,
fn: (node: LGraphNode) => void
): void {
visitGraphNodes(graph, (node) => {
// Recursively process subgraphs first
if (node.isSubgraphNode?.() && node.subgraph) {
forEachNode(node.subgraph, fn)
}
// Execute function on current node
fn(node)
})
}
/**
* Collects all nodes in a graph hierarchy (including subgraphs) into a flat array.
*
@@ -111,21 +159,12 @@ export function collectAllNodes(
graph: LGraph | Subgraph,
filter?: (node: LGraphNode) => boolean
): LGraphNode[] {
const nodes: LGraphNode[] = []
visitGraphNodes(graph, (node) => {
// Recursively collect from subgraphs
if (node.isSubgraphNode?.() && node.subgraph) {
nodes.push(...collectAllNodes(node.subgraph, filter))
}
// Add node if it passes the filter (or no filter provided)
return mapAllNodes(graph, (node) => {
if (!filter || filter(node)) {
nodes.push(node)
return node
}
return undefined
})
return nodes
}
/**
@@ -166,7 +205,7 @@ export function findSubgraphByUuid(
targetUuid: string
): Subgraph | null {
// Check all nodes in the current graph
for (const node of graph._nodes) {
for (const node of graph.nodes) {
if (node.isSubgraphNode?.() && node.subgraph) {
if (node.subgraph.id === targetUuid) {
return node.subgraph
@@ -179,6 +218,42 @@ export function findSubgraphByUuid(
return null
}
/**
* Iteratively finds the path of subgraph IDs to a target subgraph.
* @param rootGraph The graph to start searching from.
* @param targetId The ID of the subgraph to find.
* @returns An array of subgraph IDs representing the path, or `null` if not found.
*/
export function findSubgraphPathById(
rootGraph: LGraph,
targetId: string
): string[] | null {
const stack: { graph: LGraph | Subgraph; path: string[] }[] = [
{ graph: rootGraph, path: [] }
]
while (stack.length > 0) {
const { graph, path } = stack.pop()!
// Check if graph exists and has _nodes property
if (!graph || !graph._nodes || !Array.isArray(graph._nodes)) {
continue
}
for (const node of graph._nodes) {
if (node.isSubgraphNode?.() && node.subgraph) {
const newPath = [...path, String(node.subgraph.id)]
if (node.subgraph.id === targetId) {
return newPath
}
stack.push({ graph: node.subgraph, path: newPath })
}
}
}
return null
}
/**
* Get a node by its execution ID from anywhere in the graph hierarchy.
* Execution IDs use hierarchical format like "123:456:789" for nested nodes.
@@ -241,3 +316,202 @@ export function getNodeByLocatorId(
return targetSubgraph.getNodeById(localNodeId) || null
}
/**
* Finds the root graph from any graph in the hierarchy.
*
* @param graph - Any graph or subgraph in the hierarchy
* @returns The root graph
*/
export function getRootGraph(graph: LGraph | Subgraph): LGraph | Subgraph {
let current: LGraph | Subgraph = graph
while ('rootGraph' in current && current.rootGraph) {
current = current.rootGraph
}
return current
}
/**
* Applies a function to all nodes whose type matches a subgraph ID.
* Operates on the entire graph hierarchy starting from the root.
*
* @param rootGraph - The root graph to search in
* @param subgraphId - The ID/type of the subgraph to match nodes against
* @param fn - Function to apply to each matching node
*/
export function forEachSubgraphNode(
rootGraph: LGraph | Subgraph | null | undefined,
subgraphId: string | null | undefined,
fn: (node: LGraphNode) => void
): void {
if (!rootGraph || !subgraphId) return
forEachNode(rootGraph, (node) => {
if (node.type === subgraphId) {
fn(node)
}
})
}
/**
* Maps a function over all nodes whose type matches a subgraph ID.
* Operates on the entire graph hierarchy starting from the root.
*
* @param rootGraph - The root graph to search in
* @param subgraphId - The ID/type of the subgraph to match nodes against
* @param mapFn - Function to apply to each matching node
* @returns Array of mapped results
*/
export function mapSubgraphNodes<T>(
rootGraph: LGraph | Subgraph | null | undefined,
subgraphId: string | null | undefined,
mapFn: (node: LGraphNode) => T
): T[] {
if (!rootGraph || !subgraphId) return []
return mapAllNodes(rootGraph, (node) => {
if (node.type === subgraphId) {
return mapFn(node)
}
return undefined
})
}
/**
* Gets all non-IO nodes from a subgraph (excludes SubgraphInputNode and SubgraphOutputNode).
* These are the user-created nodes that can be safely removed when clearing a subgraph.
*
* @param subgraph - The subgraph to get non-IO nodes from
* @returns Array of non-IO nodes (user-created nodes)
*/
export function getAllNonIoNodesInSubgraph(subgraph: Subgraph): LGraphNode[] {
return subgraph.nodes.filter((node) => !isSubgraphIoNode(node))
}
/**
* Options for traverseNodesDepthFirst function
*/
export interface TraverseNodesOptions<T> {
/** Function called for each node during traversal */
visitor?: (node: LGraphNode, context: T) => T
/** Initial context value */
initialContext?: T
/** Whether to traverse into subgraph nodes (default: true) */
expandSubgraphs?: boolean
}
/**
* Performs depth-first traversal of nodes and their subgraphs.
* Generic visitor pattern that can be used for various node processing tasks.
*
* @param nodes - Starting nodes for traversal
* @param options - Optional traversal configuration
*/
export function traverseNodesDepthFirst<T = void>(
nodes: LGraphNode[],
options?: TraverseNodesOptions<T>
): void {
const {
visitor = () => undefined as T,
initialContext = undefined as T,
expandSubgraphs = true
} = options || {}
type StackItem = { node: LGraphNode; context: T }
const stack: StackItem[] = []
// Initialize stack with starting nodes
for (const node of nodes) {
stack.push({ node, context: initialContext })
}
// Process stack iteratively (DFS)
while (stack.length > 0) {
const { node, context } = stack.pop()!
// Visit node and get updated context for children
const childContext = visitor(node, context)
// If it's a subgraph and we should expand, add children to stack
if (expandSubgraphs && node.isSubgraphNode?.() && node.subgraph) {
// Process children in reverse order to maintain left-to-right DFS processing
// when popping from stack (LIFO). Iterate backwards to avoid array reversal.
const children = node.subgraph.nodes
for (let i = children.length - 1; i >= 0; i--) {
stack.push({ node: children[i], context: childContext })
}
}
}
}
/**
* Options for collectFromNodes function
*/
export interface CollectFromNodesOptions<T, C> {
/** Function that returns data to collect for each node */
collector?: (node: LGraphNode, context: C) => T | null
/** Function that builds context for child nodes */
contextBuilder?: (node: LGraphNode, parentContext: C) => C
/** Initial context value */
initialContext?: C
/** Whether to traverse into subgraph nodes (default: true) */
expandSubgraphs?: boolean
}
/**
* Collects nodes with custom data during depth-first traversal.
* Generic collector that can gather any type of data per node.
*
* @param nodes - Starting nodes for traversal
* @param options - Optional collection configuration
* @returns Array of collected data
*/
export function collectFromNodes<T = LGraphNode, C = void>(
nodes: LGraphNode[],
options?: CollectFromNodesOptions<T, C>
): T[] {
const {
collector = (node: LGraphNode) => node as unknown as T,
contextBuilder = () => undefined as C,
initialContext = undefined as C,
expandSubgraphs = true
} = options || {}
const results: T[] = []
traverseNodesDepthFirst(nodes, {
visitor: (node, context) => {
const data = collector(node, context)
if (data !== null) {
results.push(data)
}
return contextBuilder(node, context)
},
initialContext,
expandSubgraphs
})
return results
}
/**
* Collects execution IDs for selected nodes and all their descendants.
* Uses the generic DFS traversal with optimized string building.
*
* @param selectedNodes - The selected nodes to process
* @returns Array of execution IDs for selected nodes and all nodes within selected subgraphs
*/
export function getExecutionIdsForSelectedNodes(
selectedNodes: LGraphNode[]
): NodeExecutionId[] {
return collectFromNodes<NodeExecutionId, string>(selectedNodes, {
collector: (node, parentExecutionId) => {
const nodeId = String(node.id)
return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId
},
contextBuilder: (node, parentExecutionId) => {
const nodeId = String(node.id)
return parentExecutionId ? `${parentExecutionId}:${nodeId}` : nodeId
},
initialContext: '',
expandSubgraphs: true
})
}

View File

@@ -153,7 +153,10 @@ export function migrateWidgetsValues<TWidgetValue>(
* @param graph - The graph to fix links for.
*/
export function fixLinkInputSlots(graph: LGraph) {
// Note: We can't use forEachNode here because we need access to the graph's
// links map at each level. Links are stored in their respective graph/subgraph.
for (const node of graph.nodes) {
// Fix links for the current node
for (const [inputIndex, input] of node.inputs.entries()) {
const linkId = input.link
if (!linkId) continue
@@ -163,6 +166,11 @@ export function fixLinkInputSlots(graph: LGraph) {
link.target_slot = inputIndex
}
// Recursively fix links in subgraphs
if (node.isSubgraphNode?.() && node.subgraph) {
fixLinkInputSlots(node.subgraph)
}
}
}

View File

@@ -0,0 +1,21 @@
import type { LGraphNode } from '@comfyorg/litegraph'
/**
* Checks if a node is an output node.
* Output nodes are nodes that have the output_node flag set in their nodeData.
*
* @param node - The node to check
* @returns True if the node is an output node, false otherwise
*/
export const isOutputNode = (node: LGraphNode) =>
node.constructor.nodeData?.output_node
/**
* Filters nodes to find only output nodes.
* Output nodes are nodes that have the output_node flag set in their nodeData.
*
* @param nodes - Array of nodes to filter
* @returns Array of output nodes only
*/
export const filterOutputNodes = (nodes: LGraphNode[]): LGraphNode[] =>
nodes.filter(isOutputNode)

View File

@@ -1,11 +1,14 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { LGraph, Subgraph } from '@comfyorg/litegraph'
import { formatDate } from '@/utils/formatUtil'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
export function applyTextReplacements(
allNodes: LGraphNode[],
graph: LGraph | Subgraph,
value: string
): string {
const allNodes = collectAllNodes(graph)
return value.replace(/%([^%]+)%/g, function (match, text) {
const split = text.split('.')
if (split.length !== 2) {

View File

@@ -27,3 +27,16 @@ export const isSubgraph = (
*/
export const isNonNullish = <T>(item: T | undefined | null): item is T =>
item != null
/**
* Type guard to check if a node is a subgraph input/output node.
* These nodes are essential to subgraph structure and should not be removed.
*/
export const isSubgraphIoNode = (
node: LGraphNode
): node is LGraphNode & {
constructor: { comfyClass: 'SubgraphInputNode' | 'SubgraphOutputNode' }
} => {
const nodeClass = node.constructor?.comfyClass
return nodeClass === 'SubgraphInputNode' || nodeClass === 'SubgraphOutputNode'
}