mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
[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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user