|
| 1 | +import { app } from "../../scripts/app.js"; |
| 2 | +import { api } from "../../scripts/api.js"; |
| 3 | + |
| 4 | +let lastApiError = null; |
| 5 | + |
| 6 | +function setupExecutionErrorListener() { |
| 7 | + api.addEventListener("execution_error", (e) => { |
| 8 | + const errorData = e.detail; |
| 9 | + console.log("🔧 [API] High-quality error event captured:", errorData); |
| 10 | + |
| 11 | + lastApiError = { |
| 12 | + type: 'execution_error', |
| 13 | + nodeId: errorData.node_id, |
| 14 | + nodeType: errorData.node_type, |
| 15 | + message: errorData.exception_message, |
| 16 | + traceback: errorData.traceback, |
| 17 | + timestamp: new Date().toISOString(), |
| 18 | + captureTime: Date.now(), |
| 19 | + workflow: getWorkflowData() |
| 20 | + }; |
| 21 | + |
| 22 | + if (errorData.node_id) { |
| 23 | + setTimeout(() => addErrorMarkerToNode(errorData.node_id, lastApiError), 200); |
| 24 | + } |
| 25 | + }); |
| 26 | +} |
| 27 | + |
| 28 | +function observeErrorDialogs() { |
| 29 | + console.log("🔧 Starting to observe error dialogs..."); |
| 30 | + |
| 31 | + const observer = new MutationObserver((mutations) => { |
| 32 | + mutations.forEach((mutation) => { |
| 33 | + mutation.addedNodes.forEach((node) => { |
| 34 | + if (node.nodeType === Node.ELEMENT_NODE) { |
| 35 | + checkForErrorDialog(node); |
| 36 | + } |
| 37 | + }); |
| 38 | + }); |
| 39 | + }); |
| 40 | + |
| 41 | + observer.observe(document.body, { |
| 42 | + childList: true, |
| 43 | + subtree: true |
| 44 | + }); |
| 45 | + |
| 46 | + // 也检查已存在的元素 |
| 47 | + setTimeout(() => { |
| 48 | + const existingDialogs = document.querySelectorAll('div, [class*="modal"], [class*="dialog"], [class*="popup"]'); |
| 49 | + existingDialogs.forEach(checkForErrorDialog); |
| 50 | + }, 1000); |
| 51 | +} |
| 52 | + |
| 53 | +function checkForErrorDialog(element) { |
| 54 | + try { |
| 55 | + // 避免在自己的按钮上触发 |
| 56 | + if (element.closest('.error-fixer-button-container') || element.classList.contains('error-fixer-button')) { |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + const textContent = element.textContent || element.innerText || ''; |
| 61 | + |
| 62 | + // 多种错误模式检测(参考工作版本) |
| 63 | + const errorPatterns = [ |
| 64 | + /error/i, |
| 65 | + /exception/i, |
| 66 | + /failed/i, |
| 67 | + /object has no attribute/i, |
| 68 | + /nonetype/i, |
| 69 | + /traceback/i, |
| 70 | + /invalid/i, |
| 71 | + /cannot/i, |
| 72 | + /unable/i, |
| 73 | + /错误/i, |
| 74 | + /失败/i |
| 75 | + ]; |
| 76 | + |
| 77 | + const hasErrorText = errorPatterns.some(pattern => pattern.test(textContent)); |
| 78 | + |
| 79 | + // 检查是否是弹窗样式(参考工作版本的判断逻辑) |
| 80 | + const style = window.getComputedStyle(element); |
| 81 | + const isModal = style.position === 'fixed' || style.position === 'absolute'; |
| 82 | + const hasHighZIndex = parseInt(style.zIndex) > 100; |
| 83 | + |
| 84 | + if (hasErrorText && (isModal || hasHighZIndex || element.offsetParent === document.body)) { |
| 85 | + console.log("🔧 Found potential error dialog:", element); |
| 86 | + handleErrorDialog(element, textContent); |
| 87 | + } |
| 88 | + |
| 89 | + // 特别检查ComfyUI特定错误 |
| 90 | + if (textContent.includes("'NoneType' object has no attribute 'shape'") || |
| 91 | + textContent.includes("K采样器") || |
| 92 | + textContent.includes("帮助修复这个")) { |
| 93 | + console.log("🔧 Found ComfyUI specific error dialog:", element); |
| 94 | + handleErrorDialog(element, textContent); |
| 95 | + } |
| 96 | + |
| 97 | + } catch (e) { |
| 98 | + // 忽略检查错误 |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +function handleErrorDialog(dialogElement, errorText) { |
| 103 | + try { |
| 104 | + console.log("🔧 Processing error dialog:", errorText.substring(0, 100)); |
| 105 | + |
| 106 | + // 防止重复处理 |
| 107 | + if (dialogElement.querySelector('.error-fixer-button')) { |
| 108 | + return; |
| 109 | + } |
| 110 | + |
| 111 | + let errorInfo; |
| 112 | + const now = Date.now(); |
| 113 | + |
| 114 | + if (lastApiError && (now - lastApiError.captureTime) < 3000) { |
| 115 | + console.log("🔧 Using high-quality data from recent API event."); |
| 116 | + errorInfo = lastApiError; |
| 117 | + } else { |
| 118 | + console.log("🔧 API data not found or too old. Falling back to parsing dialog text."); |
| 119 | + |
| 120 | + // 提取错误信息(参考工作版本) |
| 121 | + let errorMessage = errorText.trim(); |
| 122 | + let nodeName = ''; |
| 123 | + let nodeType = ''; |
| 124 | + let nodeId = ''; |
| 125 | + |
| 126 | + // 尝试从对话框标题中提取节点信息 |
| 127 | + const titleElement = dialogElement.querySelector('h1, h2, h3, .title, [class*="title"]'); |
| 128 | + if (titleElement) { |
| 129 | + const titleText = titleElement.textContent || titleElement.innerText; |
| 130 | + if (titleText && titleText !== errorMessage) { |
| 131 | + nodeName = titleText.trim(); |
| 132 | + nodeType = titleText.trim(); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + // 从错误信息中提取节点ID |
| 137 | + const nodeIdMatch = errorMessage.match(/node[^\d]*(\d+)/i); |
| 138 | + if (nodeIdMatch) { |
| 139 | + nodeId = nodeIdMatch[1]; |
| 140 | + } |
| 141 | + |
| 142 | + errorInfo = { |
| 143 | + type: 'dialog_text_error', |
| 144 | + message: errorMessage, |
| 145 | + nodeName: nodeName, |
| 146 | + nodeType: nodeType, |
| 147 | + nodeId: nodeId, |
| 148 | + timestamp: new Date().toISOString() |
| 149 | + }; |
| 150 | + } |
| 151 | + |
| 152 | + // 添加按钮到对话框 |
| 153 | + addFixButtonToDialog(dialogElement, errorInfo); |
| 154 | + |
| 155 | + console.log("🔧 Error processed successfully"); |
| 156 | + |
| 157 | + } catch (e) { |
| 158 | + console.error("🔧 Error processing dialog:", e); |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +function addFixButtonToDialog(dialogElement, errorInfo) { |
| 163 | + try { |
| 164 | + const fixButton = createFixButton(errorInfo); |
| 165 | + |
| 166 | + const insertionTargets = [ |
| 167 | + dialogElement.querySelector('.buttons'), |
| 168 | + dialogElement.querySelector('.actions'), |
| 169 | + dialogElement.querySelector('.footer'), |
| 170 | + dialogElement.querySelector('button')?.parentNode, |
| 171 | + dialogElement.querySelector('.comfy-manager-dialog-actions'), // ComfyUI-Manager |
| 172 | + dialogElement.querySelector('.dialog_actions'), |
| 173 | + dialogElement.querySelector('.comfy-modal-actions'), |
| 174 | + dialogElement.querySelector('.dialog_content'), |
| 175 | + dialogElement.querySelector('.modal_content'), |
| 176 | + dialogElement // 最终备选 |
| 177 | + ]; |
| 178 | + |
| 179 | + let buttonAdded = false; |
| 180 | + for (const target of insertionTargets) { |
| 181 | + if (target) { |
| 182 | + console.log("🔧 Adding button to dialog target:", target.className || target.nodeName); |
| 183 | + target.appendChild(fixButton); |
| 184 | + buttonAdded = true; |
| 185 | + break; |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + if (!buttonAdded) { |
| 190 | + console.log("🔧 Creating new button container"); |
| 191 | + const buttonContainer = document.createElement('div'); |
| 192 | + buttonContainer.className = 'error-fixer-button-container'; |
| 193 | + buttonContainer.style.cssText = ` |
| 194 | + text-align: center !important; |
| 195 | + padding: 10px !important; |
| 196 | + border-top: 1px solid #333 !important; |
| 197 | + margin-top: 10px !important; |
| 198 | + `; |
| 199 | + buttonContainer.appendChild(fixButton); |
| 200 | + dialogElement.appendChild(buttonContainer); |
| 201 | + } |
| 202 | + |
| 203 | + } catch (e) { |
| 204 | + console.error("🔧 Error adding button to dialog:", e); |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +function createFixButton(errorInfo) { |
| 209 | + const button = document.createElement('button'); |
| 210 | + button.className = 'error-fixer-button'; |
| 211 | + button.innerHTML = '🔧 Error Fixer Online'; |
| 212 | + |
| 213 | + // 使用工作版本的样式 |
| 214 | + button.style.cssText = ` |
| 215 | + background: #ff6b35 !important; |
| 216 | + color: white !important; |
| 217 | + border: none !important; |
| 218 | + padding: 8px 16px !important; |
| 219 | + border-radius: 6px !important; |
| 220 | + cursor: pointer !important; |
| 221 | + margin: 10px 5px !important; |
| 222 | + font-size: 14px !important; |
| 223 | + font-weight: bold !important; |
| 224 | + transition: all 0.3s ease !important; |
| 225 | + box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; |
| 226 | + z-index: 10000 !important; |
| 227 | + display: inline-block !important; |
| 228 | + `; |
| 229 | + |
| 230 | + button.addEventListener('mouseenter', () => { |
| 231 | + button.style.background = '#e55a2b'; |
| 232 | + button.style.transform = 'translateY(-1px)'; |
| 233 | + }); |
| 234 | + |
| 235 | + button.addEventListener('mouseleave', () => { |
| 236 | + button.style.background = '#ff6b35'; |
| 237 | + button.style.transform = 'translateY(0)'; |
| 238 | + }); |
| 239 | + |
| 240 | + button.addEventListener('click', (e) => { |
| 241 | + e.preventDefault(); |
| 242 | + e.stopPropagation(); |
| 243 | + console.log("🔧 Fix button clicked for error:", errorInfo.message); |
| 244 | + openErrorFixPage(errorInfo); |
| 245 | + }); |
| 246 | + |
| 247 | + return button; |
| 248 | +} |
| 249 | + |
| 250 | +function openErrorFixPage(errorInfo) { |
| 251 | + const baseUrl = "https://bug.aix.ink"; |
| 252 | + const params = new URLSearchParams({ |
| 253 | + q: errorInfo.message, |
| 254 | + source: 'comfyui_plugin_final' |
| 255 | + }); |
| 256 | + window.open(`${baseUrl}?${params.toString()}`, '_blank'); |
| 257 | +} |
| 258 | + |
| 259 | +function getWorkflowData() { |
| 260 | + try { |
| 261 | + return app.graph ? JSON.stringify(app.graph.serialize()) : "{}"; |
| 262 | + } catch (e) { |
| 263 | + return `{"error": "Unable to capture workflow: ${e.message}"}`; |
| 264 | + } |
| 265 | +} |
| 266 | + |
| 267 | +function addErrorMarkerToNode(nodeId, errorInfo) { |
| 268 | + const targetNode = app.graph?.getNodeById(nodeId); |
| 269 | + if (!targetNode) return; |
| 270 | + |
| 271 | + if (targetNode.hasErrorMarker) { |
| 272 | + targetNode.errorInfo = errorInfo; |
| 273 | + return; |
| 274 | + } |
| 275 | + targetNode.errorInfo = errorInfo; |
| 276 | + const originalOnDrawForeground = targetNode.onDrawForeground; |
| 277 | + targetNode.onDrawForeground = function(ctx) { |
| 278 | + if (originalOnDrawForeground) originalOnDrawForeground.apply(this, arguments); |
| 279 | + if (typeof LiteGraph === "undefined") return; |
| 280 | + const iconSize = 22, margin = 4, titleHeight = LiteGraph.NODE_TITLE_HEIGHT || 30; |
| 281 | + const existingIconsWidth = 38; |
| 282 | + const iconX = this.size[0] - iconSize - margin - existingIconsWidth; |
| 283 | + const iconY = -titleHeight + (titleHeight - iconSize) / 2; |
| 284 | + ctx.save(); |
| 285 | + ctx.fillStyle = "#E53E3E"; |
| 286 | + ctx.beginPath(); |
| 287 | + ctx.arc(iconX + iconSize / 2, iconY + iconSize / 2, iconSize / 2, 0, 2 * Math.PI); |
| 288 | + ctx.fill(); |
| 289 | + ctx.strokeStyle = "white"; |
| 290 | + ctx.lineWidth = 1.5; |
| 291 | + ctx.stroke(); |
| 292 | + ctx.font = `bold ${iconSize * 0.65}px Arial`; |
| 293 | + ctx.fillStyle = "white"; |
| 294 | + ctx.textAlign = "center"; |
| 295 | + ctx.textBaseline = "middle"; |
| 296 | + ctx.fillText("🔧", iconX + iconSize / 2, iconY + iconSize / 2 + 1); |
| 297 | + ctx.restore(); |
| 298 | + this.errorIconBounds = { x: iconX, y: iconY, width: iconSize, height: iconSize }; |
| 299 | + }; |
| 300 | + const originalOnMouseDown = targetNode.onMouseDown; |
| 301 | + targetNode.onMouseDown = function(e, localPos) { |
| 302 | + if (this.errorIconBounds && typeof LiteGraph !== "undefined" && LiteGraph.isInsideRectangle(localPos[0], localPos[1], this.errorIconBounds.x, this.errorIconBounds.y, this.errorIconBounds.width, this.errorIconBounds.height)) { |
| 303 | + openErrorFixPage(this.errorInfo); |
| 304 | + return true; |
| 305 | + } |
| 306 | + return originalOnMouseDown ? originalOnMouseDown.apply(this, arguments) : false; |
| 307 | + }; |
| 308 | + targetNode.hasErrorMarker = true; |
| 309 | + app.canvas?.setDirty(true, true); |
| 310 | +} |
| 311 | + |
| 312 | +// 插件注册 |
| 313 | +app.registerExtension({ |
| 314 | + name: "ComfyUI.ErrorFixer.Final", |
| 315 | + async setup() { |
| 316 | + console.log("🔧 Error Fixer Plugin [Final Version] Loaded. Using robust DOM observation."); |
| 317 | + |
| 318 | + setTimeout(() => { |
| 319 | + observeErrorDialogs(); |
| 320 | + setupExecutionErrorListener(); |
| 321 | + console.log("🔧 Error monitoring started."); |
| 322 | + }, 2000); |
| 323 | + } |
| 324 | +}); |
0 commit comments