mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-26 16:05:11 +00:00
Compare commits
2 Commits
sno-qa-106
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160c615bc4 | ||
|
|
eb61c0bb4d |
@@ -7,6 +7,7 @@
|
|||||||
"empty": "Empty",
|
"empty": "Empty",
|
||||||
"noWorkflowsFound": "No workflows found.",
|
"noWorkflowsFound": "No workflows found.",
|
||||||
"comingSoon": "Coming Soon",
|
"comingSoon": "Coming Soon",
|
||||||
|
"inSubgraph": "in subgraph {name}",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadImage": "Download image",
|
"downloadImage": "Download image",
|
||||||
"downloadVideo": "Download video",
|
"downloadVideo": "Download video",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useMissingModelsDialog } from '@/composables/useMissingModelsDialog'
|
|||||||
import { useMissingNodesDialog } from '@/composables/useMissingNodesDialog'
|
import { useMissingNodesDialog } from '@/composables/useMissingNodesDialog'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { appendJsonExt } from '@/utils/formatUtil'
|
import { appendJsonExt } from '@/utils/formatUtil'
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export const useWorkflowService = () => {
|
|||||||
const missingNodesDialog = useMissingNodesDialog()
|
const missingNodesDialog = useMissingNodesDialog()
|
||||||
const workflowThumbnail = useWorkflowThumbnail()
|
const workflowThumbnail = useWorkflowThumbnail()
|
||||||
const domWidgetStore = useDomWidgetStore()
|
const domWidgetStore = useDomWidgetStore()
|
||||||
|
const executionErrorStore = useExecutionErrorStore()
|
||||||
const workflowDraftStore = useWorkflowDraftStore()
|
const workflowDraftStore = useWorkflowDraftStore()
|
||||||
|
|
||||||
async function getFilename(defaultName: string): Promise<string | null> {
|
async function getFilename(defaultName: string): Promise<string | null> {
|
||||||
@@ -472,7 +474,15 @@ export const useWorkflowService = () => {
|
|||||||
settingStore.get('Comfy.Workflow.ShowMissingNodesWarning')
|
settingStore.get('Comfy.Workflow.ShowMissingNodesWarning')
|
||||||
) {
|
) {
|
||||||
missingNodesDialog.show({ missingNodeTypes })
|
missingNodesDialog.show({ missingNodeTypes })
|
||||||
|
|
||||||
|
// For now, we'll make them coexist.
|
||||||
|
// Once the Node Replacement feature is implemented in TabErrors
|
||||||
|
// we'll remove the modal display and direct users to the error tab.
|
||||||
|
if (settingStore.get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||||
|
executionErrorStore.showErrorOverlay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
missingModels &&
|
missingModels &&
|
||||||
settingStore.get('Comfy.Workflow.ShowMissingModelsWarning')
|
settingStore.get('Comfy.Workflow.ShowMissingModelsWarning')
|
||||||
|
|||||||
@@ -547,7 +547,6 @@ export type ComfyApiWorkflow = z.infer<typeof zComfyApiWorkflow>
|
|||||||
* where that definition is instantiated in the workflow.
|
* where that definition is instantiated in the workflow.
|
||||||
*
|
*
|
||||||
* "def-A" → ["5", "10"] for each container node instantiating that subgraph definition.
|
* "def-A" → ["5", "10"] for each container node instantiating that subgraph definition.
|
||||||
* @knipIgnoreUsedByStackedPR
|
|
||||||
*/
|
*/
|
||||||
export function buildSubgraphExecutionPaths(
|
export function buildSubgraphExecutionPaths(
|
||||||
rootNodes: ComfyNode[],
|
rootNodes: ComfyNode[],
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import {
|
|||||||
type ComfyWorkflowJSON,
|
type ComfyWorkflowJSON,
|
||||||
type ModelFile,
|
type ModelFile,
|
||||||
type NodeId,
|
type NodeId,
|
||||||
isSubgraphDefinition
|
isSubgraphDefinition,
|
||||||
|
buildSubgraphExecutionPaths
|
||||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import type {
|
import type {
|
||||||
ExecutionErrorWsMessage,
|
ExecutionErrorWsMessage,
|
||||||
@@ -1099,6 +1100,15 @@ export class ComfyApp {
|
|||||||
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
||||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||||
useMissingNodesDialog().show({ missingNodeTypes })
|
useMissingNodesDialog().show({ missingNodeTypes })
|
||||||
|
|
||||||
|
// For now, we'll make them coexist.
|
||||||
|
// Once the Node Replacement feature is implemented in TabErrors
|
||||||
|
// we'll remove the modal display and direct users to the error tab.
|
||||||
|
const executionErrorStore = useExecutionErrorStore()
|
||||||
|
executionErrorStore.setMissingNodeTypes(missingNodeTypes)
|
||||||
|
if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||||
|
executionErrorStore.showErrorOverlay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,12 +1191,13 @@ export class ComfyApp {
|
|||||||
|
|
||||||
const collectMissingNodesAndModels = (
|
const collectMissingNodesAndModels = (
|
||||||
nodes: ComfyWorkflowJSON['nodes'],
|
nodes: ComfyWorkflowJSON['nodes'],
|
||||||
path: string = ''
|
pathPrefix: string = '',
|
||||||
|
displayName: string = ''
|
||||||
) => {
|
) => {
|
||||||
if (!Array.isArray(nodes)) {
|
if (!Array.isArray(nodes)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Workflow nodes data is missing or invalid, skipping node processing',
|
'Workflow nodes data is missing or invalid, skipping node processing',
|
||||||
{ nodes, path }
|
{ nodes, pathPrefix }
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1195,9 +1206,26 @@ export class ComfyApp {
|
|||||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||||
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
||||||
|
|
||||||
|
// To access missing node information in the error tab
|
||||||
|
// we collect the cnr_id and execution_id here.
|
||||||
|
let cnrId: string | undefined
|
||||||
|
if (typeof n.properties?.cnr_id === 'string') {
|
||||||
|
cnrId = n.properties.cnr_id
|
||||||
|
} else if (typeof n.properties?.aux_id === 'string') {
|
||||||
|
cnrId = n.properties.aux_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionId = pathPrefix
|
||||||
|
? `${pathPrefix}:${n.id}`
|
||||||
|
: String(n.id)
|
||||||
|
|
||||||
missingNodeTypes.push({
|
missingNodeTypes.push({
|
||||||
type: n.type,
|
type: n.type,
|
||||||
...(path && { hint: `in subgraph '${path}'` }),
|
nodeId: executionId,
|
||||||
|
cnrId,
|
||||||
|
...(displayName && {
|
||||||
|
hint: t('g.inSubgraph', { name: displayName })
|
||||||
|
}),
|
||||||
isReplaceable: replacement !== null,
|
isReplaceable: replacement !== null,
|
||||||
replacement: replacement ?? undefined
|
replacement: replacement ?? undefined
|
||||||
})
|
})
|
||||||
@@ -1216,14 +1244,25 @@ export class ComfyApp {
|
|||||||
// Process nodes at the top level
|
// Process nodes at the top level
|
||||||
collectMissingNodesAndModels(graphData.nodes)
|
collectMissingNodesAndModels(graphData.nodes)
|
||||||
|
|
||||||
|
// Build map: subgraph definition UUID → full execution path prefix.
|
||||||
|
// Handles arbitrary nesting depth (e.g. root node 11 → "11", node 14 in sg 11 → "11:14").
|
||||||
|
const subgraphContainerIdMap = buildSubgraphExecutionPaths(
|
||||||
|
graphData.nodes,
|
||||||
|
graphData.definitions?.subgraphs ?? []
|
||||||
|
)
|
||||||
|
|
||||||
// Process nodes in subgraphs
|
// Process nodes in subgraphs
|
||||||
if (graphData.definitions?.subgraphs) {
|
if (graphData.definitions?.subgraphs) {
|
||||||
for (const subgraph of graphData.definitions.subgraphs) {
|
for (const subgraph of graphData.definitions.subgraphs) {
|
||||||
if (isSubgraphDefinition(subgraph)) {
|
if (isSubgraphDefinition(subgraph)) {
|
||||||
collectMissingNodesAndModels(
|
const paths = subgraphContainerIdMap.get(subgraph.id) ?? []
|
||||||
subgraph.nodes,
|
for (const pathPrefix of paths) {
|
||||||
subgraph.name || subgraph.id
|
collectMissingNodesAndModels(
|
||||||
)
|
subgraph.nodes,
|
||||||
|
pathPrefix,
|
||||||
|
subgraph.name || subgraph.id
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { st } from '@/i18n'
|
||||||
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
@@ -10,8 +12,13 @@ import type {
|
|||||||
PromptError
|
PromptError
|
||||||
} from '@/schemas/apiSchema'
|
} from '@/schemas/apiSchema'
|
||||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import {
|
||||||
|
getAncestorExecutionIds,
|
||||||
|
getParentExecutionIds
|
||||||
|
} from '@/types/nodeIdentification'
|
||||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
import {
|
import {
|
||||||
executionIdToNodeLocatorId,
|
executionIdToNodeLocatorId,
|
||||||
forEachNode,
|
forEachNode,
|
||||||
@@ -20,12 +27,52 @@ import {
|
|||||||
} from '@/utils/graphTraversalUtil'
|
} from '@/utils/graphTraversalUtil'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store dedicated to execution error state management.
|
* @knipIgnoreUsedByStackedPR
|
||||||
*
|
|
||||||
* Extracted from executionStore to separate error-related concerns
|
|
||||||
* (state, computed properties, graph flag propagation, overlay UI)
|
|
||||||
* from execution flow management (progress, queuing, events).
|
|
||||||
*/
|
*/
|
||||||
|
interface MissingNodesError {
|
||||||
|
message: string
|
||||||
|
nodeTypes: MissingNodeType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllNodeErrorFlags(rootGraph: LGraph): void {
|
||||||
|
forEachNode(rootGraph, (node) => {
|
||||||
|
node.has_errors = false
|
||||||
|
if (node.inputs) {
|
||||||
|
for (const slot of node.inputs) {
|
||||||
|
slot.hasErrors = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function markNodeSlotErrors(node: LGraphNode, nodeError: NodeError): void {
|
||||||
|
if (!node.inputs) return
|
||||||
|
for (const error of nodeError.errors) {
|
||||||
|
const slotName = error.extra_info?.input_name
|
||||||
|
if (!slotName) continue
|
||||||
|
const slot = node.inputs.find((s) => s.name === slotName)
|
||||||
|
if (slot) slot.hasErrors = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNodeError(
|
||||||
|
rootGraph: LGraph,
|
||||||
|
executionId: NodeExecutionId,
|
||||||
|
nodeError: NodeError
|
||||||
|
): void {
|
||||||
|
const node = getNodeByExecutionId(rootGraph, executionId)
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
node.has_errors = true
|
||||||
|
markNodeSlotErrors(node, nodeError)
|
||||||
|
|
||||||
|
for (const parentId of getParentExecutionIds(executionId)) {
|
||||||
|
const parentNode = getNodeByExecutionId(rootGraph, parentId)
|
||||||
|
if (parentNode) parentNode.has_errors = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execution error state: node errors, runtime errors, prompt errors, and missing nodes. */
|
||||||
export const useExecutionErrorStore = defineStore('executionError', () => {
|
export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
@@ -33,6 +80,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||||
const lastPromptError = ref<PromptError | null>(null)
|
const lastPromptError = ref<PromptError | null>(null)
|
||||||
|
const missingNodesError = ref<MissingNodesError | null>(null)
|
||||||
|
|
||||||
const isErrorOverlayOpen = ref(false)
|
const isErrorOverlayOpen = ref(false)
|
||||||
|
|
||||||
@@ -49,6 +97,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
lastExecutionError.value = null
|
lastExecutionError.value = null
|
||||||
lastPromptError.value = null
|
lastPromptError.value = null
|
||||||
lastNodeErrors.value = null
|
lastNodeErrors.value = null
|
||||||
|
missingNodesError.value = null
|
||||||
isErrorOverlayOpen.value = false
|
isErrorOverlayOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +106,40 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
lastPromptError.value = null
|
lastPromptError.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMissingNodeTypes(types: MissingNodeType[]) {
|
||||||
|
if (!types.length) {
|
||||||
|
missingNodesError.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const uniqueTypes = types.filter((node) => {
|
||||||
|
// For string entries (group nodes), deduplicate by the string itself.
|
||||||
|
// For object entries, prefer nodeId so multiple instances of the same
|
||||||
|
// type are kept as separate rows; fall back to type if nodeId is absent.
|
||||||
|
const isString = typeof node === 'string'
|
||||||
|
let key: string
|
||||||
|
if (isString) {
|
||||||
|
key = node
|
||||||
|
} else if (node.nodeId != null) {
|
||||||
|
key = String(node.nodeId)
|
||||||
|
} else {
|
||||||
|
key = node.type
|
||||||
|
}
|
||||||
|
if (seen.has(key)) return false
|
||||||
|
seen.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
missingNodesError.value = {
|
||||||
|
message: isCloud
|
||||||
|
? st(
|
||||||
|
'rightSidePanel.missingNodePacks.unsupportedTitle',
|
||||||
|
'Unsupported Node Packs'
|
||||||
|
)
|
||||||
|
: st('rightSidePanel.missingNodePacks.title', 'Missing Node Packs'),
|
||||||
|
nodeTypes: uniqueTypes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lastExecutionErrorNodeLocatorId = computed(() => {
|
const lastExecutionErrorNodeLocatorId = computed(() => {
|
||||||
const err = lastExecutionError.value
|
const err = lastExecutionError.value
|
||||||
if (!err) return null
|
if (!err) return null
|
||||||
@@ -81,9 +164,18 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
|
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Whether any error (node validation, runtime execution, or prompt-level) is present */
|
/** Whether any missing node types are present in the current workflow
|
||||||
|
* @knipIgnoreUsedByStackedPR
|
||||||
|
*/
|
||||||
|
const hasMissingNodes = computed(() => !!missingNodesError.value)
|
||||||
|
|
||||||
|
/** Whether any error (node validation, runtime execution, prompt-level, or missing nodes) is present */
|
||||||
const hasAnyError = computed(
|
const hasAnyError = computed(
|
||||||
() => hasExecutionError.value || hasPromptError.value || hasNodeError.value
|
() =>
|
||||||
|
hasExecutionError.value ||
|
||||||
|
hasPromptError.value ||
|
||||||
|
hasNodeError.value ||
|
||||||
|
hasMissingNodes.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const allErrorExecutionIds = computed<string[]>(() => {
|
const allErrorExecutionIds = computed<string[]>(() => {
|
||||||
@@ -116,13 +208,19 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
/** Count of runtime execution errors (0 or 1) */
|
/** Count of runtime execution errors (0 or 1) */
|
||||||
const executionErrorCount = computed(() => (lastExecutionError.value ? 1 : 0))
|
const executionErrorCount = computed(() => (lastExecutionError.value ? 1 : 0))
|
||||||
|
|
||||||
|
/** Count of missing node errors (0 or 1) */
|
||||||
|
const missingNodeCount = computed(() => (missingNodesError.value ? 1 : 0))
|
||||||
|
|
||||||
/** Total count of all individual errors */
|
/** Total count of all individual errors */
|
||||||
const totalErrorCount = computed(
|
const totalErrorCount = computed(
|
||||||
() =>
|
() =>
|
||||||
promptErrorCount.value + nodeErrorCount.value + executionErrorCount.value
|
promptErrorCount.value +
|
||||||
|
nodeErrorCount.value +
|
||||||
|
executionErrorCount.value +
|
||||||
|
missingNodeCount.value
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Pre-computed Set of graph node IDs (as strings) that have errors in the current graph scope. */
|
/** Graph node IDs (as strings) that have errors in the current graph scope. */
|
||||||
const activeGraphErrorNodeIds = computed<Set<string>>(() => {
|
const activeGraphErrorNodeIds = computed<Set<string>>(() => {
|
||||||
const ids = new Set<string>()
|
const ids = new Set<string>()
|
||||||
if (!app.rootGraph) return ids
|
if (!app.rootGraph) return ids
|
||||||
@@ -150,6 +248,44 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
return ids
|
return ids
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of all execution ID prefixes derived from missing node execution IDs,
|
||||||
|
* including the missing nodes themselves.
|
||||||
|
*
|
||||||
|
* Example: missing node at "65:70:63" → Set { "65", "65:70", "65:70:63" }
|
||||||
|
*/
|
||||||
|
const missingAncestorExecutionIds = computed<Set<NodeExecutionId>>(() => {
|
||||||
|
const ids = new Set<NodeExecutionId>()
|
||||||
|
const error = missingNodesError.value
|
||||||
|
if (!error) return ids
|
||||||
|
|
||||||
|
for (const nodeType of error.nodeTypes) {
|
||||||
|
if (typeof nodeType === 'string') continue
|
||||||
|
if (nodeType.nodeId == null) continue
|
||||||
|
for (const id of getAncestorExecutionIds(String(nodeType.nodeId))) {
|
||||||
|
ids.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeMissingNodeGraphIds = computed<Set<string>>(() => {
|
||||||
|
const ids = new Set<string>()
|
||||||
|
if (!app.rootGraph) return ids
|
||||||
|
|
||||||
|
const activeGraph = canvasStore.currentGraph ?? app.rootGraph
|
||||||
|
|
||||||
|
for (const executionId of missingAncestorExecutionIds.value) {
|
||||||
|
const graphNode = getNodeByExecutionId(app.rootGraph, executionId)
|
||||||
|
if (graphNode?.graph === activeGraph) {
|
||||||
|
ids.add(String(graphNode.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
|
||||||
/** Map of node errors indexed by locator ID. */
|
/** Map of node errors indexed by locator ID. */
|
||||||
const nodeErrorsByLocatorId = computed<Record<NodeLocatorId, NodeError>>(
|
const nodeErrorsByLocatorId = computed<Record<NodeLocatorId, NodeError>>(
|
||||||
() => {
|
() => {
|
||||||
@@ -196,15 +332,11 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
*/
|
*/
|
||||||
const errorAncestorExecutionIds = computed<Set<NodeExecutionId>>(() => {
|
const errorAncestorExecutionIds = computed<Set<NodeExecutionId>>(() => {
|
||||||
const ids = new Set<NodeExecutionId>()
|
const ids = new Set<NodeExecutionId>()
|
||||||
|
|
||||||
for (const executionId of allErrorExecutionIds.value) {
|
for (const executionId of allErrorExecutionIds.value) {
|
||||||
const parts = executionId.split(':')
|
for (const id of getAncestorExecutionIds(executionId)) {
|
||||||
// Add every prefix including the full ID (error leaf node itself)
|
ids.add(id)
|
||||||
for (let i = 1; i <= parts.length; i++) {
|
|
||||||
ids.add(parts.slice(0, i).join(':'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids
|
return ids
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -216,59 +348,28 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
return errorAncestorExecutionIds.value.has(execId)
|
return errorAncestorExecutionIds.value.has(execId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** True if the node has a missing node inside it at any nesting depth.
|
||||||
* Update node and slot error flags when validation errors change.
|
* @knipIgnoreUsedByStackedPR
|
||||||
* Propagates errors up subgraph chains.
|
|
||||||
*/
|
*/
|
||||||
watch(lastNodeErrors, () => {
|
function isContainerWithMissingNode(node: LGraphNode): boolean {
|
||||||
if (!app.rootGraph) return
|
if (!app.rootGraph) return false
|
||||||
|
const execId = getExecutionIdByNode(app.rootGraph, node)
|
||||||
|
if (!execId) return false
|
||||||
|
return missingAncestorExecutionIds.value.has(execId)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all error flags
|
watch(lastNodeErrors, () => {
|
||||||
forEachNode(app.rootGraph, (node) => {
|
const rootGraph = app.rootGraph
|
||||||
node.has_errors = false
|
if (!rootGraph) return
|
||||||
if (node.inputs) {
|
|
||||||
for (const slot of node.inputs) {
|
clearAllNodeErrorFlags(rootGraph)
|
||||||
slot.hasErrors = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!lastNodeErrors.value) return
|
if (!lastNodeErrors.value) return
|
||||||
|
|
||||||
// Set error flags on nodes and slots
|
|
||||||
for (const [executionId, nodeError] of Object.entries(
|
for (const [executionId, nodeError] of Object.entries(
|
||||||
lastNodeErrors.value
|
lastNodeErrors.value
|
||||||
)) {
|
)) {
|
||||||
const node = getNodeByExecutionId(app.rootGraph, executionId)
|
applyNodeError(rootGraph, executionId, nodeError)
|
||||||
if (!node) continue
|
|
||||||
|
|
||||||
node.has_errors = true
|
|
||||||
|
|
||||||
// Mark input slots with errors
|
|
||||||
if (node.inputs) {
|
|
||||||
for (const error of nodeError.errors) {
|
|
||||||
const slotName = error.extra_info?.input_name
|
|
||||||
if (!slotName) continue
|
|
||||||
|
|
||||||
const slot = node.inputs.find((s) => s.name === slotName)
|
|
||||||
if (slot) {
|
|
||||||
slot.hasErrors = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Propagate errors to parent subgraph nodes
|
|
||||||
const parts = executionId.split(':')
|
|
||||||
for (let i = parts.length - 1; i > 0; i--) {
|
|
||||||
const parentExecutionId = parts.slice(0, i).join(':')
|
|
||||||
const parentNode = getNodeByExecutionId(
|
|
||||||
app.rootGraph,
|
|
||||||
parentExecutionId
|
|
||||||
)
|
|
||||||
if (parentNode) {
|
|
||||||
parentNode.has_errors = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,6 +378,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
lastNodeErrors,
|
lastNodeErrors,
|
||||||
lastExecutionError,
|
lastExecutionError,
|
||||||
lastPromptError,
|
lastPromptError,
|
||||||
|
missingNodesError,
|
||||||
|
|
||||||
// Clearing
|
// Clearing
|
||||||
clearAllErrors,
|
clearAllErrors,
|
||||||
@@ -291,16 +393,21 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
hasExecutionError,
|
hasExecutionError,
|
||||||
hasPromptError,
|
hasPromptError,
|
||||||
hasNodeError,
|
hasNodeError,
|
||||||
|
hasMissingNodes,
|
||||||
hasAnyError,
|
hasAnyError,
|
||||||
allErrorExecutionIds,
|
allErrorExecutionIds,
|
||||||
totalErrorCount,
|
totalErrorCount,
|
||||||
lastExecutionErrorNodeId,
|
lastExecutionErrorNodeId,
|
||||||
activeGraphErrorNodeIds,
|
activeGraphErrorNodeIds,
|
||||||
|
activeMissingNodeGraphIds,
|
||||||
|
|
||||||
|
// Missing node actions
|
||||||
|
setMissingNodeTypes,
|
||||||
|
|
||||||
// Lookup helpers
|
// Lookup helpers
|
||||||
getNodeErrors,
|
getNodeErrors,
|
||||||
slotHasError,
|
slotHasError,
|
||||||
errorAncestorExecutionIds,
|
isContainerWithInternalError,
|
||||||
isContainerWithInternalError
|
isContainerWithMissingNode
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export type MissingNodeType =
|
|||||||
// Primarily used by group nodes.
|
// Primarily used by group nodes.
|
||||||
| {
|
| {
|
||||||
type: string
|
type: string
|
||||||
|
nodeId?: string | number
|
||||||
|
cnrId?: string
|
||||||
hint?: string
|
hint?: string
|
||||||
action?: {
|
action?: {
|
||||||
text: string
|
text: string
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ export function createNodeExecutionId(nodeIds: NodeId[]): NodeExecutionId {
|
|||||||
* Returns all ancestor execution IDs for a given execution ID, including itself.
|
* Returns all ancestor execution IDs for a given execution ID, including itself.
|
||||||
*
|
*
|
||||||
* Example: "65:70:63" → ["65", "65:70", "65:70:63"]
|
* Example: "65:70:63" → ["65", "65:70", "65:70:63"]
|
||||||
* @knipIgnoreUsedByStackedPR
|
|
||||||
*/
|
*/
|
||||||
export function getAncestorExecutionIds(
|
export function getAncestorExecutionIds(
|
||||||
executionId: string | NodeExecutionId
|
executionId: string | NodeExecutionId
|
||||||
@@ -141,7 +140,6 @@ export function getAncestorExecutionIds(
|
|||||||
* Returns all ancestor execution IDs for a given execution ID, excluding itself.
|
* Returns all ancestor execution IDs for a given execution ID, excluding itself.
|
||||||
*
|
*
|
||||||
* Example: "65:70:63" → ["65", "65:70"]
|
* Example: "65:70:63" → ["65", "65:70"]
|
||||||
* @knipIgnoreUsedByStackedPR
|
|
||||||
*/
|
*/
|
||||||
export function getParentExecutionIds(
|
export function getParentExecutionIds(
|
||||||
executionId: string | NodeExecutionId
|
executionId: string | NodeExecutionId
|
||||||
|
|||||||
Reference in New Issue
Block a user