Use new error dialog for queue prompt errors (#3266)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-03-28 13:51:00 -04:00
committed by GitHub
parent 504b717575
commit 04af8cda4d
11 changed files with 87 additions and 51 deletions

View File

@@ -326,4 +326,18 @@ test.describe('Error dialog', () => {
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
test('Should display an error dialog when prompt execution fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(async () => {
const app = window['app']
app.api.queuePrompt = () => {
throw new Error('Error on queuePrompt!')
}
await app.queuePrompt(0)
})
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
})

View File

@@ -965,7 +965,8 @@
"defaultTitle": "An error occurred",
"loadWorkflowTitle": "Loading aborted due to error reloading workflow data",
"noStackTrace": "No stacktrace available",
"extensionFileHint": "This may be due to the following script"
"extensionFileHint": "This may be due to the following script",
"promptExecutionError": "Prompt execution failed"
},
"desktopUpdate": {
"title": "Updating ComfyUI Desktop",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "Ocurrió un error",
"extensionFileHint": "Esto puede deberse al siguiente script",
"loadWorkflowTitle": "La carga se interrumpió debido a un error al recargar los datos del flujo de trabajo",
"noStackTrace": "No hay seguimiento de pila disponible"
"noStackTrace": "No hay seguimiento de pila disponible",
"promptExecutionError": "La ejecución del prompt falló"
},
"g": {
"about": "Acerca de",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "Une erreur est survenue",
"extensionFileHint": "Cela peut être dû au script suivant",
"loadWorkflowTitle": "Chargement interrompu en raison d'une erreur de rechargement des données de workflow",
"noStackTrace": "Aucune trace de pile disponible"
"noStackTrace": "Aucune trace de pile disponible",
"promptExecutionError": "L'exécution de l'invite a échoué"
},
"g": {
"about": "À propos",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "エラーが発生しました",
"extensionFileHint": "これは次のスクリプトが原因かもしれません",
"loadWorkflowTitle": "ワークフローデータの再読み込みエラーにより、読み込みが中止されました",
"noStackTrace": "スタックトレースは利用できません"
"noStackTrace": "スタックトレースは利用できません",
"promptExecutionError": "プロンプトの実行に失敗しました"
},
"g": {
"about": "情報",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "오류가 발생했습니다",
"extensionFileHint": "다음 스크립트 때문일 수 있습니다",
"loadWorkflowTitle": "워크플로우 데이터를 다시 로드하는 중 오류로 인해 로드가 중단되었습니다",
"noStackTrace": "스택 추적이 사용할 수 없습니다"
"noStackTrace": "스택 추적이 사용할 수 없습니다",
"promptExecutionError": "프롬프트 실행 실패"
},
"g": {
"about": "정보",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "Произошла ошибка",
"extensionFileHint": "Это может быть связано со следующим скриптом",
"loadWorkflowTitle": "Загрузка прервана из-за ошибки при перезагрузке данных рабочего процесса",
"noStackTrace": "Стек вызовов недоступен"
"noStackTrace": "Стек вызовов недоступен",
"promptExecutionError": "Ошибка выполнения запроса"
},
"g": {
"about": "О программе",

View File

@@ -119,7 +119,8 @@
"defaultTitle": "发生错误",
"extensionFileHint": "这可能是由于以下脚本",
"loadWorkflowTitle": "由于重新加载工作流数据出错,加载被中止",
"noStackTrace": "无可用堆栈跟踪"
"noStackTrace": "无可用堆栈跟踪",
"promptExecutionError": "提示执行失败"
},
"g": {
"about": "关于",

View File

@@ -255,14 +255,26 @@ export function validateTaskItem(taskItem: unknown) {
const zEmbeddingsResponse = z.array(z.string())
const zExtensionsResponse = z.array(z.string())
const zError = z.object({
type: z.string(),
message: z.string(),
details: z.string(),
extra_info: z.record(z.string(), z.any())
})
const zNodeError = z.object({
errors: z.array(zError),
class_type: z.string(),
dependent_outputs: z.array(z.any())
})
const zPromptResponse = z.object({
node_errors: z.array(z.string()).optional(),
node_errors: z.record(zNodeId, zNodeError).optional(),
prompt_id: z.string().optional(),
exec_info: z
.object({
queue_remaining: z.number().optional()
})
.optional()
.optional(),
error: z.union([z.string(), zError])
})
const zDeviceStats = z.object({
@@ -414,6 +426,7 @@ const zSettings = z.record(z.any()).and(
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>
export type ExtensionsResponse = z.infer<typeof zExtensionsResponse>
export type PromptResponse = z.infer<typeof zPromptResponse>
export type NodeError = z.infer<typeof zNodeError>
export type Settings = z.infer<typeof zSettings>
export type DeviceStats = z.infer<typeof zDeviceStats>
export type SystemStats = z.infer<typeof zSystemStats>

View File

@@ -145,6 +145,35 @@ export interface ComfyApi extends EventTarget {
): void
}
export class PromptExecutionError extends Error {
response: PromptResponse
constructor(response: PromptResponse) {
super('Prompt execution failed')
this.response = response
}
override toString() {
let message = super.message
if (typeof this.response.error === 'string') {
message += ': ' + this.response.error
} else if (this.response.error.details) {
message += ': ' + this.response.error.details
}
for (const [_, nodeError] of Object.entries(
this.response.node_errors ?? []
)) {
message += '\n' + nodeError.class_type + ':'
for (const errorReason of nodeError.errors) {
message += '\n - ' + errorReason.message + ': ' + errorReason.details
}
}
return message
}
}
export class ComfyApi extends EventTarget {
#registered = new Set()
api_host: string
@@ -464,9 +493,10 @@ export class ComfyApi extends EventTarget {
}
/**
*
* 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
* @throws {PromptExecutionError} If the prompt fails to execute
*/
async queuePrompt(
number: number,
@@ -496,9 +526,7 @@ export class ComfyApi extends EventTarget {
})
if (res.status !== 200) {
throw {
response: await res.json()
}
throw new PromptExecutionError(await res.json())
}
return await res.json()

View File

@@ -12,7 +12,7 @@ import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'
import { st, t } from '@/i18n'
import type { ResultItem } from '@/schemas/apiSchema'
import type { NodeError, ResultItem } from '@/schemas/apiSchema'
import {
ComfyApiWorkflow,
type ComfyWorkflowJSON,
@@ -47,7 +47,7 @@ import { executeWidgetsCallback, isImageNode } from '@/utils/litegraphUtil'
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { type ComfyApi, api } from './api'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'
import {
getFlacMetadata,
@@ -121,7 +121,7 @@ export class ComfyApp {
dragOverNode: LGraphNode | null = null
// @ts-expect-error fixme ts strict error
canvasEl: HTMLCanvasElement
lastNodeErrors: any[] | null = null
lastNodeErrors: Record<NodeId, NodeError> | null = null
/** @type {ExecutionErrorWsMessage} */
lastExecutionError: { node_id?: NodeId } | null = null
configuringGraph: boolean = false
@@ -244,7 +244,7 @@ export class ComfyApp {
}
getPreviewFormatParam() {
let preview_format = this.ui.settings.getSettingValue('Comfy.PreviewFormat')
let preview_format = useSettingStore().get('Comfy.PreviewFormat')
if (preview_format) return `&preview=${preview_format}`
else return ''
}
@@ -570,7 +570,6 @@ export class ComfyApp {
// @ts-expect-error fixme ts strict error
const res = origDrawNodeShape.apply(this, arguments)
// @ts-expect-error fixme ts strict error
const nodeErrors = self.lastNodeErrors?.[node.id]
let color = null
@@ -1223,32 +1222,6 @@ export class ComfyApp {
})
}
// @ts-expect-error fixme ts strict error
#formatPromptError(error) {
if (error == null) {
return '(unknown error)'
} else if (typeof error === 'string') {
return error
} else if (error.stack && error.message) {
return error.toString()
} else if (error.response) {
let message = error.response.error.message
if (error.response.error.details)
message += ': ' + error.response.error.details
for (const [_, nodeError] of Object.entries(error.response.node_errors)) {
// @ts-expect-error
message += '\n' + nodeError.class_type + ':'
// @ts-expect-error
for (const errorReason of nodeError.errors) {
message +=
'\n - ' + errorReason.message + ': ' + errorReason.details
}
}
return message
}
return '(unknown error)'
}
async queuePrompt(number: number, batchCount: number = 1): Promise<boolean> {
this.#queueItems.push({ number, batchCount })
@@ -1287,13 +1260,14 @@ export class ComfyApp {
}
} catch (error) {}
}
} catch (error) {
const formattedError = this.#formatPromptError(error)
this.ui.dialog.show(formattedError)
// @ts-expect-error fixme ts strict error
if (error.response) {
// @ts-expect-error fixme ts strict error
this.lastNodeErrors = error.response.node_errors
} catch (error: unknown) {
useDialogService().showErrorDialog(error, {
title: t('errorDialog.promptExecutionError'),
reportType: 'promptExecutionError'
})
if (error instanceof PromptExecutionError) {
this.lastNodeErrors = error.response.node_errors ?? null
this.canvas.draw(true, true)
}
break