From 04af8cda4d9309a2e2ee5ec55d6f5bc2bf6986fd Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Fri, 28 Mar 2025 13:51:00 -0400 Subject: [PATCH] Use new error dialog for queue prompt errors (#3266) Co-authored-by: github-actions --- browser_tests/tests/dialog.spec.ts | 14 +++++++++ src/locales/en/main.json | 3 +- src/locales/es/main.json | 3 +- src/locales/fr/main.json | 3 +- src/locales/ja/main.json | 3 +- src/locales/ko/main.json | 3 +- src/locales/ru/main.json | 3 +- src/locales/zh/main.json | 3 +- src/schemas/apiSchema.ts | 17 ++++++++-- src/scripts/api.ts | 36 ++++++++++++++++++--- src/scripts/app.ts | 50 +++++++----------------------- 11 files changed, 87 insertions(+), 51 deletions(-) diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index d309b2b1e..48267b1ac 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -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() + }) }) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 51815e4fe..f140d1c48 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -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", diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 8849f0a42..606e0a91c 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -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", diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index e16624ace..a13cec545 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -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", diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index fb9c9810f..0fe8d149f 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -119,7 +119,8 @@ "defaultTitle": "エラーが発生しました", "extensionFileHint": "これは次のスクリプトが原因かもしれません", "loadWorkflowTitle": "ワークフローデータの再読み込みエラーにより、読み込みが中止されました", - "noStackTrace": "スタックトレースは利用できません" + "noStackTrace": "スタックトレースは利用できません", + "promptExecutionError": "プロンプトの実行に失敗しました" }, "g": { "about": "情報", diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index f060a967a..c6c1fc5b7 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -119,7 +119,8 @@ "defaultTitle": "오류가 발생했습니다", "extensionFileHint": "다음 스크립트 때문일 수 있습니다", "loadWorkflowTitle": "워크플로우 데이터를 다시 로드하는 중 오류로 인해 로드가 중단되었습니다", - "noStackTrace": "스택 추적이 사용할 수 없습니다" + "noStackTrace": "스택 추적이 사용할 수 없습니다", + "promptExecutionError": "프롬프트 실행 실패" }, "g": { "about": "정보", diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 960cb57fb..8ca2b7ad1 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -119,7 +119,8 @@ "defaultTitle": "Произошла ошибка", "extensionFileHint": "Это может быть связано со следующим скриптом", "loadWorkflowTitle": "Загрузка прервана из-за ошибки при перезагрузке данных рабочего процесса", - "noStackTrace": "Стек вызовов недоступен" + "noStackTrace": "Стек вызовов недоступен", + "promptExecutionError": "Ошибка выполнения запроса" }, "g": { "about": "О программе", diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index e251f68dc..aebc7a326 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -119,7 +119,8 @@ "defaultTitle": "发生错误", "extensionFileHint": "这可能是由于以下脚本", "loadWorkflowTitle": "由于重新加载工作流数据出错,加载被中止", - "noStackTrace": "无可用堆栈跟踪" + "noStackTrace": "无可用堆栈跟踪", + "promptExecutionError": "提示执行失败" }, "g": { "about": "关于", diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index aeb3522cd..c4c3ebabd 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -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 export type ExtensionsResponse = z.infer export type PromptResponse = z.infer +export type NodeError = z.infer export type Settings = z.infer export type DeviceStats = z.infer export type SystemStats = z.infer diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 548d98d45..c18d9b04a 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -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() diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 78c47a605..dc6ec6221 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -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 | 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 { 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