您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
移除轮询,使用WebSocket实时接收生成结果。IndexedDB无限容量缓存、自动暗黑模式、双指缩放、GPU硬件加速、requestIdleCallback非阻塞压缩、防抖节流性能优化、LRU智能淘汰、生成历史管理等完整功能。全新UI设计。
// ==UserScript== // @name 公益酒馆ComfyUI插图脚本 (WebSocket实时版 - 终极优化版) // @namespace http://tampermonkey.net/ // @version 40.1 // UI优化:全新配色方案,深色渐变主题,蓝色科技风,增强交互动效 // @license GPL // @description 移除轮询,使用WebSocket实时接收生成结果。IndexedDB无限容量缓存、自动暗黑模式、双指缩放、GPU硬件加速、requestIdleCallback非阻塞压缩、防抖节流性能优化、LRU智能淘汰、生成历史管理等完整功能。全新UI设计。 // @author feng zheng (升级 by Gemini, 终极优化 by Claude) // @match *://*/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @require https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js // @require https://code.jquery.com/ui/1.13.2/jquery-ui.min.js // ==/UserScript== (function() { 'use strict'; // --- Configuration Constants --- const BUTTON_ID = 'comfyui-launcher-button'; const PANEL_ID = 'comfyui-panel'; const STORAGE_KEY_IMAGES = 'comfyui_generated_images'; const STORAGE_KEY_PROMPT_PREFIX = 'comfyui_prompt_prefix'; const STORAGE_KEY_MAX_WIDTH = 'comfyui_image_max_width'; const STORAGE_KEY_CACHE_LIMIT = 'comfyui_cache_limit'; const COOLDOWN_DURATION_MS = 60000; const CONFIG_VERSION = '2.0'; const ENCRYPTION_KEY = 42; // 简单的XOR密钥 // --- Notification Manager --- function showNotification(message, type = 'info', level = 'standard') { if (typeof toastr === 'undefined') return; const userLevel = cachedSettings.notificationLevel; // silent: 只显示错误 // standard: 显示成功和错误 // verbose: 显示所有 if (userLevel === 'silent' && type !== 'error') return; if (userLevel === 'standard' && type === 'info') return; toastr[type](message); } // --- Security and Utility Functions --- function encryptApiKey(key) { if (!key) return ''; try { return btoa(key.split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ ENCRYPTION_KEY)).join('')); } catch (e) { console.error('API密钥加密失败:', e); return key; } } function decryptApiKey(encryptedKey) { if (!encryptedKey) return ''; try { return atob(encryptedKey).split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ ENCRYPTION_KEY)).join(''); } catch (e) { console.error('API密钥解密失败:', e); return encryptedKey; } } function sanitizePrompt(prompt) { if (!prompt || typeof prompt !== 'string') return ''; // 创建临时div元素进行HTML转义 const div = document.createElement('div'); div.textContent = prompt; return div.innerHTML .replace(/[<>]/g, '') // 移除尖括号 .replace(/javascript:/gi, '') // 移除javascript:协议 .replace(/on\w+\s*=/gi, '') // 移除事件处理器 .trim(); } function validateUrl(url) { if (!url || typeof url !== 'string') return false; try { const urlObj = new URL(url); // 强制要求HTTPS(除了localhost) if (urlObj.hostname !== 'localhost' && urlObj.hostname !== '127.0.0.1' && urlObj.protocol !== 'https:') { return false; } return ['http:', 'https:'].includes(urlObj.protocol); } catch (e) { return false; } } function validateConfig(config) { const errors = []; if (!config.comfyuiUrl || typeof config.comfyuiUrl !== 'string') { errors.push('调度器URL无效'); } else if (!validateUrl(config.comfyuiUrl)) { errors.push('调度器URL格式错误或不安全'); } if (config.maxWidth && (typeof config.maxWidth !== 'number' || config.maxWidth < 100 || config.maxWidth > 2000)) { errors.push('图片最大宽度必须在100-2000像素之间'); } if (config.cacheLimit && (typeof config.cacheLimit !== 'number' || config.cacheLimit < 1 || config.cacheLimit > 100)) { errors.push('缓存限制必须在1-100之间'); } return errors; } // --- Error Handling Classes --- class ComfyUIError extends Error { constructor(message, type = 'UNKNOWN', details = {}) { super(message); this.name = 'ComfyUIError'; this.type = type; this.details = details; this.timestamp = Date.now(); } } class ErrorHandler { static handle(error, context = '') { const errorLog = { message: error.message || '未知错误', type: error.type || 'UNKNOWN', context, timestamp: error.timestamp || Date.now(), stack: error.stack }; console.error('[ComfyUI Error]', errorLog); const userMessage = this.getUserFriendlyMessage(error.type, error.message); if (typeof toastr !== 'undefined') { toastr.error(userMessage); } return errorLog; } static getUserFriendlyMessage(type, originalMessage) { const messages = { 'NETWORK': '网络连接失败,请检查网络连接和服务器状态', 'AUTH': '身份验证失败,请检查API密钥是否正确', 'GENERATION': '图片生成失败,请稍后重试', 'CONFIG': '配置错误,请检查设置', 'VALIDATION': '输入验证失败,请检查输入内容', 'CACHE': '缓存操作失败', 'WEBSOCKET': 'WebSocket连接失败,将尝试重新连接' }; return messages[type] || originalMessage || '操作失败,请重试'; } } // --- Smart Reconnection System --- class SmartReconnector { constructor(getWsUrl, onConnect, onDisconnect) { this.getWsUrl = getWsUrl; this.onConnect = onConnect; this.onDisconnect = onDisconnect; this.reconnectAttempts = 0; this.maxAttempts = 10; this.baseDelay = 1000; this.maxDelay = 30000; this.isOnline = navigator.onLine; this.setupNetworkMonitoring(); } setupNetworkMonitoring() { window.addEventListener('online', () => { this.isOnline = true; if (typeof toastr !== 'undefined') { toastr.success('网络连接已恢复,正在重新连接...'); } this.reconnect(); }); window.addEventListener('offline', () => { this.isOnline = false; if (typeof toastr !== 'undefined') { toastr.warning('网络连接已断开,将在恢复后自动重连'); } this.onDisconnect(); }); } async reconnect() { if (!this.isOnline) { console.log('网络离线,暂停重连尝试'); return false; } if (this.reconnectAttempts >= this.maxAttempts) { this.showPermanentDisconnectionNotice(); return false; } const delay = Math.min( this.baseDelay * Math.pow(2, this.reconnectAttempts), this.maxDelay ); console.log(`尝试重连 (${this.reconnectAttempts + 1}/${this.maxAttempts}),延迟 ${delay}ms`); await this.wait(delay); try { await this.attemptConnection(); this.reconnectAttempts = 0; if (typeof toastr !== 'undefined') { toastr.success('WebSocket连接已恢复!'); } return true; } catch (error) { this.reconnectAttempts++; console.warn(`重连失败 (${this.reconnectAttempts}/${this.maxAttempts}):`, error.message); return this.reconnect(); } } async attemptConnection() { return new Promise((resolve, reject) => { try { const wsUrl = this.getWsUrl(); if (!wsUrl) { throw new ComfyUIError('WebSocket URL未配置', 'CONFIG'); } const testSocket = io(wsUrl, { timeout: 5000, reconnection: false }); const connectTimeout = setTimeout(() => { testSocket.disconnect(); reject(new ComfyUIError('连接超时', 'WEBSOCKET')); }, 5000); testSocket.on('connect', () => { clearTimeout(connectTimeout); testSocket.disconnect(); this.onConnect(); resolve(); }); testSocket.on('connect_error', (error) => { clearTimeout(connectTimeout); reject(new ComfyUIError('连接失败: ' + error.message, 'WEBSOCKET')); }); } catch (error) { reject(new ComfyUIError('连接尝试失败: ' + error.message, 'WEBSOCKET')); } }); } wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } showPermanentDisconnectionNotice() { if (typeof toastr !== 'undefined') { toastr.error('无法连接到服务器,请检查网络和服务器状态'); } // 在UI中显示离线提示 this.showOfflineNotice(); } showOfflineNotice() { // 移除现有的离线提示 const existingNotice = document.querySelector('.comfy-offline-notice'); if (existingNotice) { existingNotice.remove(); } const notice = document.createElement('div'); notice.className = 'comfy-offline-notice'; notice.innerHTML = ` <i class="fa fa-wifi-slash"></i> <span>连接断开,仅显示缓存内容</span> <button class="retry-connection">重试连接</button> `; notice.querySelector('.retry-connection').addEventListener('click', () => { notice.remove(); this.reconnectAttempts = 0; this.reconnect(); }); document.body.appendChild(notice); } reset() { this.reconnectAttempts = 0; } } // --- Performance Monitor --- class PerformanceMonitor { constructor() { this.metrics = {}; this.memoryCheckInterval = null; } startTimer(operation) { this.metrics[operation] = performance.now(); } endTimer(operation) { if (this.metrics[operation]) { const duration = performance.now() - this.metrics[operation]; console.log(`[Performance] ${operation}: ${duration.toFixed(2)}ms`); delete this.metrics[operation]; return duration; } return 0; } trackMemoryUsage() { if (performance.memory) { const usage = { used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024), total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024), limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) }; console.log(`[Memory] Used: ${usage.used}MB, Total: ${usage.total}MB, Limit: ${usage.limit}MB`); // 内存使用超过80%时发出警告 if (usage.used / usage.limit > 0.8) { console.warn('[Memory Warning] 内存使用过高,建议清理缓存'); if (typeof toastr !== 'undefined') { toastr.warning('内存使用过高,建议清理图片缓存'); } } return usage; } return null; } startMemoryMonitoring(interval = 30000) { this.stopMemoryMonitoring(); this.memoryCheckInterval = setInterval(() => { this.trackMemoryUsage(); }, interval); } stopMemoryMonitoring() { if (this.memoryCheckInterval) { clearInterval(this.memoryCheckInterval); this.memoryCheckInterval = null; } } } // --- Global State Variables --- let globalCooldownEndTime = 0; let socket = null; let activePrompts = {}; // 存储 prompt_id -> { button, generationId } 的映射 let reconnector = null; let performanceMonitor = new PerformanceMonitor(); let cachedDOMElements = {}; // DOM元素缓存 let debugMode = false; // 调试模式开关 // --- Cached User Settings --- let cachedSettings = { comfyuiUrl: '', startTag: 'image###', endTag: '###', promptPrefix: '', maxWidth: 600, cacheLimit: 20, apiKey: '', // 将存储加密后的密钥 defaultModel: '', notificationLevel: 'silent', // 'silent' | 'standard' | 'verbose' enableRetry: true, // 启用自动重试 retryCount: 3 // 重试次数 }; // --- Inject Custom CSS Styles --- GM_addStyle(` /* 新增:离线提示样式 */ .comfy-offline-notice { position: fixed; top: 20px; right: 20px; background: rgba(220, 53, 69, 0.9); color: white; padding: 10px 15px; border-radius: 5px; display: flex; align-items: center; gap: 10px; z-index: 10000; font-size: 14px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } .comfy-offline-notice .retry-connection { background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); color: white; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; } .comfy-offline-notice .retry-connection:hover { background: rgba(255,255,255,0.3); } /* 新增:缓存状态显示样式 */ #comfyui-cache-status { margin-top: 15px; margin-bottom: 10px; padding: 12px; /* 【优化】增加内边距 */ background: linear-gradient(90deg, rgba(30, 144, 255, 0.1) 0%, rgba(135, 206, 235, 0.1) 100%); /* 【优化】蓝色渐变背景 */ border: 1px solid rgba(135, 206, 235, 0.3); /* 【优化】蓝色边框 */ border-radius: 6px; text-align: center; font-size: 0.9em; color: #87CEEB; /* 【优化】蓝色文字 */ font-weight: 500; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* 【优化】添加阴影 */ } /* 新增:配置验证错误提示 */ .comfy-config-error { background-color: rgba(220, 53, 69, 0.1); border: 1px solid #dc3545; color: #dc3545; padding: 8px; border-radius: 4px; margin: 10px 0; font-size: 0.9em; } /* 控制面板主容器样式 */ #${PANEL_ID} { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90vw; max-width: 500px; z-index: 9999; color: #e0e0e0; /* 【优化】使用固定的浅色文字 */ background: linear-gradient(145deg, rgba(30, 30, 35, 0.98) 0%, rgba(20, 20, 25, 0.98) 100%); /* 【优化】渐变背景 */ border: 1px solid rgba(135, 206, 235, 0.3); /* 【优化】蓝色边框 */ border-radius: 12px; /* 【优化】更圆润 */ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(135, 206, 235, 0.1); /* 【优化】双层阴影 */ padding: 15px; box-sizing: border-box; backdrop-filter: blur(20px); /* 【优化】增强毛玻璃效果 */ flex-direction: column; } /* 面板标题栏 */ #${PANEL_ID} .panel-control-bar { padding: 14px 18px; /* 【优化】增加内边距 */ margin: -15px -15px 18px -15px; background: linear-gradient(135deg, rgba(135, 206, 235, 0.2) 0%, rgba(0, 191, 255, 0.25) 100%); /* 【优化】更明显的渐变 */ border-bottom: 2px solid rgba(135, 206, 235, 0.4); /* 【优化】更粗的分隔线 */ border-radius: 12px 12px 0 0; /* 【优化】匹配容器圆角 */ display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 【优化】添加阴影 */ } #${PANEL_ID} .panel-control-bar b { font-size: 1.15em; /* 【优化】稍大的字体 */ margin-left: 10px; color: #87CEEB; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); /* 【优化】文字阴影 */ font-weight: 600; /* 【优化】更粗的字体 */ } #${PANEL_ID} .panel-control-bar .drag-grabber { color: #87CEEB; cursor: move; transition: color 0.3s; /* 【优化】过渡动画 */ } #${PANEL_ID} .panel-control-bar .drag-grabber:hover { color: #00BFFF; /* 【优化】悬停变色 */ } #${PANEL_ID} .floating_panel_close { cursor: pointer; font-size: 1.8em; color: #87CEEB; transition: transform 0.2s, color 0.2s; padding: 5px; /* 【优化】增加点击区域 */ } #${PANEL_ID} .floating_panel_close:hover { transform: scale(1.15) rotate(90deg); /* 【优化】旋转动画 */ color: #ff6b6b; } #${PANEL_ID} .comfyui-panel-content { overflow-y: auto; flex-grow: 1; padding-right: 5px; max-height: 70vh; } /* 设置分组样式 */ .comfy-settings-group { margin-bottom: 20px; padding: 16px; /* 【优化】增加内边距 */ background: linear-gradient(135deg, rgba(40, 40, 50, 0.4) 0%, rgba(30, 30, 40, 0.4) 100%); /* 【优化】渐变背景 */ border: 1px solid rgba(135, 206, 235, 0.25); /* 【优化】更明显的边框 */ border-radius: 8px; /* 【优化】更圆润 */ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); /* 【优化】添加阴影 */ transition: all 0.3s ease; /* 【优化】过渡动画 */ } .comfy-settings-group:hover { border-color: rgba(135, 206, 235, 0.4); /* 【优化】悬停高亮 */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .comfy-settings-group-title { font-size: 1.05em; /* 【优化】稍大的字体 */ font-weight: 600; color: #87CEEB; /* 天蓝色 */ margin-bottom: 14px; padding-bottom: 10px; border-bottom: 2px solid rgba(135, 206, 235, 0.3); /* 【优化】更粗的分隔线 */ display: flex; align-items: center; gap: 10px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); /* 【优化】文字阴影 */ } .comfy-settings-group-title i { font-size: 1.1em; /* 【优化】图标稍大 */ color: #00BFFF; /* 深天蓝色 */ } .comfy-field { margin-bottom: 12px; } .comfy-field:last-child { margin-bottom: 0; } .comfy-field label { display: block; margin-bottom: 6px; font-size: 0.9em; color: #b0b0b0; /* 【优化】更浅的灰色 */ font-weight: 500; /* 【优化】稍粗的字体 */ } /* 输入框和文本域样式 */ #${PANEL_ID} input[type="text"], #${PANEL_ID} textarea, #${PANEL_ID} input[type="number"], #${PANEL_ID} select, #${PANEL_ID} input[type="password"] { width: 100%; box-sizing: border-box; padding: 10px 12px; /* 【优化】增加内边距 */ border-radius: 6px; /* 【优化】更圆润 */ border: 1px solid rgba(100, 100, 120, 0.4); /* 【优化】更明显的边框 */ background-color: rgba(15, 15, 20, 0.6); /* 【优化】更深的背景 */ color: #e0e0e0; /* 【优化】浅色文字 */ margin-bottom: 10px; font-size: 14px; transition: all 0.3s ease; /* 【优化】过渡动画 */ } /* 【优化】输入框聚焦效果 */ #${PANEL_ID} input[type="text"]:focus, #${PANEL_ID} textarea:focus, #${PANEL_ID} input[type="number"]:focus, #${PANEL_ID} select:focus, #${PANEL_ID} input[type="password"]:focus { outline: none; border-color: #87CEEB; background-color: rgba(20, 20, 30, 0.7); box-shadow: 0 0 0 3px rgba(135, 206, 235, 0.15); } /* 【优化】输入框占位符样式 */ #${PANEL_ID} input::placeholder, #${PANEL_ID} textarea::placeholder { color: rgba(176, 176, 176, 0.5); font-style: italic; } #${PANEL_ID} textarea { min-height: 150px; resize: vertical; } #${PANEL_ID} .workflow-info { font-size: 0.9em; color: #aaa; margin-top: -5px; margin-bottom: 10px; } /* 通用按钮样式 */ .comfy-button { padding: 11px 16px; /* 【优化】增加内边距 */ border: none; /* 【优化】移除边框 */ border-radius: 8px; /* 【优化】更圆润 */ cursor: pointer; background: linear-gradient(135deg, #4A9EFF 0%, #1E90FF 100%); /* 【优化】更鲜艳的蓝色渐变 */ color: white; font-weight: 600; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 【优化】缓动函数 */ flex-shrink: 0; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 2px 8px rgba(30, 144, 255, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2); /* 【优化】双层阴影 */ position: relative; overflow: hidden; } /* 【优化】按钮闪光效果 */ .comfy-button::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); transition: left 0.5s; } .comfy-button:hover::before { left: 100%; } .comfy-button i { font-size: 1em; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2)); /* 【优化】图标阴影 */ } .comfy-button:disabled { opacity: 0.5; cursor: not-allowed; background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%); /* 【优化】禁用状态灰色 */ } .comfy-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(30, 144, 255, 0.4), 0 2px 4px rgba(0, 0, 0, 0.2); /* 【优化】悬停阴影更明显 */ background: linear-gradient(135deg, #5AADFF 0%, #2E9FFF 100%); /* 【优化】悬停颜色变化 */ } .comfy-button:active:not(:disabled) { transform: translateY(0); box-shadow: 0 2px 4px rgba(30, 144, 255, 0.2); } /* 按钮状态样式 */ .comfy-button.testing { background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%); /* 【优化】灰色渐变 */ box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3); } .comfy-button.success { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); /* 【优化】更鲜艳的绿色 */ box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4); } .comfy-button.error { background: linear-gradient(135deg, #dc3545 0%, #e74c3c 100%); /* 【优化】更鲜艳的红色 */ box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4); } .comfy-button.success:hover:not(:disabled) { background: linear-gradient(135deg, #34ce57 0%, #2dd4a7 100%); box-shadow: 0 6px 16px rgba(40, 167, 69, 0.5); } .comfy-button.error:hover:not(:disabled) { background: linear-gradient(135deg, #e74c3c 0%, #f85149 100%); box-shadow: 0 6px 16px rgba(220, 53, 69, 0.5); } /* 特殊布局样式 */ #comfyui-test-conn { position: relative; top: -5px; } .comfy-url-container { display: flex; gap: 10px; align-items: center; } .comfy-url-container input { flex-grow: 1; margin-bottom: 0; } #${PANEL_ID} label { display: block; margin-bottom: 5px; font-weight: bold; } #options > .options-content > a#${BUTTON_ID} { display: flex; align-items: center; gap: 10px; } /* 标记输入框容器样式 */ #${PANEL_ID} .comfy-tags-container { display: flex; gap: 10px; align-items: flex-end; margin-top: 10px; margin-bottom: 10px; } #${PANEL_ID} .comfy-tags-container div { flex-grow: 1; } /* 聊天内按钮组容器 */ .comfy-button-group { display: inline-flex; align-items: center; gap: 5px; margin: 5px 4px; } /* 生成的图片容器样式 */ .comfy-image-container { margin-top: 10px; max-width: 100%; } .comfy-image-container img { max-width: var(--comfy-image-max-width, 100%); height: auto; border-radius: 8px; border: 1px solid var(--SmartThemeBorderColor, #555); } /* 图片加载动画 */ .comfy-loading-indicator { text-align: center; color: #888; padding: 10px; font-style: italic; } .comfy-loading-indicator::after { content: '...'; animation: comfy-dots 1.5s steps(4, end) infinite; } @keyframes comfy-dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60%, 100% { content: '...'; } } /* 图片错误占位符 */ .comfy-image-error { padding: 20px; text-align: center; color: #dc3545; border: 2px dashed #dc3545; border-radius: 8px; background: rgba(220, 53, 69, 0.1); } .comfy-image-error i { font-size: 2em; margin-bottom: 10px; display: block; } /* 图片容器悬停效果 */ .comfy-image-container { position: relative; display: inline-block; contain: layout; /* 【优化】CSS Containment,减少重排 */ } .comfy-image-container img { cursor: pointer; transition: opacity 0.3s ease, transform 0.3s ease; will-change: transform; /* 【优化】GPU加速 */ transform: translateZ(0); /* 【优化】启用硬件加速 */ } .comfy-image-container img:hover { opacity: 0.9; transform: scale(1.02) translateZ(0); } /* 图片工具栏 */ .comfy-image-toolbar { position: absolute; top: 5px; right: 5px; display: none; gap: 5px; z-index: 10; } .comfy-image-container:hover .comfy-image-toolbar { display: flex; } .comfy-image-tool-btn { background: rgba(0, 0, 0, 0.7); border: none; border-radius: 4px; color: white; padding: 6px 10px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .comfy-image-tool-btn:hover { background: rgba(0, 0, 0, 0.9); } /* Lightbox 图片放大查看器 */ .comfy-lightbox { display: none; position: fixed; top: 0; left: 0; width: 100vw; /* 【修复】使用视口单位确保全屏 */ height: 100vh; background: rgba(0, 0, 0, 0.95); z-index: 100000; justify-content: center; align-items: center; animation: comfy-fadeIn 0.3s ease; overflow: hidden; /* 【修复】防止滚动条 */ } .comfy-lightbox.active { display: flex !important; /* 【修复】使用!important确保显示 */ } .comfy-lightbox img { max-width: 90vw; /* 【修复】使用视口单位 */ max-height: 90vh; width: auto; height: auto; object-fit: contain; border-radius: 8px; box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); animation: comfy-zoomIn 0.3s ease; display: block; /* 【修复】确保图片是块级元素 */ margin: auto; /* 【修复】额外的居中保障 */ } .comfy-lightbox-close { position: absolute; top: 20px; right: 30px; font-size: 40px; color: white; cursor: pointer; z-index: 100001; /* 【修复】确保关闭按钮在Lightbox之上 */ transition: transform 0.2s; } .comfy-lightbox-close:hover { transform: scale(1.2); } .comfy-lightbox-download { position: absolute; bottom: 30px; right: 30px; background: linear-gradient(135deg, #87CEEB 0%, #00BFFF 100%); border: none; border-radius: 8px; color: white; padding: 12px 24px; cursor: pointer; font-size: 16px; font-weight: 600; transition: opacity 0.3s; } .comfy-lightbox-download:hover { opacity: 0.85; } @keyframes comfy-fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes comfy-zoomIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } /* 图片淡入动画 */ .comfy-image-container img.fade-in { animation: comfy-imageFadeIn 0.5s ease; } @keyframes comfy-imageFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 骨架屏加载效果 */ .comfy-skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: comfy-skeleton-loading 1.5s infinite; border-radius: 8px; } @keyframes comfy-skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* 移动端适配 */ @media (max-width: 1000px) { #${PANEL_ID} { top: 20px; left: 50%; transform: translateX(-50%); max-height: calc(100vh - 40px); width: 95vw; } /* 【修复】移动端Lightbox优化 */ .comfy-lightbox img { max-width: 95vw !important; max-height: 95vh !important; } .comfy-lightbox-close { top: 10px !important; right: 10px !important; font-size: 36px !important; background: rgba(0, 0, 0, 0.5); border-radius: 50%; width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; } .comfy-lightbox-download { bottom: 10px !important; right: 10px !important; font-size: 14px !important; padding: 10px 20px !important; } } /* 历史记录面板 */ #comfy-history-panel { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.5); padding: 20px; box-sizing: border-box; } #comfy-history-panel.active { display: flex; } .comfy-history-container { width: 90vw; max-width: 800px; max-height: 80vh; color: #e0e0e0; /* 【优化】浅色文字 */ background: linear-gradient(145deg, rgba(30, 30, 35, 0.98) 0%, rgba(20, 20, 25, 0.98) 100%); /* 【优化】渐变背景 */ border: 1px solid rgba(135, 206, 235, 0.3); /* 【优化】蓝色边框 */ border-radius: 12px; /* 【优化】更圆润 */ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(135, 206, 235, 0.1); /* 【优化】双层阴影 */ backdrop-filter: blur(20px); /* 【优化】增强毛玻璃 */ display: flex; flex-direction: column; overflow: hidden; } .comfy-history-header { padding: 18px 20px; /* 【优化】增加内边距 */ background: linear-gradient(135deg, rgba(135, 206, 235, 0.15) 0%, rgba(0, 191, 255, 0.2) 100%); /* 【优化】蓝色渐变背景 */ border-bottom: 2px solid rgba(135, 206, 235, 0.3); /* 【优化】更粗的分隔线 */ display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 【优化】添加阴影 */ } .comfy-history-header h3 { margin: 0; font-size: 1.25em; /* 【优化】稍大的字体 */ color: #87CEEB; /* 【优化】蓝色标题 */ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); /* 【优化】文字阴影 */ font-weight: 600; } .comfy-history-header h3 i { color: #00BFFF; /* 【优化】图标颜色 */ margin-right: 8px; } .comfy-history-stats { font-size: 0.9em; color: #a0a0a0; /* 【优化】更浅的灰色 */ margin-top: 6px; } /* 【优化】历史面板关闭按钮样式 */ #comfy-history-close { color: #87CEEB !important; transition: all 0.3s ease; padding: 5px; cursor: pointer; } #comfy-history-close:hover { color: #ff6b6b !important; transform: scale(1.15) rotate(90deg); } .comfy-history-content { flex: 1; overflow-y: auto; padding: 15px; -webkit-overflow-scrolling: touch; /* 【优化】iOS平滑滚动 */ will-change: scroll-position; /* 【优化】提示浏览器优化滚动 */ } .comfy-history-item { display: flex; gap: 15px; padding: 12px; /* 【优化】增加内边距 */ border: 1px solid rgba(100, 100, 120, 0.3); /* 【优化】更明显的边框 */ border-radius: 10px; /* 【优化】更圆润 */ margin-bottom: 12px; background: linear-gradient(135deg, rgba(35, 35, 45, 0.4) 0%, rgba(25, 25, 35, 0.4) 100%); /* 【优化】渐变背景 */ transition: all 0.3s ease; /* 【优化】平滑过渡 */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .comfy-history-item:hover { background: linear-gradient(135deg, rgba(45, 45, 60, 0.5) 0%, rgba(35, 35, 50, 0.5) 100%); /* 【优化】悬停渐变 */ border-color: rgba(135, 206, 235, 0.4); /* 【优化】悬停边框变色 */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* 【优化】悬停阴影 */ transform: translateY(-2px); /* 【优化】轻微上浮 */ } .comfy-history-thumbnail { width: 120px; height: 120px; flex-shrink: 0; border-radius: 4px; overflow: hidden; cursor: pointer; position: relative; } .comfy-history-thumbnail img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; will-change: transform; /* 【优化】GPU加速 */ transform: translateZ(0); /* 【优化】启用硬件加速 */ } .comfy-history-thumbnail:hover img { transform: scale(1.1); } .comfy-history-thumbnail::after { content: '🔍'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; opacity: 0; transition: opacity 0.3s; } .comfy-history-thumbnail:hover::after { opacity: 0.9; } .comfy-history-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; } .comfy-history-meta { font-size: 0.85em; color: #888; } .comfy-history-meta span { margin-right: 15px; } .comfy-history-actions { display: flex; gap: 8px; margin-top: 8px; } .comfy-history-btn { padding: 8px 14px; /* 【优化】增加内边距 */ border: 1px solid rgba(135, 206, 235, 0.3); /* 【优化】蓝色边框 */ border-radius: 6px; /* 【优化】更圆润 */ background: linear-gradient(135deg, rgba(30, 30, 40, 0.6) 0%, rgba(20, 20, 30, 0.6) 100%); /* 【优化】渐变背景 */ color: #e0e0e0; /* 【优化】浅色文字 */ cursor: pointer; font-size: 0.9em; transition: all 0.3s ease; /* 【优化】过渡动画 */ font-weight: 500; display: inline-flex; align-items: center; gap: 6px; } .comfy-history-btn i { font-size: 0.95em; } .comfy-history-btn:hover { background: linear-gradient(135deg, rgba(135, 206, 235, 0.2) 0%, rgba(0, 191, 255, 0.25) 100%); /* 【优化】悬停渐变 */ border-color: rgba(135, 206, 235, 0.5); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(135, 206, 235, 0.3); } .comfy-history-btn.danger { border-color: rgba(220, 53, 69, 0.4); /* 【优化】红色边框 */ } .comfy-history-btn.danger:hover { background: linear-gradient(135deg, rgba(220, 53, 69, 0.3) 0%, rgba(231, 76, 60, 0.35) 100%); /* 【优化】红色悬停 */ border-color: #dc3545; box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4); } .comfy-history-empty { text-align: center; padding: 40px; color: #888; } .comfy-history-empty i { font-size: 48px; display: block; margin-bottom: 15px; opacity: 0.5; } /* 移动端适配 - 历史面板 */ @media (max-width: 768px) { #comfy-history-panel { padding: 5px; } #comfy-history-panel.active { display: flex !important; justify-content: center !important; align-items: center !important; } .comfy-history-container { width: 95vw; max-width: 95vw; max-height: 90vh; border-radius: 12px; margin: auto; } .comfy-history-header { padding: 12px; flex-shrink: 0; } .comfy-history-header h3 { font-size: 1em; } .comfy-history-header #comfy-history-close { font-size: 28px !important; } .comfy-history-stats { font-size: 0.8em; } .comfy-history-content { overflow-y: auto; -webkit-overflow-scrolling: touch; } .comfy-history-item { flex-direction: column; padding: 8px; } .comfy-history-thumbnail { width: 100%; height: auto; max-height: 300px; object-fit: contain; } .comfy-history-actions { flex-wrap: wrap; gap: 5px; } .comfy-history-btn { flex: 1; min-width: 80px; font-size: 0.85em; padding: 8px; } /* 设置面板移动端适配 */ #${PANEL_ID} { width: 95vw; max-height: 85vh; padding: 10px; } #${PANEL_ID} .panel-control-bar { padding: 10px; margin: -10px -10px 12px -10px; } #${PANEL_ID} .panel-control-bar b { font-size: 1em; } .comfy-settings-group { padding: 10px; margin-bottom: 15px; } .comfy-settings-group-title { font-size: 0.95em; } .comfy-url-container { flex-direction: column; } .comfy-url-container button { margin-top: 8px; margin-left: 0 !important; } } /* CSS变量,用于动态控制图片最大宽度 */ :root { --comfy-image-max-width: 600px; } /* 【优化】暗黑模式支持 - 自动适配系统主题 */ @media (prefers-color-scheme: dark) { .comfy-lightbox { background: rgba(0, 0, 0, 0.98); } .comfy-history-container { background-color: rgba(18, 18, 18, 0.98); } #${PANEL_ID} { background-color: rgba(18, 18, 18, 0.95); } } @media (prefers-color-scheme: light) { .comfy-lightbox { background: rgba(255, 255, 255, 0.98); } .comfy-lightbox-close { color: #333; } .comfy-history-container { background-color: rgba(255, 255, 255, 0.98); color: #333; } #${PANEL_ID} { background-color: rgba(255, 255, 255, 0.95); color: #333; } } /* 【优化】移动端触摸优化 - 增大可点击区域 */ @media (max-width: 768px) and (hover: none) { .comfy-button { min-height: 44px; /* Apple人机界面指南推荐的最小触摸目标 */ padding: 12px 16px; font-size: 16px; /* 防止iOS自动缩放 */ } .comfy-history-btn { min-height: 44px; padding: 10px 16px; } .comfy-image-tool-btn { min-width: 44px; min-height: 44px; padding: 10px; } /* 优化长按效果 */ .comfy-button:active { transform: scale(0.95); opacity: 0.8; } } `); // --- Configuration Migration --- async function migrateConfig() { try { const currentVersion = await GM_getValue('config_version', '1.0'); if (currentVersion !== CONFIG_VERSION) { console.log(`配置迁移: ${currentVersion} -> ${CONFIG_VERSION}`); // 迁移旧版本的API密钥 const oldApiKey = await GM_getValue('comfyui_api_key', ''); if (oldApiKey && !oldApiKey.includes('=')) { // 检查是否已加密 const encryptedKey = encryptApiKey(oldApiKey); await GM_setValue('comfyui_api_key', encryptedKey); console.log('API密钥已加密存储'); } await GM_setValue('config_version', CONFIG_VERSION); console.log('配置迁移完成'); } } catch (error) { console.error('配置迁移失败:', error); } } // --- Cache Integrity and Management --- async function validateCacheIntegrity() { try { performanceMonitor.startTimer('validateCache'); const records = await GM_getValue(STORAGE_KEY_IMAGES, {}); const validRecords = {}; let removedCount = 0; for (const [id, data] of Object.entries(records)) { try { // 验证Base64数据完整性 if (typeof data === 'string' && data.startsWith('data:image/') && data.includes('base64,') && data.length > 100) { // 基本长度检查 validRecords[id] = data; } else { console.warn(`缓存记录 ${id} 数据格式无效,已清理`); removedCount++; } } catch (error) { console.error(`缓存记录 ${id} 验证失败:`, error); removedCount++; } } if (removedCount > 0) { await GM_setValue(STORAGE_KEY_IMAGES, validRecords); if (typeof toastr !== 'undefined') { toastr.info(`已清理 ${removedCount} 条无效缓存记录`); } } performanceMonitor.endTimer('validateCache'); return Object.keys(validRecords).length; } catch (error) { ErrorHandler.handle(new ComfyUIError('缓存验证失败: ' + error.message, 'CACHE'), 'validateCacheIntegrity'); return 0; } } // --- Image Compression and Optimization --- // 【优化】创建内联WebWorker用于图片压缩(避免阻塞主线程) let compressionWorker = null; function createCompressionWorker() { // 由于油猴环境限制,使用OffscreenCanvas在主线程压缩 // 但使用requestIdleCallback优化,避免阻塞UI return { compress: async (canvas, quality, maxWidth) => { return new Promise((resolve) => { // 使用requestIdleCallback在浏览器空闲时压缩 const idleCallback = window.requestIdleCallback || ((cb) => setTimeout(cb, 1)); idleCallback(async () => { try { // 如果图片太大,先缩放 if (canvas.width > maxWidth) { const scale = maxWidth / canvas.width; const scaledCanvas = document.createElement('canvas'); const ctx = scaledCanvas.getContext('2d'); scaledCanvas.width = maxWidth; scaledCanvas.height = canvas.height * scale; ctx.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height); canvas = scaledCanvas; } // 压缩为JPEG格式 canvas.toBlob((blob) => { if (blob) { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.readAsDataURL(blob); } else { resolve(null); } }, 'image/jpeg', quality); } catch (error) { console.error('图片压缩失败:', error); resolve(null); } }, { timeout: 2000 }); }); } }; } function compressImage(canvas, quality = 0.8, maxWidth = 1024) { // 【优化】使用compressionWorker处理(如果可用) if (!compressionWorker) { compressionWorker = createCompressionWorker(); } return compressionWorker.compress(canvas, quality, maxWidth); } async function fetchImageAsBase64(imageUrl) { return new Promise((resolve, reject) => { if (debugMode) { console.log(`[ComfyUI Debug] 开始获取图片: ${imageUrl}`); } performanceMonitor.startTimer('fetchImage'); GM_xmlhttpRequest({ method: 'GET', url: imageUrl, responseType: 'blob', timeout: 30000, onload: async (response) => { try { if (debugMode) { console.log(`[ComfyUI Debug] 图片请求响应: 状态=${response.status}, 大小=${response.response?.size || 0}字节`); } if (response.status === 200) { const blob = response.response; // 检查文件大小 if (blob.size > 10 * 1024 * 1024) { // 10MB限制 throw new ComfyUIError('图片文件过大', 'VALIDATION'); } if (blob.size === 0) { throw new ComfyUIError('图片文件为空', 'VALIDATION'); } // 【优化】降低压缩阈值,移动端更激进 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const compressionThreshold = isMobile ? 1 * 1024 * 1024 : 2 * 1024 * 1024; // 移动端1MB,PC端2MB // 尝试压缩大图片 if (blob.size > compressionThreshold) { if (debugMode) { console.log(`[ComfyUI Debug] 图片较大(${(blob.size / 1024).toFixed(1)}KB),开始压缩...`); console.log(`[ComfyUI Debug] 设备: ${isMobile ? '移动端' : 'PC端'}, 压缩阈值: ${(compressionThreshold / 1024).toFixed(0)}KB`); } const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); img.onload = async () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); // 【优化】移动端使用更激进的压缩 const quality = isMobile ? 0.6 : 0.75; const maxWidth = isMobile ? 800 : 1024; const compressedData = await compressImage(canvas, quality, maxWidth); if (compressedData) { const compressedSize = (compressedData.length * 0.75 / 1024).toFixed(1); console.log(`图片已压缩: ${(blob.size / 1024).toFixed(1)}KB -> ${compressedSize}KB (质量:${quality}, 最大宽度:${maxWidth}px)`); if (debugMode) { console.log(`[ComfyUI Debug] 压缩比: ${(100 - (compressedSize / (blob.size / 1024) * 100)).toFixed(1)}%`); } performanceMonitor.endTimer('fetchImage'); resolve(compressedData); } else { // 压缩失败,使用原图 console.warn('图片压缩失败,使用原始图片'); const reader = new FileReader(); reader.onloadend = () => { performanceMonitor.endTimer('fetchImage'); resolve(reader.result); }; reader.readAsDataURL(blob); } }; img.onerror = () => { // 图片解析失败,使用原始数据 console.warn('图片解析失败,使用原始数据'); const reader = new FileReader(); reader.onloadend = () => { performanceMonitor.endTimer('fetchImage'); resolve(reader.result); }; reader.readAsDataURL(blob); }; const reader = new FileReader(); reader.onload = () => img.src = reader.result; reader.readAsDataURL(blob); } else { // 小文件直接转换 if (debugMode) { console.log(`[ComfyUI Debug] 图片较小(${(blob.size / 1024).toFixed(1)}KB),直接转换base64`); } const reader = new FileReader(); reader.onloadend = () => { performanceMonitor.endTimer('fetchImage'); if (debugMode) { console.log(`[ComfyUI Debug] Base64转换完成`); } resolve(reader.result); }; reader.onerror = (err) => { performanceMonitor.endTimer('fetchImage'); reject(new ComfyUIError('FileReader error: ' + err, 'CACHE')); }; reader.readAsDataURL(blob); } } else { reject(new ComfyUIError(`获取图片失败,状态: ${response.status}`, 'NETWORK')); } } catch (error) { reject(error); } finally { performanceMonitor.endTimer('fetchImage'); } }, onerror: (err) => { performanceMonitor.endTimer('fetchImage'); reject(new ComfyUIError('网络错误: ' + err, 'NETWORK')); }, ontimeout: () => { performanceMonitor.endTimer('fetchImage'); reject(new ComfyUIError('下载图片超时', 'NETWORK')); } }); }); } // --- WebSocket & State Management --- function connectWebSocket() { try { if (socket && socket.connected) return; const schedulerUrl = new URL(cachedSettings.comfyuiUrl); const wsUrl = `${schedulerUrl.protocol}//${schedulerUrl.host}`; if (typeof io === 'undefined') { throw new ComfyUIError('Socket.IO 客户端库未加载!', 'CONFIG'); } socket = io(wsUrl, { reconnectionAttempts: 5, timeout: 20000, transports: ['websocket', 'polling'] // 添加备用传输方式 }); socket.on('connect', () => { console.log('成功连接到调度器 WebSocket!'); if (typeof toastr !== 'undefined') toastr.success('已建立实时连接!'); // 重置重连器状态 if (reconnector) { reconnector.reset(); } // 移除离线提示 const offlineNotice = document.querySelector('.comfy-offline-notice'); if (offlineNotice) { offlineNotice.remove(); } }); socket.on('disconnect', (reason) => { console.log('与调度器 WebSocket 断开连接:', reason); // 只在非主动断开时尝试重连 if (reason !== 'io client disconnect' && reason !== 'io server disconnect') { if (reconnector) { reconnector.reconnect(); } } }); socket.on('connect_error', (error) => { console.error('WebSocket连接错误:', error); ErrorHandler.handle(new ComfyUIError('WebSocket连接失败: ' + error.message, 'WEBSOCKET'), 'connectWebSocket'); }); socket.on('generation_complete', async (data) => { try { const { prompt_id, status, imageUrl, error } = data; const promptInfo = activePrompts[prompt_id]; if (!promptInfo) return; const { button, generationId } = promptInfo; const group = button.closest('.comfy-button-group'); if (status === 'success' && imageUrl) { showNotification('图片已生成,正在下载...', 'info', 'verbose'); try { // 通过GM_xmlhttpRequest获取图片并转换为base64(绕过CORS限制) if (debugMode) { console.log(`[ComfyUI Debug] 开始获取图片: ${imageUrl}`); } // 使用重试机制获取图片 let imageBase64Data; if (cachedSettings.enableRetry) { imageBase64Data = await retryWithBackoff( () => fetchImageAsBase64(imageUrl), cachedSettings.retryCount, 1000 ); } else { imageBase64Data = await fetchImageAsBase64(imageUrl); } if (!imageBase64Data) { throw new ComfyUIError('获取图片数据失败', 'NETWORK'); } if (debugMode) { console.log(`[ComfyUI Debug] 图片下载成功,大小: ${(imageBase64Data.length / 1024).toFixed(2)}KB`); } // 使用base64数据显示图片 await displayImageWithValidation(group, imageBase64Data, generationId); // 保存到缓存 await saveImageRecord(generationId, imageBase64Data); showNotification('图片加载完成!', 'success', 'standard'); button.textContent = '生成成功'; button.classList.remove('testing'); button.classList.add('success'); setTimeout(() => { setupGeneratedState(button, generationId); }, 2000); } catch (imgError) { console.error('图片下载或加载失败:', imgError); showNotification(`图片加载失败: ${imgError.message}`, 'error'); button.textContent = '图片加载失败'; button.classList.remove('testing'); button.classList.add('error'); setTimeout(() => { button.disabled = false; button.classList.remove('error'); button.textContent = group.querySelector('.comfy-delete-button') ? '重新生成' : '开始生成'; }, 3000); } } else { if (typeof toastr !== 'undefined') toastr.error(`生成失败: ${error || '未知错误'}`); button.textContent = '生成失败'; button.classList.remove('testing'); button.classList.add('error'); setTimeout(() => { button.disabled = false; button.classList.remove('error'); button.textContent = group.querySelector('.comfy-delete-button') ? '重新生成' : '开始生成'; }, 3000); } delete activePrompts[prompt_id]; } catch (error) { ErrorHandler.handle(new ComfyUIError('处理生成完成事件失败: ' + error.message, 'WEBSOCKET'), 'generation_complete'); } }); } catch (error) { ErrorHandler.handle(error, 'connectWebSocket'); } } // --- History Panel --- function createHistoryPanel() { if (document.getElementById('comfy-history-panel')) return; const panel = document.createElement('div'); panel.id = 'comfy-history-panel'; panel.innerHTML = ` <div class="comfy-history-container"> <div class="comfy-history-header"> <div> <h3><i class="fa fa-history"></i> 生成历史记录</h3> <div class="comfy-history-stats" id="comfy-history-stats"></div> </div> <i class="fa fa-times" style="cursor: pointer; font-size: 24px;" id="comfy-history-close"></i> </div> <div class="comfy-history-content" id="comfy-history-content"> <!-- 动态内容 --> </div> </div> `; document.body.appendChild(panel); // 关闭按钮 const closeBtn = document.getElementById('comfy-history-close'); closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); panel.classList.remove('active'); // 【修复】清除内联样式,避免display属性冲突 panel.style.display = ''; console.log('[ComfyUI] 历史面板已关闭'); }); // 点击背景遮罩关闭(点击container内部不关闭) panel.addEventListener('click', (e) => { if (e.target === panel) { panel.classList.remove('active'); // 【修复】清除内联样式 panel.style.display = ''; console.log('[ComfyUI] 点击背景关闭历史面板'); } }); // 阻止点击container内部时关闭 const container = panel.querySelector('.comfy-history-container'); container.addEventListener('click', (e) => { e.stopPropagation(); }); // ESC键关闭 const handleEsc = (e) => { if (e.key === 'Escape' && panel.classList.contains('active')) { panel.classList.remove('active'); // 【修复】清除内联样式 panel.style.display = ''; console.log('[ComfyUI] ESC键关闭历史面板'); } }; document.addEventListener('keydown', handleEsc); } async function openHistoryPanel() { const panel = document.getElementById('comfy-history-panel'); if (!panel) { createHistoryPanel(); setTimeout(() => openHistoryPanel(), 100); return; } // 显示面板 panel.classList.add('active'); // 加载历史记录 await refreshHistoryPanel(); } async function refreshHistoryPanel() { const content = document.getElementById('comfy-history-content'); const statsDiv = document.getElementById('comfy-history-stats'); if (!content || !lruCache) return; const items = lruCache.getAll(); const stats = lruCache.getStats(); // 更新统计信息 statsDiv.innerHTML = ` 总计: ${stats.count} 张 | 大小: ${stats.totalSizeMB}MB | 平均访问: ${stats.avgAccessCount} 次 `; // 按时间倒序排列 items.sort((a, b) => b.timestamp - a.timestamp); if (items.length === 0) { content.innerHTML = ` <div class="comfy-history-empty"> <i class="fa fa-image"></i> <p>还没有生成任何图片</p> </div> `; return; } content.innerHTML = items.map(item => ` <div class="comfy-history-item" data-id="${item.id}"> <div class="comfy-history-thumbnail" data-src="${item.data}"> <img src="${item.data}" alt="生成的图片"> </div> <div class="comfy-history-info"> <div> <strong>ID:</strong> ${item.id} </div> <div class="comfy-history-meta"> <span><i class="fa fa-clock"></i> ${item.ageInHours}小时前</span> <span><i class="fa fa-eye"></i> 访问 ${item.accessCount} 次</span> <span><i class="fa fa-file"></i> ${item.sizeMB}MB</span> </div> <div class="comfy-history-actions"> <button class="comfy-history-btn" data-action="view" data-id="${item.id}"> <i class="fa fa-search-plus"></i> 查看 </button> <button class="comfy-history-btn" data-action="download" data-id="${item.id}"> <i class="fa fa-download"></i> 下载 </button> <button class="comfy-history-btn danger" data-action="delete" data-id="${item.id}"> <i class="fa fa-trash"></i> 删除 </button> </div> </div> </div> `).join(''); // 绑定事件 content.querySelectorAll('.comfy-history-thumbnail').forEach(thumb => { thumb.addEventListener('click', () => { openLightbox(thumb.dataset.src); }); }); content.querySelectorAll('.comfy-history-btn').forEach(btn => { btn.addEventListener('click', async (e) => { const action = btn.dataset.action; const id = btn.dataset.id; const item = items.find(i => i.id === id); if (!item) return; if (action === 'view') { openLightbox(item.data); } else if (action === 'download') { const filename = `comfyui-${id}-${Date.now()}.png`; downloadImage(item.data, filename); } else if (action === 'delete') { if (confirm('确定要删除这张图片吗?')) { await deleteImageRecord(id); await refreshHistoryPanel(); showNotification('图片已删除', 'success', 'standard'); } } }); }); } // --- Lightbox (Image Viewer) --- function createLightbox() { if (document.getElementById('comfy-lightbox')) return; const lightbox = document.createElement('div'); lightbox.id = 'comfy-lightbox'; lightbox.className = 'comfy-lightbox'; lightbox.innerHTML = ` <span class="comfy-lightbox-close">×</span> <img src="" alt="查看大图" data-current-index="-1"> <button class="comfy-lightbox-download"> <i class="fa fa-download"></i> 下载图片 </button> `; document.body.appendChild(lightbox); const closeBtn = lightbox.querySelector('.comfy-lightbox-close'); const downloadBtn = lightbox.querySelector('.comfy-lightbox-download'); const img = lightbox.querySelector('img'); // 【优化】添加移动端触摸手势支持(双指缩放、左右滑动) let touchStartX = 0; let touchStartY = 0; let touchStartDistance = 0; let currentScale = 1; lightbox.addEventListener('touchstart', (e) => { if (e.touches.length === 1) { // 单指触摸 - 准备滑动 touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } else if (e.touches.length === 2) { // 双指触摸 - 准备缩放 const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; touchStartDistance = Math.sqrt(dx * dx + dy * dy); } }, { passive: true }); lightbox.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { // 双指缩放 e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const distance = Math.sqrt(dx * dx + dy * dy); if (touchStartDistance > 0) { const scale = (distance / touchStartDistance) * currentScale; const clampedScale = Math.min(Math.max(scale, 0.5), 3); // 限制缩放范围 img.style.transform = `scale(${clampedScale})`; } } }, { passive: false }); lightbox.addEventListener('touchend', (e) => { if (e.changedTouches.length === 1 && e.touches.length === 0) { // 单指离开 - 检查是否滑动 const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const deltaX = touchEndX - touchStartX; const deltaY = touchEndY - touchStartY; // 水平滑动超过50px且垂直滑动小于30px if (Math.abs(deltaX) > 50 && Math.abs(deltaY) < 30) { if (debugMode) { console.log('[ComfyUI] 检测到滑动手势:', deltaX > 0 ? '右滑' : '左滑'); } // 这里可以添加切换上一张/下一张图片的逻辑 // 暂时只做提示 } } else if (e.touches.length === 0) { // 双指离开 - 保存当前缩放比例 const transform = window.getComputedStyle(img).transform; if (transform !== 'none') { const matrix = new DOMMatrix(transform); currentScale = matrix.a; // a 是 scaleX } } }, { passive: true }); // 【修复】关闭Lightbox的统一处理函数 const closeLightbox = () => { lightbox.classList.remove('active'); // 【优化】重置图片缩放和变换 img.style.transform = ''; currentScale = 1; // 【修复】恢复历史面板显示 - 清除内联样式即可,让CSS类控制 const historyPanel = document.getElementById('comfy-history-panel'); if (historyPanel && historyPanel.classList.contains('active')) { // 不再直接设置display:flex,而是清除内联样式,让.active类的CSS生效 historyPanel.style.display = ''; } console.log('[ComfyUI] Lightbox已关闭,历史面板已恢复'); }; // 关闭按钮 closeBtn.addEventListener('click', closeLightbox); // 点击背景关闭 lightbox.addEventListener('click', (e) => { if (e.target === lightbox) { closeLightbox(); } }); // ESC键关闭 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && lightbox.classList.contains('active')) { closeLightbox(); } }); // 下载按钮 downloadBtn.addEventListener('click', () => { const imageSrc = img.src; if (imageSrc) { const filename = `comfyui-${Date.now()}.png`; downloadImage(imageSrc, filename); } }); } function openLightbox(imageSrc) { console.log('[ComfyUI] 尝试打开Lightbox'); const lightbox = document.getElementById('comfy-lightbox'); if (!lightbox) { console.log('[ComfyUI] Lightbox不存在,创建中...'); createLightbox(); // 递归调用以打开lightbox setTimeout(() => openLightbox(imageSrc), 100); return; } console.log('[ComfyUI] Lightbox元素已找到'); // 【修复】在打开Lightbox时,暂时隐藏历史面板(避免层级冲突) const historyPanel = document.getElementById('comfy-history-panel'); if (historyPanel && historyPanel.classList.contains('active')) { historyPanel.style.display = 'none'; console.log('[ComfyUI] 历史面板已暂时隐藏'); } const img = lightbox.querySelector('img'); if (!img) { console.error('[ComfyUI Error] Lightbox中未找到img元素!'); return; } console.log('[ComfyUI] 设置图片src,数据大小:', (imageSrc.length / 1024).toFixed(2), 'KB'); console.log('[ComfyUI] 图片src前缀:', imageSrc.substring(0, 50)); img.src = imageSrc; lightbox.classList.add('active'); // 【修复】强制重绘,确保样式生效 void lightbox.offsetHeight; console.log('[ComfyUI] Lightbox已添加active类'); console.log('[ComfyUI] Lightbox display样式:', window.getComputedStyle(lightbox).display); console.log('[ComfyUI] 图片naturalWidth:', img.naturalWidth, 'naturalHeight:', img.naturalHeight); // 【修复】等待图片加载完成后再检查 img.onload = () => { console.log('[ComfyUI] Lightbox图片加载成功!尺寸:', img.naturalWidth, 'x', img.naturalHeight); }; img.onerror = () => { console.error('[ComfyUI Error] Lightbox图片加载失败!'); }; } // --- Core Application Logic (UI, Settings, Image Handling) --- // A flag to prevent duplicate execution let lastTapTimestamp = 0; const TAP_THRESHOLD = 300; function createComfyUIPanel() { if (document.getElementById(PANEL_ID)) return; const panelHTML = ` <div id="${PANEL_ID}"> <div class="panel-control-bar"> <div style="display: flex; align-items: center;"> <i class="fa-fw fa-solid fa-grip drag-grabber"></i> <b>ComfyUI 插件设置</b> </div> <i class="fa-fw fa-solid fa-circle-xmark floating_panel_close"></i> </div> <div class="comfyui-panel-content"> <div id="comfyui-config-errors" class="comfy-config-error" style="display: none;"></div> <!-- 基础设置 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-server"></i> <span>连接设置</span> </div> <div class="comfy-field"> <label for="comfyui-url">调度器 URL (推荐使用HTTPS)</label> <div class="comfy-url-container"> <input id="comfyui-url" type="text" placeholder="例如: https://127.0.0.1:5001"> <button id="comfyui-test-conn" class="comfy-button">测试连接</button> </div> </div> <div class="comfy-field"> <label for="comfyui-api-key">API 密钥 (已加密存储)</label> <input id="comfyui-api-key" type="password" placeholder="在此输入您的密钥"> </div> </div> <!-- 标记设置 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-tags"></i> <span>标记与提示词</span> </div> <div class="comfy-tags-container"> <div class="comfy-field"> <label for="comfyui-start-tag">开始标记</label> <input id="comfyui-start-tag" type="text"> </div> <div class="comfy-field"> <label for="comfyui-end-tag">结束标记</label> <input id="comfyui-end-tag" type="text"> </div> </div> <div class="comfy-field"> <label for="comfyui-prompt-prefix">提示词固定前缀 (LoRA等)</label> <input id="comfyui-prompt-prefix" type="text" placeholder="例如: <lora:cool_style:0.8>"> </div> </div> <!-- 生成设置 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-image"></i> <span>图片生成</span> </div> <div class="comfy-field"> <label for="comfyui-default-model">默认模型 (不指定时生效)</label> <select id="comfyui-default-model"> <option value="">自动选择</option> <option value="waiNSFWIllustrious_v140">waiNSFWIllustrious_v140</option> <option value="Pony_alpha">Pony_alpha</option> </select> </div> <div class="comfy-field"> <label for="comfyui-max-width">最大图片宽度 (px, 100-2000)</label> <input id="comfyui-max-width" type="number" placeholder="600" min="100" max="2000"> </div> </div> <!-- 缓存与性能 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-database"></i> <span>缓存管理</span> </div> <div class="comfy-field"> <label for="comfyui-cache-limit">最大缓存数量 (1-100)</label> <input id="comfyui-cache-limit" type="number" placeholder="20" min="1" max="100"> </div> <div id="comfyui-cache-status" style="margin-top: 8px;">当前缓存: ...</div> </div> <!-- 通知与行为 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-bell"></i> <span>通知与行为</span> </div> <div class="comfy-field"> <label for="comfyui-notification-level">通知级别</label> <select id="comfyui-notification-level"> <option value="silent">静默(仅错误)</option> <option value="standard">标准(成功+错误)</option> <option value="verbose">详细(所有通知)</option> </select> </div> <div class="comfy-field"> <label> <input type="checkbox" id="comfyui-enable-retry" style="width: auto; margin-right: 8px;"> 启用自动重试(图片下载失败时) </label> </div> <div style="margin-left: 24px; display: none;" id="comfyui-retry-options" class="comfy-field"> <label for="comfyui-retry-count">重试次数 (1-5)</label> <input id="comfyui-retry-count" type="number" placeholder="3" min="1" max="5" style="width: 80px;"> </div> <div class="comfy-field"> <label> <input type="checkbox" id="comfyui-debug-mode" style="width: auto; margin-right: 8px;"> 启用调试模式(在控制台查看详细日志) </label> </div> </div> <!-- 操作按钮 --> <div class="comfy-settings-group"> <div class="comfy-settings-group-title"> <i class="fa fa-cogs"></i> <span>操作</span> </div> <button id="comfyui-view-history" class="comfy-button" style="width: 100%; margin-bottom: 8px;"> <i class="fa fa-history"></i> 查看生成历史 </button> <button id="comfyui-force-rescan" class="comfy-button" style="width: 100%; margin-bottom: 8px;"> <i class="fa fa-refresh"></i> 强制重新扫描所有消息 </button> <button id="comfyui-validate-cache" class="comfy-button" style="width: 100%; margin-bottom: 8px;"> <i class="fa fa-check-circle"></i> 验证缓存完整性 </button> <button id="comfyui-clear-cache" class="comfy-button error" style="width: 100%;"> <i class="fa fa-trash"></i> 删除所有图片缓存 </button> </div> </div> </div> `; document.body.insertAdjacentHTML('beforeend', panelHTML); initPanelLogic(); } async function updateCacheStatusDisplay() { try { const display = getCachedDOMElement('comfyui-cache-status') || document.getElementById('comfyui-cache-status'); if (!display) return; let count, sizeMB; if (lruCache) { // 使用LRU缓存统计 const stats = lruCache.getStats(); count = stats.count; sizeMB = stats.totalSizeMB; } else { // 使用旧方法统计 const records = await GM_getValue(STORAGE_KEY_IMAGES, {}); count = Object.keys(records).length; const totalSize = Object.values(records).reduce((total, data) => { return total + (typeof data === 'string' ? data.length : 0); }, 0); sizeMB = (totalSize * 0.75 / 1024 / 1024).toFixed(1); } display.textContent = `当前缓存: ${count} / ${cachedSettings.cacheLimit} 张 (约 ${sizeMB}MB)`; setCachedDOMElement('comfyui-cache-status', display); } catch (error) { ErrorHandler.handle(new ComfyUIError('更新缓存状态失败: ' + error.message, 'CACHE'), 'updateCacheStatusDisplay'); } } // 【优化】创建防抖版本的缓存状态更新函数 const debouncedUpdateCacheStatus = debounce(updateCacheStatusDisplay, 300); // DOM缓存管理 function getCachedDOMElement(id) { return cachedDOMElements[id]; } function setCachedDOMElement(id, element) { cachedDOMElements[id] = element; } function clearDOMCache() { cachedDOMElements = {}; } function showConfigErrors(errors) { const errorContainer = document.getElementById('comfyui-config-errors'); if (errorContainer) { if (errors.length > 0) { errorContainer.innerHTML = errors.map(error => `• ${error}`).join('<br>'); errorContainer.style.display = 'block'; } else { errorContainer.style.display = 'none'; } } } function initPanelLogic() { const panel = document.getElementById(PANEL_ID); const closeButton = panel.querySelector('.floating_panel_close'); const testButton = document.getElementById('comfyui-test-conn'); const clearCacheButton = document.getElementById('comfyui-clear-cache'); const validateCacheButton = document.getElementById('comfyui-validate-cache'); const forceRescanButton = document.getElementById('comfyui-force-rescan'); const viewHistoryButton = document.getElementById('comfyui-view-history'); const debugModeCheckbox = document.getElementById('comfyui-debug-mode'); const urlInput = document.getElementById('comfyui-url'); const startTagInput = document.getElementById('comfyui-start-tag'); const endTagInput = document.getElementById('comfyui-end-tag'); const promptPrefixInput = document.getElementById('comfyui-prompt-prefix'); const maxWidthInput = document.getElementById('comfyui-max-width'); const cacheLimitInput = document.getElementById('comfyui-cache-limit'); const apiKeyInput = document.getElementById('comfyui-api-key'); const defaultModelSelect = document.getElementById('comfyui-default-model'); const notificationLevelSelect = document.getElementById('comfyui-notification-level'); const enableRetryCheckbox = document.getElementById('comfyui-enable-retry'); const retryCountInput = document.getElementById('comfyui-retry-count'); const retryOptions = document.getElementById('comfyui-retry-options'); // 缓存常用DOM元素 setCachedDOMElement('panel', panel); setCachedDOMElement('testButton', testButton); // 重试选项显示/隐藏控制 enableRetryCheckbox.addEventListener('change', () => { retryOptions.style.display = enableRetryCheckbox.checked ? 'block' : 'none'; }); // 查看历史记录按钮 viewHistoryButton.addEventListener('click', () => { panel.style.display = 'none'; openHistoryPanel(); }); closeButton.addEventListener('click', () => { panel.style.display = 'none'; }); testButton.addEventListener('click', () => { try { let url = urlInput.value.trim(); if (!url) { throw new ComfyUIError('请输入调度器的URL', 'VALIDATION'); } if (!url.startsWith('http')) { url = 'https://' + url; // 默认使用HTTPS } if (url.endsWith('/')) { url = url.slice(0, -1); } // 验证URL if (!validateUrl(url)) { throw new ComfyUIError('URL格式无效或不安全,请使用HTTPS', 'VALIDATION'); } urlInput.value = url; const testUrl = url + '/system_stats'; if (typeof toastr !== 'undefined') toastr.info('正在尝试连接服务...'); testButton.className = 'comfy-button testing'; testButton.disabled = true; performanceMonitor.startTimer('testConnection'); GM_xmlhttpRequest({ method: "GET", url: testUrl, timeout: 10000, onload: (res) => { performanceMonitor.endTimer('testConnection'); testButton.disabled = false; testButton.className = res.status === 200 ? 'comfy-button success' : 'comfy-button error'; if (res.status === 200) { if (typeof toastr !== 'undefined') toastr.success('连接成功!'); } else { if (typeof toastr !== 'undefined') toastr.error(`连接失败!状态: ${res.status}`); } }, onerror: (error) => { performanceMonitor.endTimer('testConnection'); testButton.disabled = false; testButton.className = 'comfy-button error'; ErrorHandler.handle(new ComfyUIError('连接失败', 'NETWORK'), 'testConnection'); }, ontimeout: () => { performanceMonitor.endTimer('testConnection'); testButton.disabled = false; testButton.className = 'comfy-button error'; ErrorHandler.handle(new ComfyUIError('连接超时', 'NETWORK'), 'testConnection'); } }); } catch (error) { ErrorHandler.handle(error, 'testConnection'); testButton.disabled = false; testButton.className = 'comfy-button error'; } }); clearCacheButton.addEventListener('click', async () => { if (confirm('您确定要删除所有已生成的图片缓存吗?')) { try { performanceMonitor.startTimer('clearCache'); if (lruCache) { await lruCache.clear(); } else { await GM_setValue(STORAGE_KEY_IMAGES, {}); } await updateCacheStatusDisplay(); // 【注意】这里立即更新,不使用防抖 performanceMonitor.endTimer('clearCache'); showNotification('图片缓存已清空!', 'success', 'standard'); } catch (error) { ErrorHandler.handle(new ComfyUIError('清空缓存失败: ' + error.message, 'CACHE'), 'clearCache'); } } }); validateCacheButton.addEventListener('click', async () => { try { validateCacheButton.disabled = true; validateCacheButton.textContent = '验证中...'; const validCount = await validateCacheIntegrity(); await updateCacheStatusDisplay(); // 【注意】这里立即更新,不使用防抖 validateCacheButton.disabled = false; validateCacheButton.textContent = '验证缓存完整性'; if (typeof toastr !== 'undefined') { toastr.success(`缓存验证完成,有效记录: ${validCount} 条`); } } catch (error) { validateCacheButton.disabled = false; validateCacheButton.textContent = '验证缓存完整性'; ErrorHandler.handle(error, 'validateCache'); } }); // 调试模式开关 debugModeCheckbox.addEventListener('change', async () => { debugMode = debugModeCheckbox.checked; await GM_setValue('comfyui_debug_mode', debugMode); if (debugMode) { console.log('[ComfyUI Debug] 调试模式已启用'); if (typeof toastr !== 'undefined') { toastr.info('调试模式已启用,请查看浏览器控制台'); } } else { console.log('[ComfyUI Debug] 调试模式已禁用'); } }); // 强制重新扫描所有消息 forceRescanButton.addEventListener('click', async () => { try { forceRescanButton.disabled = true; forceRescanButton.textContent = '扫描中...'; const chatElement = document.getElementById('chat'); if (chatElement) { const allMessages = chatElement.querySelectorAll('.mes'); const savedImages = await GM_getValue(STORAGE_KEY_IMAGES, {}); console.log(`[ComfyUI] 开始强制重新扫描 ${allMessages.length} 条消息`); // 清除已有的监听器标记 allMessages.forEach(node => { const mesTextElement = node.querySelector('.mes_text'); if (mesTextElement) { mesTextElement.dataset.listenersAttached = ''; // 移除现有的按钮组以避免重复 const existingButtons = mesTextElement.querySelectorAll('.comfy-button-group'); existingButtons.forEach(btn => btn.remove()); } }); // 重新处理所有消息 for (const node of allMessages) { try { const mesTextElement = node.querySelector('.mes_text'); if (mesTextElement && !mesTextElement.dataset.listenersAttached) { mesTextElement.addEventListener('touchstart', (event) => handleComfyButtonClick(event, true), { passive: false }); mesTextElement.addEventListener('click', (event) => handleComfyButtonClick(event, false)); mesTextElement.dataset.listenersAttached = 'true'; } await processMessageForComfyButton(node, savedImages); } catch (error) { console.error('处理消息节点失败:', error); } } console.log('[ComfyUI] 强制重新扫描完成'); if (typeof toastr !== 'undefined') { toastr.success(`已重新扫描 ${allMessages.length} 条消息`); } } else { if (typeof toastr !== 'undefined') { toastr.error('未找到聊天区域,无法执行扫描'); } } forceRescanButton.disabled = false; forceRescanButton.textContent = '强制重新扫描所有消息'; } catch (error) { forceRescanButton.disabled = false; forceRescanButton.textContent = '强制重新扫描所有消息'; ErrorHandler.handle(new ComfyUIError('强制重新扫描失败: ' + error.message, 'UI'), 'forceRescan'); } }); loadSettings(urlInput, startTagInput, endTagInput, promptPrefixInput, maxWidthInput, cacheLimitInput, apiKeyInput, defaultModelSelect, notificationLevelSelect, enableRetryCheckbox, retryCountInput, debugModeCheckbox).then(() => { applyCurrentMaxWidthToAllImages(); }); [urlInput, startTagInput, endTagInput, promptPrefixInput, maxWidthInput, cacheLimitInput, apiKeyInput, defaultModelSelect, notificationLevelSelect, enableRetryCheckbox, retryCountInput, debugModeCheckbox].forEach(input => { const eventType = input.type === 'checkbox' ? 'change' : (input.tagName.toLowerCase() === 'select' ? 'change' : 'input'); input.addEventListener(eventType, async () => { try { if (input === urlInput) testButton.className = 'comfy-button'; // 【优化】使用防抖版本的saveSettings,减少频繁保存 debouncedSaveSettings(urlInput, startTagInput, endTagInput, promptPrefixInput, maxWidthInput, cacheLimitInput, apiKeyInput, defaultModelSelect, notificationLevelSelect, enableRetryCheckbox, retryCountInput); if (input === maxWidthInput) applyCurrentMaxWidthToAllImages(); if (input === urlInput) { if (socket) socket.disconnect(); setTimeout(connectWebSocket, 500); // 延迟重连 } } catch (error) { ErrorHandler.handle(error, 'saveSettings'); } }); }); } async function loadSettings(urlInput, startTagInput, endTagInput, promptPrefixInput, maxWidthInput, cacheLimitInput, apiKeyInput, defaultModelSelect, notificationLevelSelect, enableRetryCheckbox, retryCountInput, debugModeCheckbox) { try { performanceMonitor.startTimer('loadSettings'); cachedSettings.comfyuiUrl = await GM_getValue('comfyui_url', 'https://127.0.0.1:5001'); cachedSettings.startTag = await GM_getValue('comfyui_start_tag', 'image###'); cachedSettings.endTag = await GM_getValue('comfyui_end_tag', '###'); cachedSettings.promptPrefix = await GM_getValue(STORAGE_KEY_PROMPT_PREFIX, ''); cachedSettings.maxWidth = await GM_getValue(STORAGE_KEY_MAX_WIDTH, 600); cachedSettings.cacheLimit = await GM_getValue(STORAGE_KEY_CACHE_LIMIT, 20); // 解密API密钥 const encryptedApiKey = await GM_getValue('comfyui_api_key', ''); cachedSettings.apiKey = decryptApiKey(encryptedApiKey); cachedSettings.defaultModel = await GM_getValue('comfyui_default_model', ''); cachedSettings.notificationLevel = await GM_getValue('comfyui_notification_level', 'silent'); cachedSettings.enableRetry = await GM_getValue('comfyui_enable_retry', true); cachedSettings.retryCount = await GM_getValue('comfyui_retry_count', 3); urlInput.value = cachedSettings.comfyuiUrl; startTagInput.value = cachedSettings.startTag; endTagInput.value = cachedSettings.endTag; promptPrefixInput.value = cachedSettings.promptPrefix; maxWidthInput.value = cachedSettings.maxWidth; cacheLimitInput.value = cachedSettings.cacheLimit; apiKeyInput.value = cachedSettings.apiKey; defaultModelSelect.value = cachedSettings.defaultModel; notificationLevelSelect.value = cachedSettings.notificationLevel; enableRetryCheckbox.checked = cachedSettings.enableRetry; retryCountInput.value = cachedSettings.retryCount; // 显示/隐藏重试选项 document.getElementById('comfyui-retry-options').style.display = cachedSettings.enableRetry ? 'block' : 'none'; // 加载调试模式设置 debugMode = await GM_getValue('comfyui_debug_mode', false); debugModeCheckbox.checked = debugMode; document.documentElement.style.setProperty('--comfy-image-max-width', (cachedSettings.maxWidth || 600) + 'px'); await updateCacheStatusDisplay(); // 验证配置 const configErrors = validateConfig(cachedSettings); showConfigErrors(configErrors); performanceMonitor.endTimer('loadSettings'); } catch (error) { ErrorHandler.handle(new ComfyUIError('加载设置失败: ' + error.message, 'CONFIG'), 'loadSettings'); } } async function saveSettings(urlInput, startTagInput, endTagInput, promptPrefixInput, maxWidthInput, cacheLimitInput, apiKeyInput, defaultModelSelect, notificationLevelSelect, enableRetryCheckbox, retryCountInput) { try { performanceMonitor.startTimer('saveSettings'); const newSettings = { comfyuiUrl: urlInput.value.trim(), startTag: startTagInput.value, endTag: endTagInput.value, promptPrefix: promptPrefixInput.value.trim(), maxWidth: parseInt(maxWidthInput.value) || 600, cacheLimit: parseInt(cacheLimitInput.value) || 20, apiKey: apiKeyInput.value.trim(), defaultModel: defaultModelSelect.value, notificationLevel: notificationLevelSelect.value, enableRetry: enableRetryCheckbox.checked, retryCount: parseInt(retryCountInput.value) || 3 }; // 验证新配置 const configErrors = validateConfig(newSettings); showConfigErrors(configErrors); // 如果有严重错误,不保存配置 if (configErrors.length > 0 && configErrors.some(error => error.includes('URL'))) { return; } // 更新缓存设置 Object.assign(cachedSettings, newSettings); // 加密并保存API密钥 const encryptedApiKey = encryptApiKey(cachedSettings.apiKey); await GM_setValue('comfyui_url', cachedSettings.comfyuiUrl); await GM_setValue('comfyui_start_tag', cachedSettings.startTag); await GM_setValue('comfyui_end_tag', cachedSettings.endTag); await GM_setValue(STORAGE_KEY_PROMPT_PREFIX, cachedSettings.promptPrefix); await GM_setValue(STORAGE_KEY_MAX_WIDTH, cachedSettings.maxWidth); await GM_setValue(STORAGE_KEY_CACHE_LIMIT, cachedSettings.cacheLimit); await GM_setValue('comfyui_api_key', encryptedApiKey); await GM_setValue('comfyui_default_model', cachedSettings.defaultModel); await GM_setValue('comfyui_notification_level', cachedSettings.notificationLevel); await GM_setValue('comfyui_enable_retry', cachedSettings.enableRetry); await GM_setValue('comfyui_retry_count', cachedSettings.retryCount); // 如果缓存限制改变,更新LRU缓存大小 if (lruCache && lruCache.maxSize !== cachedSettings.cacheLimit) { lruCache.maxSize = cachedSettings.cacheLimit; if (lruCache.cache.size > lruCache.maxSize) { await lruCache.evict(); } } document.documentElement.style.setProperty('--comfy-image-max-width', cachedSettings.maxWidth + 'px'); debouncedUpdateCacheStatus(); // 【优化】使用防抖版本 performanceMonitor.endTimer('saveSettings'); } catch (error) { ErrorHandler.handle(new ComfyUIError('保存设置失败: ' + error.message, 'CONFIG'), 'saveSettings'); } } // 【优化】创建防抖版本的设置保存函数 const debouncedSaveSettings = debounce(saveSettings, 500); async function applyCurrentMaxWidthToAllImages() { try { const images = document.querySelectorAll('.comfy-image-container img'); const maxWidthPx = (cachedSettings.maxWidth || 600) + 'px'; images.forEach(img => { img.style.maxWidth = maxWidthPx; }); } catch (error) { console.error('应用图片宽度设置失败:', error); } } function addMainButton() { if (document.getElementById(BUTTON_ID)) return; const optionsMenuContent = document.querySelector('#options .options-content'); if (optionsMenuContent) { const continueButton = optionsMenuContent.querySelector('#option_continue'); if (continueButton) { const comfyButton = document.createElement('a'); comfyButton.id = BUTTON_ID; comfyButton.className = 'interactable'; comfyButton.innerHTML = `<i class="fa-lg fa-solid fa-image"></i><span>ComfyUI生图 (优化版)</span>`; comfyButton.style.cursor = 'pointer'; comfyButton.addEventListener('click', (event) => { event.preventDefault(); document.getElementById(PANEL_ID).style.display = 'flex'; document.getElementById('options').style.display = 'none'; }); continueButton.parentNode.insertBefore(comfyButton, continueButton.nextSibling); } } } // --- Helper and Cache Management --- function escapeRegex(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function generateClientId() { return 'client-' + Math.random().toString(36).substring(2, 15) + '-' + Date.now(); } function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; } return 'comfy-id-' + Math.abs(hash).toString(36); } // --- 防抖节流工具函数 --- /** * 防抖函数 - 延迟执行,多次触发只执行最后一次 * @param {Function} func - 要执行的函数 * @param {number} wait - 延迟时间(毫秒) * @returns {Function} 防抖后的函数 */ function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * 节流函数 - 限制执行频率,固定时间内只执行一次 * @param {Function} func - 要执行的函数 * @param {number} limit - 时间间隔(毫秒) * @returns {Function} 节流后的函数 */ function throttle(func, limit = 200) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // --- Image Download Helper --- function downloadImage(imageData, filename = 'comfyui-image.png') { try { const link = document.createElement('a'); link.href = imageData; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('图片已保存!', 'success', 'standard'); } catch (error) { console.error('下载图片失败:', error); showNotification('下载失败,请重试', 'error'); } } // --- IndexedDB Cache Manager --- class IndexedDBCache { constructor(dbName = 'ComfyUICache', version = 1) { this.dbName = dbName; this.version = version; this.db = null; this.storeName = 'images'; } async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); request.onerror = () => { console.error('[IndexedDB] 打开数据库失败:', request.error); reject(new ComfyUIError('IndexedDB初始化失败', 'CACHE')); }; request.onsuccess = () => { this.db = request.result; console.log('[IndexedDB] 数据库已打开'); resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; // 创建对象存储 if (!db.objectStoreNames.contains(this.storeName)) { const objectStore = db.createObjectStore(this.storeName, { keyPath: 'id' }); objectStore.createIndex('timestamp', 'timestamp', { unique: false }); objectStore.createIndex('size', 'size', { unique: false }); console.log('[IndexedDB] 对象存储已创建'); } }; }); } async set(id, data, metadata = {}) { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readwrite'); const objectStore = transaction.objectStore(this.storeName); const record = { id, data, timestamp: Date.now(), size: data.length, accessCount: metadata.accessCount || 0, ...metadata }; const request = objectStore.put(record); request.onsuccess = () => { if (debugMode) { console.log(`[IndexedDB] 已保存: ${id}, 大小: ${(data.length / 1024).toFixed(2)}KB`); } resolve(); }; request.onerror = () => { console.error('[IndexedDB] 保存失败:', request.error); reject(new ComfyUIError('IndexedDB保存失败', 'CACHE')); }; }); } async get(id) { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readonly'); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.get(id); request.onsuccess = () => { const record = request.result; if (record) { if (debugMode) { console.log(`[IndexedDB] 读取成功: ${id}`); } resolve(record); } else { resolve(null); } }; request.onerror = () => { console.error('[IndexedDB] 读取失败:', request.error); reject(new ComfyUIError('IndexedDB读取失败', 'CACHE')); }; }); } async delete(id) { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readwrite'); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.delete(id); request.onsuccess = () => { if (debugMode) { console.log(`[IndexedDB] 已删除: ${id}`); } resolve(); }; request.onerror = () => { console.error('[IndexedDB] 删除失败:', request.error); reject(new ComfyUIError('IndexedDB删除失败', 'CACHE')); }; }); } async getAll() { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readonly'); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.getAll(); request.onsuccess = () => { resolve(request.result || []); }; request.onerror = () => { console.error('[IndexedDB] 获取全部数据失败:', request.error); reject(new ComfyUIError('IndexedDB查询失败', 'CACHE')); }; }); } async clear() { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readwrite'); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.clear(); request.onsuccess = () => { console.log('[IndexedDB] 已清空所有数据'); resolve(); }; request.onerror = () => { console.error('[IndexedDB] 清空失败:', request.error); reject(new ComfyUIError('IndexedDB清空失败', 'CACHE')); }; }); } async count() { return new Promise((resolve, reject) => { if (!this.db) { reject(new ComfyUIError('IndexedDB未初始化', 'CACHE')); return; } const transaction = this.db.transaction([this.storeName], 'readonly'); const objectStore = transaction.objectStore(this.storeName); const request = objectStore.count(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(new ComfyUIError('IndexedDB计数失败', 'CACHE')); }; }); } async getTotalSize() { const records = await this.getAll(); return records.reduce((total, record) => total + (record.size || 0), 0); } } // --- LRU Cache Manager (增强版 - 配合IndexedDB使用) --- class LRUCache { constructor(maxSize = 20, useIndexedDB = true) { this.maxSize = maxSize; this.cache = new Map(); // key -> {data, timestamp, size, accessCount} this.useIndexedDB = useIndexedDB; this.indexedDB = null; } async load() { try { // 【优化】优先从IndexedDB加载 if (this.useIndexedDB && this.indexedDB) { const records = await this.indexedDB.getAll(); this.cache.clear(); for (const record of records) { this.cache.set(record.id, { data: record.data, timestamp: record.timestamp || Date.now(), size: record.size || record.data.length, accessCount: record.accessCount || 0 }); } console.log(`[LRU Cache] 从IndexedDB加载 ${this.cache.size} 条记录`); } else { // 回退到GM_getValue const records = await GM_getValue(STORAGE_KEY_IMAGES, {}); this.cache.clear(); for (const [id, data] of Object.entries(records)) { this.cache.set(id, { data, timestamp: Date.now(), size: data.length, accessCount: 0 }); } console.log(`[LRU Cache] 从GM_getValue加载 ${this.cache.size} 条记录`); } } catch (error) { console.error('LRU缓存加载失败:', error); } } async get(key) { const item = this.cache.get(key); if (item) { // 更新访问时间和计数 item.timestamp = Date.now(); item.accessCount++; this.cache.delete(key); this.cache.set(key, item); // 移到末尾(最近使用) if (debugMode) { console.log(`[LRU Cache] 缓存命中: ${key}, 访问次数: ${item.accessCount}`); } return item.data; } return null; } async set(key, data) { // 如果已存在,先删除 if (this.cache.has(key)) { this.cache.delete(key); } // 添加新项 const item = { data, timestamp: Date.now(), size: data.length, accessCount: 1 }; this.cache.set(key, item); // 【优化】如果使用IndexedDB,直接保存到IndexedDB if (this.useIndexedDB && this.indexedDB) { try { await this.indexedDB.set(key, data, { timestamp: item.timestamp, accessCount: item.accessCount }); } catch (error) { console.error('[LRU Cache] IndexedDB保存失败,回退到GM_setValue:', error); await this.save(); // 回退方案 } } else { // 回退到GM_setValue await this.save(); } // 如果超过最大容量,移除最少使用的项 if (this.cache.size > this.maxSize) { await this.evict(); } } async evict() { // 获取所有缓存项并按优先级排序 const items = Array.from(this.cache.entries()).map(([key, value]) => ({ key, ...value, score: this.calculateScore(value) })); // 按分数排序(分数越低越应该被删除) items.sort((a, b) => a.score - b.score); // 删除最低优先级的项 const toRemove = items.slice(0, this.cache.size - this.maxSize + 1); for (const item of toRemove) { this.cache.delete(item.key); // 【优化】同时从IndexedDB删除 if (this.useIndexedDB && this.indexedDB) { try { await this.indexedDB.delete(item.key); } catch (error) { console.error('[LRU Cache] IndexedDB删除失败:', error); } } if (debugMode) { console.log(`[LRU Cache] 移除缓存: ${item.key}, 分数: ${item.score.toFixed(2)}`); } } } calculateScore(item) { // 计算缓存项的重要性分数 const ageInDays = (Date.now() - item.timestamp) / (1000 * 60 * 60 * 24); const sizeInMB = item.size / (1024 * 1024); // 分数 = 访问次数 / (年龄 + 1) - 大小惩罚 // 访问次数越多、越新的项分数越高 // 文件越大会降低分数 return (item.accessCount / (ageInDays + 1)) - (sizeInMB * 0.5); } async delete(key) { if (this.cache.has(key)) { this.cache.delete(key); // 【优化】从IndexedDB删除 if (this.useIndexedDB && this.indexedDB) { try { await this.indexedDB.delete(key); } catch (error) { console.error('[LRU Cache] IndexedDB删除失败:', error); await this.save(); // 回退方案 } } else { await this.save(); } return true; } return false; } async save() { try { // 【优化】如果使用IndexedDB,不需要批量保存 if (this.useIndexedDB && this.indexedDB) { // IndexedDB已经实时保存,这里不需要做任何事 return; } // 回退到GM_setValue const records = {}; for (const [key, value] of this.cache.entries()) { records[key] = value.data; } await GM_setValue(STORAGE_KEY_IMAGES, records); } catch (error) { console.error('LRU缓存保存失败:', error); } } async clear() { this.cache.clear(); // 【优化】同时清空IndexedDB if (this.useIndexedDB && this.indexedDB) { try { await this.indexedDB.clear(); } catch (error) { console.error('[LRU Cache] IndexedDB清空失败:', error); await GM_setValue(STORAGE_KEY_IMAGES, {}); // 回退方案 } } else { await GM_setValue(STORAGE_KEY_IMAGES, {}); } } getStats() { const items = Array.from(this.cache.values()); const totalSize = items.reduce((sum, item) => sum + item.size, 0); const totalAccess = items.reduce((sum, item) => sum + item.accessCount, 0); return { count: this.cache.size, maxSize: this.maxSize, totalSize, totalSizeMB: (totalSize * 0.75 / 1024 / 1024).toFixed(1), totalAccess, avgAccessCount: items.length > 0 ? (totalAccess / items.length).toFixed(1) : 0 }; } getAll() { return Array.from(this.cache.entries()).map(([key, value]) => ({ id: key, ...value, sizeMB: (value.size * 0.75 / 1024 / 1024).toFixed(2), ageInHours: ((Date.now() - value.timestamp) / (1000 * 60 * 60)).toFixed(1) })); } } // 全局LRU缓存实例 let lruCache = null; // --- Retry with Exponential Backoff --- async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) { throw error; } const delay = baseDelay * Math.pow(2, i); console.log(`重试 ${i + 1}/${maxRetries},等待 ${delay}ms...`); if (debugMode) { console.log(`[ComfyUI Debug] 重试原因: ${error.message}`); } await new Promise(resolve => setTimeout(resolve, delay)); } } } async function saveImageRecord(generationId, imageBase64Data) { try { performanceMonitor.startTimer('saveImageRecord'); if (lruCache) { await lruCache.set(generationId, imageBase64Data); const stats = lruCache.getStats(); if (debugMode) { console.log(`[LRU Cache] 保存成功: ${generationId}, 当前: ${stats.count}/${stats.maxSize}`); } } else { // 回退到旧方法 let records = await GM_getValue(STORAGE_KEY_IMAGES, {}); if (records.hasOwnProperty(generationId)) delete records[generationId]; records[generationId] = imageBase64Data; const keys = Object.keys(records); if (keys.length > cachedSettings.cacheLimit) { const keysToDelete = keys.slice(0, keys.length - cachedSettings.cacheLimit); keysToDelete.forEach(key => delete records[key]); console.log(`缓存已满,删除了 ${keysToDelete.length} 条旧记录。`); } await GM_setValue(STORAGE_KEY_IMAGES, records); } debouncedUpdateCacheStatus(); // 【优化】使用防抖版本 performanceMonitor.endTimer('saveImageRecord'); } catch (error) { ErrorHandler.handle(new ComfyUIError('保存图片记录失败: ' + error.message, 'CACHE'), 'saveImageRecord'); } } async function deleteImageRecord(generationId) { try { if (lruCache) { await lruCache.delete(generationId); } else { const records = await GM_getValue(STORAGE_KEY_IMAGES, {}); delete records[generationId]; await GM_setValue(STORAGE_KEY_IMAGES, records); } debouncedUpdateCacheStatus(); // 【优化】使用防抖版本 } catch (error) { ErrorHandler.handle(new ComfyUIError('删除图片记录失败: ' + error.message, 'CACHE'), 'deleteImageRecord'); } } // --- Chat Message Processing and Image Generation --- function handleComfyButtonClick(event, isTouch = false) { try { const button = event.target.closest('.comfy-chat-generate-button'); if (!button) return; if (isTouch) { event.preventDefault(); const now = Date.now(); if (now - lastTapTimestamp < TAP_THRESHOLD) return; lastTapTimestamp = now; onGenerateButtonClickLogic(button); } else { if (Date.now() - lastTapTimestamp < TAP_THRESHOLD) return; onGenerateButtonClickLogic(button); } } catch (error) { ErrorHandler.handle(new ComfyUIError('处理按钮点击失败: ' + error.message, 'UI'), 'handleComfyButtonClick'); } } async function processMessageForComfyButton(messageNode, savedImagesCache) { try { const mesText = messageNode.querySelector('.mes_text'); if (!mesText) { if (debugMode) console.log('[ComfyUI Debug] 未找到 .mes_text 元素'); return; } const { startTag, endTag } = cachedSettings; if (!startTag || !endTag) { if (debugMode) console.log('[ComfyUI Debug] 开始或结束标签未配置:', { startTag, endTag }); return; } if (debugMode) { console.log('[ComfyUI Debug] 开始处理消息, 标签:', { startTag, endTag }); console.log('[ComfyUI Debug] 消息内容:', mesText.innerHTML); } const regex = new RegExp( escapeRegex(startTag) + '(?:\\[model=([\\w.-]+)\\])?' + '([\\s\\S]*?)' + escapeRegex(endTag), 'g' ); if (debugMode) console.log('[ComfyUI Debug] 使用的正则表达式:', regex); const currentHtml = mesText.innerHTML; const matches = currentHtml.match(regex); if (debugMode) console.log('[ComfyUI Debug] 正则匹配结果:', matches); // 检查是否存在文本内容中的标签(未被HTML转义的) const textContent = mesText.textContent || mesText.innerText || ''; if (debugMode) console.log('[ComfyUI Debug] 纯文本内容:', textContent); const textMatches = textContent.match(regex); if (debugMode) console.log('[ComfyUI Debug] 纯文本匹配结果:', textMatches); // 尝试处理HTML内容 let processedHtml = false; if (regex.test(currentHtml) && !mesText.querySelector('.comfy-button-group')) { if (debugMode) console.log('[ComfyUI Debug] 在HTML中发现匹配,开始替换'); mesText.innerHTML = currentHtml.replace(regex, (match, model, prompt) => { if (debugMode) console.log('[ComfyUI Debug] 替换匹配项:', { match, model, prompt }); const cleanPrompt = sanitizePrompt(prompt.trim()); const encodedPrompt = cleanPrompt.replace(/"/g, '"'); const modelName = model ? model.trim() : ''; const generationId = simpleHash(modelName + cleanPrompt); return `<span class="comfy-button-group" data-generation-id="${generationId}"><button class="comfy-button comfy-chat-generate-button" data-prompt="${encodedPrompt}" data-model="${modelName}">开始生成</button></span>`; }); processedHtml = true; } // 如果HTML中没有找到,尝试处理纯文本内容 if (!processedHtml && textMatches && textMatches.length > 0 && !mesText.querySelector('.comfy-button-group')) { if (debugMode) console.log('[ComfyUI Debug] HTML中未找到匹配,尝试处理纯文本内容'); // 重置正则表达式状态 regex.lastIndex = 0; let newHtml = currentHtml; let match; while ((match = regex.exec(textContent)) !== null) { if (debugMode) console.log('[ComfyUI Debug] 处理纯文本匹配:', match); const fullMatch = match[0]; const model = match[1] || ''; const prompt = match[2] || ''; const cleanPrompt = sanitizePrompt(prompt.trim()); const encodedPrompt = cleanPrompt.replace(/"/g, '"'); const modelName = model.trim(); const generationId = simpleHash(modelName + cleanPrompt); const buttonHtml = `<span class="comfy-button-group" data-generation-id="${generationId}"><button class="comfy-button comfy-chat-generate-button" data-prompt="${encodedPrompt}" data-model="${modelName}">开始生成</button></span>`; // 在HTML中查找并替换对应的文本 newHtml = newHtml.replace(escapeRegex(fullMatch), buttonHtml); } if (newHtml !== currentHtml) { mesText.innerHTML = newHtml; processedHtml = true; } } // 特殊处理:检查是否有被HTML编码的标签 const htmlEncodedStartTag = startTag.replace(/</g, '<').replace(/>/g, '>'); const htmlEncodedEndTag = endTag.replace(/</g, '<').replace(/>/g, '>'); if (htmlEncodedStartTag !== startTag || htmlEncodedEndTag !== endTag) { if (debugMode) console.log('[ComfyUI Debug] 检查HTML编码的标签:', { htmlEncodedStartTag, htmlEncodedEndTag }); const htmlEncodedRegex = new RegExp( escapeRegex(htmlEncodedStartTag) + '(?:\\[model=([\\w.-]+)\\])?' + '([\\s\\S]*?)' + escapeRegex(htmlEncodedEndTag), 'g' ); if (htmlEncodedRegex.test(currentHtml) && !mesText.querySelector('.comfy-button-group')) { if (debugMode) console.log('[ComfyUI Debug] 发现HTML编码的标签,进行替换'); mesText.innerHTML = currentHtml.replace(htmlEncodedRegex, (match, model, prompt) => { if (debugMode) console.log('[ComfyUI Debug] 替换HTML编码匹配项:', { match, model, prompt }); const cleanPrompt = sanitizePrompt(prompt.trim()); const encodedPrompt = cleanPrompt.replace(/"/g, '"'); const modelName = model ? model.trim() : ''; const generationId = simpleHash(modelName + cleanPrompt); return `<span class="comfy-button-group" data-generation-id="${generationId}"><button class="comfy-button comfy-chat-generate-button" data-prompt="${encodedPrompt}" data-model="${modelName}">开始生成</button></span>`; }); processedHtml = true; } } if (debugMode && !processedHtml) { console.log('[ComfyUI Debug] 未找到任何匹配的标签'); } const buttonGroups = mesText.querySelectorAll('.comfy-button-group'); if (debugMode) console.log('[ComfyUI Debug] 找到的按钮组数量:', buttonGroups.length); buttonGroups.forEach(group => { if (group.dataset.listenerAttached) return; const generationId = group.dataset.generationId; if (savedImagesCache[generationId]) { displayImage(group, savedImagesCache[generationId]); const generateButton = group.querySelector('.comfy-chat-generate-button'); if(generateButton) setupGeneratedState(generateButton, generationId); } group.dataset.listenerAttached = 'true'; }); } catch (error) { ErrorHandler.handle(new ComfyUIError('处理消息失败: ' + error.message, 'UI'), 'processMessageForComfyButton'); } } function setupGeneratedState(generateButton, generationId) { try { generateButton.textContent = '重新生成'; generateButton.disabled = false; generateButton.classList.remove('testing', 'success', 'error'); const group = generateButton.closest('.comfy-button-group'); let deleteButton = group.querySelector('.comfy-delete-button'); if (!deleteButton) { deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; deleteButton.className = 'comfy-button error comfy-delete-button'; deleteButton.addEventListener('click', async () => { try { await deleteImageRecord(generationId); const imageContainer = group.nextElementSibling; if (imageContainer?.classList.contains('comfy-image-container')) { imageContainer.remove(); } deleteButton.remove(); generateButton.textContent = '开始生成'; } catch (error) { ErrorHandler.handle(new ComfyUIError('删除图片失败: ' + error.message, 'CACHE'), 'deleteImage'); } }); generateButton.insertAdjacentElement('afterend', deleteButton); } deleteButton.style.display = 'inline-flex'; } catch (error) { ErrorHandler.handle(new ComfyUIError('设置生成状态失败: ' + error.message, 'UI'), 'setupGeneratedState'); } } async function onGenerateButtonClickLogic(button) { try { const group = button.closest('.comfy-button-group'); let prompt = button.dataset.prompt; const generationId = group.dataset.generationId; let model = button.dataset.model || ''; if (!model) { model = await GM_getValue('comfyui_default_model', ''); } if (button.disabled) return; // 获取并解密API密钥 const encryptedApiKey = await GM_getValue('comfyui_api_key', ''); const apiKey = decryptApiKey(encryptedApiKey); if (!apiKey) { throw new ComfyUIError('请先在设置面板中配置 API 密钥!', 'AUTH'); } if (Date.now() < globalCooldownEndTime) { const remainingTime = Math.ceil((globalCooldownEndTime - Date.now()) / 1000); if (typeof toastr !== 'undefined') toastr.warning(`请稍候,冷却中 (${remainingTime}s)。`); return; } // 验证和净化提示词 prompt = sanitizePrompt(prompt); if (!prompt) { throw new ComfyUIError('提示词内容无效', 'VALIDATION'); } button.textContent = '请求中...'; button.disabled = true; button.classList.add('testing'); const deleteButton = group.querySelector('.comfy-delete-button'); if (deleteButton) deleteButton.style.display = 'none'; const oldImageContainer = group.nextElementSibling; if (oldImageContainer?.classList.contains('comfy-image-container')) { oldImageContainer.style.opacity = '0.5'; } performanceMonitor.startTimer('generateImage'); connectWebSocket(); const { comfyuiUrl, promptPrefix } = cachedSettings; if (!comfyuiUrl) { throw new ComfyUIError('调度器 URL 未配置', 'CONFIG'); } if (!validateUrl(comfyuiUrl)) { throw new ComfyUIError('调度器 URL 格式无效', 'CONFIG'); } if (promptPrefix) prompt = promptPrefix + ' ' + prompt; if (typeof toastr !== 'undefined') toastr.info('正在向调度器发送请求...'); const promptResponse = await sendPromptRequestToScheduler(comfyuiUrl, { client_id: generateClientId(), positive_prompt: prompt, api_key: apiKey, model: model }); const promptId = promptResponse.prompt_id; if (!promptId) { throw new ComfyUIError('调度器未返回有效的任务 ID', 'GENERATION'); } if (socket && socket.connected) { socket.emit('subscribe_to_prompt', { prompt_id: promptId }); } else { throw new ComfyUIError('WebSocket连接未建立', 'WEBSOCKET'); } activePrompts[promptId] = { button, generationId }; button.textContent = '生成中...'; if(promptResponse.assigned_instance_name) { if (typeof toastr !== 'undefined') { toastr.success(`任务已分配到: ${promptResponse.assigned_instance_name} (队列: ${promptResponse.assigned_instance_queue_size})`); } } performanceMonitor.endTimer('generateImage'); } catch (error) { performanceMonitor.endTimer('generateImage'); ErrorHandler.handle(error, 'generateImage'); button.textContent = error.type === 'AUTH' ? '认证失败' : '请求失败'; button.classList.add('error'); setTimeout(() => { button.classList.remove('testing', 'error'); button.textContent = group.querySelector('.comfy-delete-button') ? '重新生成' : '开始生成'; button.disabled = false; if(oldImageContainer) oldImageContainer.style.opacity = '1'; if(deleteButton) deleteButton.style.display = 'inline-flex'; }, 3000); if (error.type === 'AUTH') { // 认证失败时打开设置面板 document.getElementById(PANEL_ID).style.display = 'flex'; } } } // --- API Request Functions --- function sendPromptRequestToScheduler(url, payload) { return new Promise((resolve, reject) => { // 验证payload if (!payload.api_key) { reject(new ComfyUIError('API密钥缺失', 'AUTH')); return; } if (!payload.positive_prompt || payload.positive_prompt.trim().length === 0) { reject(new ComfyUIError('提示词不能为空', 'VALIDATION')); return; } GM_xmlhttpRequest({ method: 'POST', url: `${url}/generate`, headers: { 'Content-Type': 'application/json', 'User-Agent': 'ComfyUI-TamperMonkey-Script/2.0' }, data: JSON.stringify(payload), timeout: 15000, onload: (res) => { try { if (res.status === 202 || res.status === 200) { const responseData = JSON.parse(res.responseText); resolve(responseData); } else if (res.status === 401 || res.status === 403) { reject(new ComfyUIError('API密钥无效或权限不足', 'AUTH')); } else { let errorMsg = `调度器 API 错误: ${res.status}`; try { const errorJson = JSON.parse(res.responseText); if (errorJson.error) errorMsg = errorJson.error; } catch (e) { // JSON解析失败,使用默认错误消息 } reject(new ComfyUIError(errorMsg, 'GENERATION')); } } catch (error) { reject(new ComfyUIError('解析服务器响应失败: ' + error.message, 'NETWORK')); } }, onerror: (e) => reject(new ComfyUIError('无法连接到调度器 API', 'NETWORK')), ontimeout: () => reject(new ComfyUIError('连接调度器 API 超时', 'NETWORK')), }); }); } // 验证图片URL是否可访问 function validateImageUrl(imageUrl) { return new Promise((resolve, reject) => { // 如果是base64数据,直接通过验证 if (imageUrl.startsWith('data:image/')) { resolve(true); return; } // 验证URL格式 try { new URL(imageUrl); } catch (e) { reject(new ComfyUIError('图片URL格式无效', 'VALIDATION')); return; } // 尝试HEAD请求验证图片是否可访问 GM_xmlhttpRequest({ method: 'HEAD', url: imageUrl, timeout: 5000, onload: (response) => { if (response.status === 200 || response.status === 304) { resolve(true); } else { reject(new ComfyUIError(`图片URL不可访问 (状态: ${response.status})`, 'NETWORK')); } }, onerror: () => reject(new ComfyUIError('无法访问图片URL', 'NETWORK')), ontimeout: () => reject(new ComfyUIError('验证图片URL超时', 'NETWORK')) }); }); } // 带验证的图片显示函数(用于新生成的图片)- 简化版(参考34.0)+ 移动端优化 async function displayImageWithValidation(anchorElement, imageBase64Data, generationId) { return new Promise((resolve, reject) => { try { // 验证base64数据 if (!imageBase64Data || !imageBase64Data.startsWith('data:image/')) { throw new ComfyUIError('无效的图片数据格式', 'VALIDATION'); } const group = anchorElement.closest('.comfy-button-group') || anchorElement; let container = group.nextElementSibling; if (!container || !container.classList.contains('comfy-image-container')) { container = document.createElement('div'); container.className = 'comfy-image-container'; const img = document.createElement('img'); img.alt = 'ComfyUI 生成的图片'; img.loading = 'eager'; // 【修复】改为立即加载,不使用lazy container.appendChild(img); group.insertAdjacentElement('afterend', container); } container.style.opacity = '1'; const imgElement = container.querySelector('img'); // 清除之前的错误提示(如果有) const oldError = container.querySelector('.comfy-image-error'); if (oldError) { oldError.remove(); } imgElement.style.display = ''; // 添加加载状态提示(简化版) const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'comfy-loading-indicator'; loadingIndicator.textContent = '图片渲染中'; container.insertBefore(loadingIndicator, imgElement); // 设置图片加载成功处理 imgElement.onload = () => { if (loadingIndicator.parentNode) { loadingIndicator.remove(); } container.style.opacity = '1'; if (debugMode) { console.log(`[ComfyUI Debug] 图片显示成功: ${generationId}`); console.log(`[ComfyUI Debug] 图片尺寸: ${imgElement.naturalWidth}x${imgElement.naturalHeight}`); } resolve(); }; // 设置图片加载失败处理 imgElement.onerror = () => { if (loadingIndicator.parentNode) { loadingIndicator.remove(); } console.error(`[ComfyUI Error] Base64图片渲染失败: ${generationId}`); // 显示错误占位图 const errorDiv = document.createElement('div'); errorDiv.className = 'comfy-image-error'; errorDiv.style.maxWidth = `${cachedSettings.maxWidth || 600}px`; errorDiv.innerHTML = ` <i class="fa fa-exclamation-triangle"></i> <strong>图片渲染失败</strong><br> <small>图片数据可能已损坏,请重新生成</small> `; imgElement.style.display = 'none'; container.appendChild(errorDiv); reject(new ComfyUIError('图片渲染失败', 'UI')); }; // 设置图片源(base64数据) imgElement.style.maxWidth = (cachedSettings.maxWidth || 600) + 'px'; if (debugMode) { console.log(`[ComfyUI Debug] 开始渲染图片: ${generationId}`); console.log(`[ComfyUI Debug] Base64数据大小: ${(imageBase64Data.length / 1024).toFixed(2)}KB`); } imgElement.src = imageBase64Data; // 【修复】超时检查 - 使用 imgElement.complete(旧版本方式) const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const timeoutDuration = isMobile ? 90000 : 30000; // 移动端90秒,PC端30秒(更宽松) setTimeout(() => { if (!imgElement.complete) { if (loadingIndicator.parentNode) { loadingIndicator.remove(); } console.error(`[ComfyUI Error] 图片渲染超时(${timeoutDuration/1000}秒): ${generationId}`); console.error('[ComfyUI] 图片数据大小:', (imageBase64Data.length / 1024).toFixed(2), 'KB'); console.error('[ComfyUI] 设备类型:', isMobile ? '移动端' : 'PC端'); console.error('[ComfyUI] imgElement.complete:', imgElement.complete); console.error('[ComfyUI] imgElement.naturalWidth:', imgElement.naturalWidth); // 显示超时错误 - 但图片可能实际已加载,只是complete标志未设置 const errorDiv = document.createElement('div'); errorDiv.className = 'comfy-image-error'; errorDiv.style.maxWidth = `${cachedSettings.maxWidth || 600}px`; errorDiv.innerHTML = ` <i class="fa fa-clock"></i> <strong>图片渲染超时</strong><br> <small>图片已压缩至 ${(imageBase64Data.length / 1024).toFixed(0)}KB,但渲染仍然超时<br>建议在ComfyUI后端降低输出分辨率</small> `; imgElement.style.display = 'none'; container.appendChild(errorDiv); reject(new ComfyUIError('图片渲染超时', 'UI')); } }, timeoutDuration); } catch (error) { reject(error); } }); } // 显示图片,现在可以接受URL或Base64数据(用于缓存恢复) async function displayImage(anchorElement, imageData) { try { const group = anchorElement.closest('.comfy-button-group') || anchorElement; let container = group.nextElementSibling; if (!container || !container.classList.contains('comfy-image-container')) { container = document.createElement('div'); container.className = 'comfy-image-container'; const img = document.createElement('img'); img.alt = 'ComfyUI 生成的图片'; img.loading = 'lazy'; // 懒加载优化 container.appendChild(img); group.insertAdjacentElement('afterend', container); } container.style.opacity = '1'; const imgElement = container.querySelector('img'); imgElement.src = imageData; imgElement.style.maxWidth = (cachedSettings.maxWidth || 600) + 'px'; } catch (error) { ErrorHandler.handle(new ComfyUIError('显示图片失败: ' + error.message, 'UI'), 'displayImage'); } } // --- Main Execution Logic --- console.log('[ComfyUI] 插件开始加载...'); createComfyUIPanel(); const chatObserver = new MutationObserver(async (mutations) => { try { const nodesToProcess = new Set(); for (const mutation of mutations) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches('.mes')) nodesToProcess.add(node); node.querySelectorAll('.mes').forEach(mes => nodesToProcess.add(mes)); } }); if (mutation.target.nodeType === Node.ELEMENT_NODE && mutation.target.closest('.mes')) { nodesToProcess.add(mutation.target.closest('.mes')); } } if (nodesToProcess.size > 0) { // 从LRU缓存或旧存储获取缓存图片 let savedImages = {}; if (lruCache) { // 从LRU缓存获取所有数据 const items = lruCache.getAll(); items.forEach(item => { savedImages[item.id] = item.data; }); } else { savedImages = await GM_getValue(STORAGE_KEY_IMAGES, {}); } nodesToProcess.forEach(node => { try { const mesTextElement = node.querySelector('.mes_text'); if (mesTextElement && !mesTextElement.dataset.listenersAttached) { mesTextElement.addEventListener('touchstart', (event) => handleComfyButtonClick(event, true), { passive: false }); mesTextElement.addEventListener('click', (event) => handleComfyButtonClick(event, false)); mesTextElement.dataset.listenersAttached = 'true'; } processMessageForComfyButton(node, savedImages); } catch (error) { console.error('处理消息节点失败:', error); } }); } } catch (error) { ErrorHandler.handle(new ComfyUIError('聊天观察器处理失败: ' + error.message, 'UI'), 'chatObserver'); } }); async function loadSettingsFromStorageAndApplyToCache() { try { await migrateConfig(); // 执行配置迁移 cachedSettings.comfyuiUrl = await GM_getValue('comfyui_url', 'https://127.0.0.1:5001'); cachedSettings.startTag = await GM_getValue('comfyui_start_tag', 'image###'); cachedSettings.endTag = await GM_getValue('comfyui_end_tag', '###'); cachedSettings.promptPrefix = await GM_getValue(STORAGE_KEY_PROMPT_PREFIX, ''); cachedSettings.maxWidth = await GM_getValue(STORAGE_KEY_MAX_WIDTH, 600); cachedSettings.cacheLimit = await GM_getValue(STORAGE_KEY_CACHE_LIMIT, 20); document.documentElement.style.setProperty('--comfy-image-max-width', (cachedSettings.maxWidth || 600) + 'px'); } catch (error) { ErrorHandler.handle(new ComfyUIError('加载初始设置失败: ' + error.message, 'CONFIG'), 'loadSettingsFromStorageAndApplyToCache'); } } function observeChat() { const chatElement = document.getElementById('chat'); if (chatElement) { loadSettingsFromStorageAndApplyToCache().then(async () => { try { // 从LRU缓存或旧存储获取缓存图片 let initialSavedImages = {}; if (lruCache) { const items = lruCache.getAll(); items.forEach(item => { initialSavedImages[item.id] = item.data; }); } else { initialSavedImages = await GM_getValue(STORAGE_KEY_IMAGES, {}); } chatElement.querySelectorAll('.mes').forEach(node => { try { const mesTextElement = node.querySelector('.mes_text'); if (mesTextElement && !mesTextElement.dataset.listenersAttached) { mesTextElement.addEventListener('touchstart', (event) => handleComfyButtonClick(event, true), { passive: false }); mesTextElement.addEventListener('click', (event) => handleComfyButtonClick(event, false)); mesTextElement.dataset.listenersAttached = 'true'; } processMessageForComfyButton(node, initialSavedImages); } catch (error) { console.error('初始化消息节点失败:', error); } }); chatObserver.observe(chatElement, { childList: true, subtree: true }); } catch (error) { ErrorHandler.handle(new ComfyUIError('初始化聊天观察失败: ' + error.message, 'UI'), 'observeChat'); } }); } else { setTimeout(observeChat, 500); } } const optionsObserver = new MutationObserver(() => { try { const optionsMenu = document.getElementById('options'); if (optionsMenu && optionsMenu.style.display !== 'none') { addMainButton(); } } catch (error) { console.error('选项观察器错误:', error); } }); // 初始化重连器 function initializeReconnector() { reconnector = new SmartReconnector( () => { if (cachedSettings.comfyuiUrl) { const schedulerUrl = new URL(cachedSettings.comfyuiUrl); return `${schedulerUrl.protocol}//${schedulerUrl.host}`; } return null; }, connectWebSocket, () => { if (socket) { socket.disconnect(); socket = null; } } ); } // 页面卸载时清理资源 window.addEventListener('beforeunload', () => { try { performanceMonitor.stopMemoryMonitoring(); if (socket) { socket.disconnect(); } clearDOMCache(); } catch (error) { console.error('清理资源失败:', error); } }); window.addEventListener('load', () => { try { loadSettingsFromStorageAndApplyToCache().then(async () => { initializeReconnector(); if (cachedSettings.comfyuiUrl && validateUrl(cachedSettings.comfyuiUrl)) { connectWebSocket(); // 页面加载后立即尝试连接 } // 启动性能监控 performanceMonitor.startMemoryMonitoring(); // 定期验证缓存完整性 setInterval(async () => { try { await validateCacheIntegrity(); } catch (error) { console.error('定期缓存验证失败:', error); } }, 300000); // 每5分钟验证一次 // 【优化】初始化IndexedDB + LRU缓存 console.log('[ComfyUI] 正在初始化缓存系统...'); try { // 创建IndexedDB实例 const indexedDBCache = new IndexedDBCache(); await indexedDBCache.init(); // 创建LRU缓存并关联IndexedDB lruCache = new LRUCache(cachedSettings.cacheLimit || 20, true); lruCache.indexedDB = indexedDBCache; // 从IndexedDB加载缓存 await lruCache.load(); const stats = lruCache.getStats(); const totalSize = await indexedDBCache.getTotalSize(); const totalSizeMB = (totalSize * 0.75 / 1024 / 1024).toFixed(1); console.log('[ComfyUI] IndexedDB + LRU缓存已初始化'); console.log(`[ComfyUI] 缓存统计: ${stats.count}张图片, 总大小约${totalSizeMB}MB`); // 数据迁移:从GM_getValue迁移到IndexedDB const oldRecords = await GM_getValue(STORAGE_KEY_IMAGES, {}); const oldKeys = Object.keys(oldRecords); if (oldKeys.length > 0 && stats.count === 0) { console.log(`[ComfyUI] 检测到${oldKeys.length}条旧缓存,开始迁移到IndexedDB...`); let migratedCount = 0; for (const [id, data] of Object.entries(oldRecords)) { try { await indexedDBCache.set(id, data, { timestamp: Date.now(), accessCount: 0 }); migratedCount++; } catch (error) { console.error(`[ComfyUI] 迁移失败: ${id}`, error); } } console.log(`[ComfyUI] 数据迁移完成: ${migratedCount}/${oldKeys.length}条`); // 重新加载缓存 await lruCache.load(); // 清空旧的GM_setValue缓存(可选) // await GM_setValue(STORAGE_KEY_IMAGES, {}); } } catch (error) { console.error('[ComfyUI] IndexedDB初始化失败,回退到GM_getValue:', error); // 回退方案:使用GM_getValue lruCache = new LRUCache(cachedSettings.cacheLimit || 20, false); await lruCache.load(); const stats = lruCache.getStats(); console.log('[ComfyUI] LRU缓存已初始化(GM_getValue模式):', stats); } // 初始化Lightbox(图片放大查看器) console.log('[ComfyUI] 正在初始化Lightbox...'); createLightbox(); // 初始化历史记录面板 console.log('[ComfyUI] 正在初始化历史记录面板...'); createHistoryPanel(); console.log('[ComfyUI] 所有组件初始化完成!'); }).catch(error => { ErrorHandler.handle(new ComfyUIError('初始化失败: ' + error.message, 'CONFIG'), 'window.load'); }); observeChat(); const body = document.querySelector('body'); if (body) { optionsObserver.observe(body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] }); } } catch (error) { ErrorHandler.handle(new ComfyUIError('页面加载初始化失败: ' + error.message, 'CONFIG'), 'window.load'); } }); })();