您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
用于酒馆SillyTavern的ai插图脚本,使用酒馆世界书生成的prompt通过ComfyUI API生图
当前为
// ==UserScript== // @name 酒馆ComfyUI插图脚本 // @namespace http://tampermonkey.net/ // @version 1 // @license GPL // @description 用于酒馆SillyTavern的ai插图脚本,使用酒馆世界书生成的prompt通过ComfyUI API生图 // @author soulostar // @match *://*/* // @require https://code.jquery.com/jquery-3.4.1.min.js // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== (function() { 'use strict'; let ster = null; // UI injection timer let scanSter = null; // Button scanning timer let comfyuiPanel = null; // Settings panel DOM element let panelListenersAttached = false; // Settings panel event listener flag let isGenerating = {}; // Generation lock per prompt const CACHE_PREFIX = 'comfyui_image_cache_'; // GM storage key prefix // --- Default Settings --- const defaultSettings = { url: 'http://127.0.0.1:8188', workflow: `确保工作流包含 '%prompt%' 和可选的 '%seed%' 占位符", 示例: { "inputs": { "text": "%prompt%", "clip": [ "6", 1 ] }, }`, // Example now includes %seed% startTag: 'image###', endTag: '###', targetSelector: '.mes_text', imageMaxWidth: '300px', autoGenerate: false // Auto-generate toggle }; // --- Load Settings --- let comfyuiSettings = {}; for (const key in defaultSettings) { comfyuiSettings[key] = GM_getValue(`comfyui_${key}`, defaultSettings[key]); } // Ensure boolean type for autoGenerate if (typeof comfyuiSettings.autoGenerate !== 'boolean') { comfyuiSettings.autoGenerate = defaultSettings.autoGenerate; GM_setValue('comfyui_autoGenerate', comfyuiSettings.autoGenerate); } // Ensure workflow is string if (typeof comfyuiSettings.workflow !== 'string') { comfyuiSettings.workflow = defaultSettings.workflow; GM_setValue('comfyui_workflow', comfyuiSettings.workflow); } // Ensure imageMaxWidth is loaded correctly if (typeof comfyuiSettings.imageMaxWidth !== 'string' || !comfyuiSettings.imageMaxWidth) { comfyuiSettings.imageMaxWidth = defaultSettings.imageMaxWidth; GM_setValue('comfyui_imageMaxWidth', comfyuiSettings.imageMaxWidth); } // --- Helper Functions (escapeRegExp, generateUniqueId, getCacheKey, blobToDataURL) --- function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function generateUniqueId(prefix = 'id') { return `${prefix}-${Math.random().toString(36).substr(2, 9)}`; } function getCacheKey(prompt) { const maxLength = 100; const truncatedPrompt = prompt.length > maxLength ? prompt.substring(0, maxLength) : prompt; const safePromptPart = truncatedPrompt.replace(/[^a-zA-Z0-9]/g, '_'); return `${CACHE_PREFIX}${safePromptPart}`; } function blobToDataURL(blob) { /* ... (implementation remains the same) ... */ return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = (error) => reject(error); reader.readAsDataURL(blob); }); } // --- Helper: Generate Random Seed --- function generateRandomSeed() { // Generate a large random integer (adjust range as needed for ComfyUI) return Math.floor(Math.random() * 0xFFFFFFFFFFFFF); // Max safe integer is not needed here } // --- Create Settings Panel (Add Auto Generate Toggle) --- function createComfyUIPanel() { if (document.getElementById('comfyui-settings-panel')) return document.getElementById('comfyui-settings-panel'); console.log("创建 ComfyUI 设置面板..."); const panel = document.createElement('div'); panel.id = 'comfyui-settings-panel'; // --- Styles (remain mostly the same) --- panel.style.position = 'fixed'; // **Mobile Adjustments:** // Start closer to the top instead of exact vertical center panel.style.top = '20px'; panel.style.left = '50%'; // Only translate horizontally to center it panel.style.transform = 'translateX(-50%)'; // Adjust width clamp for smaller screens (e.g., min 300px, use 90% of viewport width) panel.style.width = 'clamp(300px, 90vw, 750px)'; // Ensure max height leaves some space, combined with top: 20px panel.style.maxHeight = 'calc(100vh - 40px)'; // 20px top margin + 20px bottom margin panel.style.overflowY = 'auto'; // Allow scrolling if content overflows // --- Standard Styles (mostly unchanged) --- panel.style.backgroundColor = '#333'; panel.style.color = 'white'; panel.style.padding = '20px'; // Slightly reduced padding for smaller screens panel.style.border = '1px solid #555'; panel.style.borderRadius = '8px'; panel.style.boxShadow = '0 5px 15px rgba(0,0,0,0.5)'; panel.style.zIndex = '10001'; // Ensure it's on top panel.style.display = 'none'; // Initially hidden panel.style.fontFamily = 'sans-serif'; panel.style.boxSizing = 'border-box'; panel.innerHTML = ` <h3 style="text-align: center; margin-top: 0; margin-bottom: 20px; border-bottom: 1px solid #555; padding-bottom: 10px;">ComfyUI 设置</h3> <div style="margin-bottom: 15px;"> <label for="comfyui-url-input" style="display: block; margin-bottom: 5px; font-weight: bold;">ComfyUI URL:</label> <div style="display: flex; gap: 10px; align-items: center;"> <input type="text" id="comfyui-url-input" style="flex-grow: 1; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white;"> <button id="test-comfyui-connection" style="padding: 8px 12px; border: none; border-radius: 4px; background-color: #555; color: white; cursor: pointer; transition: background-color 0.3s; white-space: nowrap;">测试连接</button> </div> <div id="connection-status" style="font-size: 0.8em; margin-top: 5px; height: 1.2em;"></div> </div> <div style="margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 15px;"> <div style="flex: 1 1 150px;"> <label for="comfyui-starttag-input" style="display: block; margin-bottom: 5px; font-weight: bold;">开始标记:</label> <input type="text" id="comfyui-starttag-input" style="width: 95%; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white;"> </div> <div style="flex: 1 1 150px;"> <label for="comfyui-endtag-input" style="display: block; margin-bottom: 5px; font-weight: bold;">结束标记:</label> <input type="text" id="comfyui-endtag-input" style="width: 95%; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white;"> </div> <div style="flex: 1 1 150px;"> <label for="comfyui-imageMaxWidth-input" style="display: block; margin-bottom: 5px; font-weight: bold;">图片最大宽度:</label> <input type="text" id="comfyui-imageMaxWidth-input" style="width: 95%; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white;" placeholder="例如: 300px, 50%"> </div> </div> <div style="margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 15px; align-items: center;"> <div style="flex: 2 1 300px;"> <label for="comfyui-targetselector-input" style="display: block; margin-bottom: 5px; font-weight: bold;">扫描目标选择器:</label> <input type="text" id="comfyui-targetselector-input" style="width: 98%; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white;" placeholder="例如: .chat-message, .mes_text"> </div> <div style="flex: 1 1 150px; padding-top: 20px;"> <label style="display: flex; align-items: center; cursor: pointer;"> <input type="checkbox" id="comfyui-autogenerate-toggle" style="margin-right: 8px; transform: scale(1.2);"> <span style="font-weight: bold;">自动生成图片</span> </label> </div> </div> <div style="margin-bottom: 20px;"> <label for="comfyui-workflow-input" style="display: block; margin-bottom: 5px; font-weight: bold;">工作流 (JSON - 支持 "%prompt%" 和 "%seed%" 占位符):</label> <textarea id="comfyui-workflow-input" style="width: 98%; height: 250px; padding: 8px; border-radius: 4px; border: 1px solid #666; background-color: #444; color: white; resize: vertical; font-family: monospace;"></textarea> </div> <div style="margin-top: 20px; border-top: 1px solid #555; padding-top: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px;"> <button id="clear-comfyui-cache" style="padding: 10px 15px; border: none; border-radius: 4px; background-color: #ff9800; color: white; cursor: pointer; font-weight: bold;">清除图片缓存</button> <div> <button id="save-comfyui-settings" style="padding: 10px 15px; border: none; border-radius: 4px; background-color: #4CAF50; color: white; cursor: pointer; font-weight: bold; margin-left: 10px;">保存设置</button> <button id="close-comfyui-panel" style="padding: 10px 15px; border: none; border-radius: 4px; background-color: #f44336; color: white; cursor: pointer; font-weight: bold; margin-left: 10px;">关闭</button> </div> </div> `; document.body.appendChild(panel); comfyuiPanel = panel; if (!panelListenersAttached) { console.log("为面板按钮附加事件监听器..."); try { document.getElementById('test-comfyui-connection').addEventListener('click', testComfyuiConnection); document.getElementById('save-comfyui-settings').addEventListener('click', saveComfyUISettings); document.getElementById('close-comfyui-panel').addEventListener('click', hideComfyUIPanel); document.getElementById('clear-comfyui-cache').addEventListener('click', clearComfyUIImageCache); panelListenersAttached = true; } catch (error) { console.error("添加面板监听器时出错:", error); } } return panel; } // --- Show Settings Panel (Load Auto Generate State) --- function showComfyUIPanel() { const panel = comfyuiPanel || createComfyUIPanel(); if (!panel) { console.error("无法创建或找到设置面板。"); return; } try { document.getElementById('comfyui-url-input').value = comfyuiSettings.url; document.getElementById('comfyui-workflow-input').value = comfyuiSettings.workflow; document.getElementById('comfyui-starttag-input').value = comfyuiSettings.startTag; document.getElementById('comfyui-endtag-input').value = comfyuiSettings.endTag; document.getElementById('comfyui-targetselector-input').value = comfyuiSettings.targetSelector; document.getElementById('comfyui-imageMaxWidth-input').value = comfyuiSettings.imageMaxWidth; document.getElementById('comfyui-autogenerate-toggle').checked = comfyuiSettings.autoGenerate; // Set checkbox state const testButton = document.getElementById('test-comfyui-connection'); const statusDiv = document.getElementById('connection-status'); if (testButton) testButton.style.backgroundColor = '#555'; if (statusDiv) statusDiv.textContent = ''; panel.style.display = 'block'; console.log("ComfyUI 设置面板已显示。"); } catch (error) { console.error("填充设置面板时出错:", error); alert("无法加载设置面板,请检查控制台。"); } } // --- Hide Settings Panel (Remains the same) --- function hideComfyUIPanel() { /* ... */ if (comfyuiPanel) { comfyuiPanel.style.display = 'none'; console.log("ComfyUI 设置面板已隐藏。"); } } // --- Test Connection (Remains the same) --- function testComfyuiConnection() { /* ... */ const urlInput = document.getElementById('comfyui-url-input'); const testButton = document.getElementById('test-comfyui-connection'); const statusDiv = document.getElementById('connection-status'); let url = urlInput.value.trim(); if (!url) { statusDiv.textContent = '请输入URL'; statusDiv.style.color = 'orange'; return; } if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'http://' + url; } if (url.endsWith('/')) { url = url.slice(0, -1); } const testUrl = url + '/system_stats'; console.log(`尝试连接到: ${testUrl}`); statusDiv.textContent = '正在连接...'; statusDiv.style.color = 'white'; testButton.style.backgroundColor = '#555'; testButton.disabled = true; GM_xmlhttpRequest({ method: "GET", url: testUrl, timeout: 5000, onload: (res) => { /* ... */ testButton.disabled = false; if (res.status === 200) { testButton.style.backgroundColor = 'green'; statusDiv.textContent = '连接成功!'; statusDiv.style.color = 'lightgreen'; } else { testButton.style.backgroundColor = 'orange'; statusDiv.textContent = `连接失败 (状态: ${res.status})`; statusDiv.style.color = 'orange'; } }, onerror: (err) => { /* ... */ testButton.disabled = false; testButton.style.backgroundColor = 'orange'; statusDiv.textContent = '连接错误 (请检查URL或网络)'; statusDiv.style.color = 'orange'; }, ontimeout: () => { /* ... */ testButton.disabled = false; testButton.style.backgroundColor = 'orange'; statusDiv.textContent = '连接超时'; statusDiv.style.color = 'orange'; } }); } // --- Save Settings (Add Auto Generate) --- function saveComfyUISettings() { const urlInput = document.getElementById('comfyui-url-input'); const workflowInput = document.getElementById('comfyui-workflow-input'); const startTagInput = document.getElementById('comfyui-starttag-input'); const endTagInput = document.getElementById('comfyui-endtag-input'); const targetSelectorInput = document.getElementById('comfyui-targetselector-input'); const imageMaxWidthInput = document.getElementById('comfyui-imageMaxWidth-input'); const autoGenerateToggle = document.getElementById('comfyui-autogenerate-toggle'); // Get toggle element const newUrl = urlInput.value.trim(); const newWorkflow = workflowInput.value; const newStartTag = startTagInput.value.trim(); const newEndTag = endTagInput.value.trim(); const newTargetSelector = targetSelectorInput.value.trim(); const newImageMaxWidth = imageMaxWidthInput.value.trim(); const newAutoGenerate = autoGenerateToggle.checked; // Get toggle state if (!newUrl || !newStartTag || !newEndTag || !newTargetSelector || !newImageMaxWidth) { alert("保存失败: URL, 开始/结束标记, 目标选择器, 和图片最大宽度不能为空。"); return; } try { // Basic JSON validation (doesn't guarantee workflow validity) JSON.parse(newWorkflow); comfyuiSettings.url = newUrl; comfyuiSettings.workflow = newWorkflow; comfyuiSettings.startTag = newStartTag; comfyuiSettings.endTag = newEndTag; comfyuiSettings.targetSelector = newTargetSelector; comfyuiSettings.imageMaxWidth = newImageMaxWidth; comfyuiSettings.autoGenerate = newAutoGenerate; // Save new setting GM_setValue('comfyui_url', newUrl); GM_setValue('comfyui_workflow', newWorkflow); GM_setValue('comfyui_startTag', newStartTag); GM_setValue('comfyui_endTag', newEndTag); GM_setValue('comfyui_targetSelector', newTargetSelector); GM_setValue('comfyui_imageMaxWidth', newImageMaxWidth); GM_setValue('comfyui_autoGenerate', newAutoGenerate); // Store new setting console.log("ComfyUI 设置已保存:", comfyuiSettings); alert("设置已保存!"); hideComfyUIPanel(); updateImageStyles(); startScanning(); } catch (e) { alert(`保存失败: 工作流 JSON 格式无效。\n错误: ${e.message}`); console.error("无效的 JSON 工作流:", e); } } // --- Clear Image Cache (Remains the same) --- function clearComfyUIImageCache() { /* ... */ if (!confirm("确定要清除所有缓存的 ComfyUI 图片吗?此操作无法撤销。")) { return; } let clearedCount = 0; const keys = GM_listValues(); keys.forEach(key => { if (key.startsWith(CACHE_PREFIX)) { GM_deleteValue(key); clearedCount++; } }); alert(`已清除 ${clearedCount} 张缓存图片。`); console.log(`Cleared ${clearedCount} cached images.`); scanAndInjectButtons(); } // --- Toggle Image Visibility (Remains the same) --- function toggleImageVisibility(buttonElement) { /* ... */ const imageSpanId = buttonElement.dataset.imageSpanId; const imageSpan = document.getElementById(imageSpanId); if (imageSpan) { const img = imageSpan.querySelector('img'); if (img) { if (img.style.display === 'none') { img.style.display = 'block'; buttonElement.textContent = '隐藏'; } else { img.style.display = 'none'; buttonElement.textContent = '显示'; } } } } // --- Delete Image and Cache (Update button states) --- function deleteImageAndCache(buttonElement) { const prompt = buttonElement.dataset.prompt; // Get prompt from button if (!prompt) { console.warn("Delete button missing prompt data."); return; // Should not happen if created correctly } const cacheKey = getCacheKey(prompt); const imageSpanId = buttonElement.dataset.imageSpanId; const imageSpan = document.getElementById(imageSpanId); const buttonContainer = buttonElement.parentElement; if (confirm(`确定要删除提示为 "${prompt.substring(0, 30)}..." 的图片及其缓存吗?`)) { if (imageSpan) { imageSpan.innerHTML = ''; } // Clear image display GM_deleteValue(cacheKey); // Delete from storage console.log(`Image cache deleted for key: ${cacheKey}`); // Update main button state const mainButton = buttonContainer.querySelector('button[id^="comfy-button-generate-"]'); if (mainButton) { mainButton.textContent = '生成图片'; mainButton.disabled = false; // Ensure it's enabled } // Disable Hide and Delete buttons as there's nothing to hide/delete const hideButton = buttonContainer.querySelector('button[id^="comfy-button-hide-"]'); const deleteBtn = buttonContainer.querySelector('button[id^="comfy-button-delete-"]'); // Use different name if (hideButton) { hideButton.disabled = true; hideButton.textContent = '隐藏'; // Reset text } if (deleteBtn) { deleteBtn.disabled = true; } } } // --- ComfyUI Image Generation (Replace Seed, Enable controls) --- async function generateComfyUIImage(buttonElement) { const prompt = buttonElement.dataset.prompt; const cacheKey = getCacheKey(prompt); if (isGenerating[cacheKey]) { alert("当前正在为该提示生成图片,请稍候..."); return; } isGenerating[cacheKey] = true; buttonElement.textContent = '生成中...'; buttonElement.disabled = true; const buttonContainer = buttonElement.parentElement; const hideButton = buttonContainer.querySelector('button[id^="comfy-button-hide-"]'); const deleteButton = buttonContainer.querySelector('button[id^="comfy-button-delete-"]'); if(hideButton) hideButton.disabled = true; if(deleteButton) deleteButton.disabled = true; const imageSpanId = buttonElement.dataset.imageSpanId; const imageSpan = document.getElementById(imageSpanId); if (imageSpan) imageSpan.innerHTML = '<i>正在加载...</i>'; let workflowString = comfyuiSettings.workflow; // Start with the template string const url = comfyuiSettings.url.endsWith('/') ? comfyuiSettings.url.slice(0, -1) : comfyuiSettings.url; // 1. Replace Prompt Placeholder const escapedPrompt = JSON.stringify(prompt).slice(1, -1); // Escape prompt for JSON string workflowString = workflowString.replace(/"%prompt%"/g, `"${escapedPrompt}"`); // 2. Replace Seed Placeholder (if exists) if (workflowString.includes('"%seed%"')) { const randomSeed = generateRandomSeed(); console.log(`Using random seed: ${randomSeed}`); // Replace "%seed%" (including quotes) with the number workflowString = workflowString.replace(/"%seed%"/g, randomSeed.toString()); } else { console.log("Workflow does not contain '%seed%', using workflow's default seed."); } const clientId = generateUniqueId('client'); let payload; try { // 3. Parse the modified workflow string payload = { client_id: clientId, prompt: JSON.parse(workflowString) }; } catch (e) { console.error("解析工作流 JSON 失败:", e, workflowString); // Log the processed string alert(`工作流 JSON 格式错误 (替换占位符后): ${e.message}`); buttonElement.textContent = '生成图片'; buttonElement.disabled = false; if(hideButton) hideButton.disabled = false; if(deleteButton) deleteButton.disabled = false; if (imageSpan) imageSpan.innerHTML = '<i style="color:red;">工作流错误</i>'; delete isGenerating[cacheKey]; return; } console.log("发送到 ComfyUI 的 Payload:", JSON.stringify(payload, null, 2)); try { // --- Steps 4-7: /prompt request, /history polling, extract info, get blob (remain the same) --- const response = await new Promise((resolve, reject) => { /* /prompt request */ GM_xmlhttpRequest({ method: "POST", url: `${url}/prompt`, headers: { "Content-Type": "application/json" }, data: JSON.stringify(payload), timeout: 10000, onload: (res) => res.status >= 200 && res.status < 300 ? resolve(JSON.parse(res.responseText)) : reject(new Error(`/prompt 请求失败: ${res.status} ${res.statusText} - ${res.responseText}`)), onerror: (err) => reject(new Error(`网络错误: ${err}`)), ontimeout: () => reject(new Error("/prompt 超时")) }); }); const promptId = response.prompt_id; if (!promptId) throw new Error("未能获取 prompt_id"); buttonElement.textContent = '等待生成...'; let historyData = null, attempts = 0, maxAttempts = 60; while (attempts < maxAttempts) { /* /history polling */ await new Promise(resolve => setTimeout(resolve, 2000)); attempts++; buttonElement.textContent = `等待生成... (${attempts})`; try { const historyResponse = await new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `${url}/history/${promptId}`, timeout: 5000, onload: (res) => res.status === 200 ? resolve(JSON.parse(res.responseText)) : resolve(null), onerror: (err) => { console.error(`/history error:`, err); resolve(null); }, ontimeout: () => { console.warn(`/history timeout`); resolve(null); } }); }); if (historyResponse && historyResponse[promptId]) { historyData = historyResponse[promptId]; break; } } catch (e) { console.warn("Polling /history error:", e); } } if (!historyData) throw new Error(`未能获取生成结果 (prompt_id: ${promptId})`); const outputs = historyData.outputs; let imageFilename = null, imageType = 'output', imageSubfolder = ''; for (const nodeId in outputs) { /* Find image output */ if (outputs[nodeId].images && outputs[nodeId].images.length > 0) { const imgInfo = outputs[nodeId].images[0]; imageFilename = imgInfo.filename; imageType = imgInfo.type || 'output'; imageSubfolder = imgInfo.subfolder || ''; break; } } if (!imageFilename) throw new Error("未能找到图片文件名"); buttonElement.textContent = '加载图片...'; const imageUrl = `${url}/view?filename=${encodeURIComponent(imageFilename)}&type=${imageType}&subfolder=${encodeURIComponent(imageSubfolder)}`; const imageBlob = await new Promise((resolve, reject) => { /* Fetch image blob */ GM_xmlhttpRequest({ method: "GET", url: imageUrl, responseType: 'blob', timeout: 15000, onload: (res) => res.status === 200 ? resolve(res.response) : reject(new Error(`无法加载图片: ${res.status}`)), onerror: (err) => reject(new Error(`加载图片网络错误: ${err}`)), ontimeout: () => reject(new Error("加载图片超时")) }); }); // --- Step 8: Convert and Cache --- const imageDataUrl = await blobToDataURL(imageBlob); GM_setValue(cacheKey, imageDataUrl); console.log(`图片已缓存 (Key: ${cacheKey})`); // --- Step 9: Display Image and Enable Controls --- if (imageSpan) { const imgElement = document.createElement('img'); imgElement.src = imageDataUrl; imageSpan.innerHTML = ''; imageSpan.appendChild(imgElement); } buttonElement.textContent = '重新生成'; if (hideButton) { hideButton.disabled = false; hideButton.textContent = '隐藏'; } if (deleteButton) { deleteButton.disabled = false; } } catch (error) { /* ... (error handling remains the same) ... */ console.error("ComfyUI 图片生成失败:", error); alert(`图片生成失败: ${error.message}`); buttonElement.textContent = '生成失败'; if (imageSpan) imageSpan.innerHTML = `<i style="color:red;">生成失败</i>`; setTimeout(() => { buttonElement.textContent = '生成图片'; buttonElement.disabled = false; if(hideButton) hideButton.disabled = false; if(deleteButton) deleteButton.disabled = false; }, 3000); } finally { buttonElement.disabled = false; if(hideButton) hideButton.disabled = false; if(deleteButton) deleteButton.disabled = false; delete isGenerating[cacheKey]; } } // --- Create Control Buttons (Remains the same) --- function createControlButton(type, prompt, imageSpanId) { /* ... */ const button = document.createElement('button'); button.id = generateUniqueId(`comfy-button-${type}-`); button.classList.add('button_image', `button_${type}`); button.dataset.prompt = prompt; // Add prompt data to delete button button.dataset.imageSpanId = imageSpanId; button.style.marginLeft = '5px'; button.style.fontSize = '10px'; button.style.padding = '2px 4px'; if (type === 'hide') { button.textContent = '隐藏'; // Event listener is added in scanAndInjectButtons } else if (type === 'delete') { button.textContent = '删除'; button.style.backgroundColor = '#f44336'; // Event listener is added in scanAndInjectButtons } return button; } // --- Scan and Inject Buttons (Always add controls, check auto-gen) --- function scanAndInjectButtons() { const { startTag, endTag, targetSelector, autoGenerate } = comfyuiSettings; // Get autoGenerate setting if (!startTag || !endTag || !targetSelector) { return; } const regex = new RegExp(escapeRegExp(startTag) + '([\\s\\S]*?)' + escapeRegExp(endTag), 'g'); // Select elements that haven't been processed OR elements that were processed but might need updates (e.g., if content changed) // A more robust approach might involve checking content hash, but this is simpler. const targetElements = document.querySelectorAll(`${targetSelector}`); // Scan all target elements targetElements.forEach(element => { // Only process if content seems to contain tags and hasn't been fully processed with buttons const needsProcessing = element.textContent.includes(startTag) && !element.querySelector('.comfy-button-container'); if (!needsProcessing && element.dataset.comfyuiProcessed === 'true') { // Already processed and no tags found, skip. return; } // Reset processed flag if we are reprocessing element.dataset.comfyuiProcessed = 'false'; const originalHTML = element.innerHTML; let newHTML = ''; let lastIndex = 0; let hasMatches = false; let autoGeneratedPrompts = []; // Track prompts to auto-generate // Use a temporary div to parse and manipulate nodes safely const tempDiv = document.createElement('div'); tempDiv.innerHTML = originalHTML; // Find text nodes containing the start tag const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT); let node; const nodesToReplace = []; while(node = walker.nextNode()) { let nodeContent = node.nodeValue; let match; let nodeLastIndex = 0; const replacements = []; // Replacements for this specific text node regex.lastIndex = 0; // Reset regex index for each text node while ((match = regex.exec(nodeContent)) !== null) { hasMatches = true; const promptText = match[1].trim(); const fullMatchText = match[0]; const cacheKey = getCacheKey(promptText); const cachedImage = GM_getValue(cacheKey, null); const hasCache = !!cachedImage; // Part before the match replacements.push(document.createTextNode(nodeContent.substring(nodeLastIndex, match.index))); // --- Create Buttons and Image Span --- const generateButtonId = generateUniqueId('comfy-button-generate-'); const hideButtonId = generateUniqueId('comfy-button-hide-'); const deleteButtonId = generateUniqueId('comfy-button-delete-'); const spanId = generateUniqueId('comfy-image-span'); const buttonContainer = document.createElement('span'); buttonContainer.classList.add('comfy-button-container'); buttonContainer.style.display = 'inline-block'; // Use inline-block const generateButton = document.createElement('button'); generateButton.id = generateButtonId; generateButton.classList.add('button_image'); generateButton.textContent = hasCache ? '重新生成' : '生成图片'; generateButton.dataset.prompt = promptText; generateButton.dataset.imageSpanId = spanId; generateButton.style.margin = '0 3px 0 0'; buttonContainer.appendChild(generateButton); const hideButton = createControlButton('hide', promptText, spanId); hideButton.id = hideButtonId; // Assign ID here hideButton.disabled = !hasCache; buttonContainer.appendChild(hideButton); const deleteButton = createControlButton('delete', promptText, spanId); deleteButton.id = deleteButtonId; // Assign ID here deleteButton.disabled = !hasCache; buttonContainer.appendChild(deleteButton); const imageSpan = document.createElement('span'); imageSpan.id = spanId; imageSpan.classList.add('comfy-image-container'); imageSpan.style.display = 'block'; imageSpan.style.marginTop = '5px'; if (hasCache) { const imgElement = document.createElement('img'); imgElement.src = cachedImage; imageSpan.appendChild(imgElement); } else if (autoGenerate && !isGenerating[cacheKey]) { autoGeneratedPrompts.push(generateButtonId); } // Add the button container and image span to replacements replacements.push(buttonContainer); replacements.push(imageSpan); nodeLastIndex = regex.lastIndex; } // Add the remaining part of the text node replacements.push(document.createTextNode(nodeContent.substring(nodeLastIndex))); // Store the original node and its replacements if(replacements.length > 1 || hasMatches) { // Only if actual replacements happened in this node nodesToReplace.push({ original: node, replacements: replacements }); } } // End of TreeWalker loop // Perform replacements after iterating if (nodesToReplace.length > 0) { nodesToReplace.forEach(item => { item.replacements.forEach(newNode => { item.original.parentNode.insertBefore(newNode, item.original); }); item.original.parentNode.removeChild(item.original); }); // Update the element's content with the modified HTML from tempDiv element.innerHTML = tempDiv.innerHTML; element.dataset.comfyuiProcessed = 'true'; // Mark as processed // Re-bind events specifically for the buttons within this element element.querySelectorAll('button[id^="comfy-button-generate-"]').forEach(btn => { btn.removeEventListener('click', handleGenerateButtonClick); btn.addEventListener('click', handleGenerateButtonClick); }); element.querySelectorAll('button[id^="comfy-button-hide-"]').forEach(btn => { btn.removeEventListener('click', handleHideButtonClick); btn.addEventListener('click', handleHideButtonClick); }); element.querySelectorAll('button[id^="comfy-button-delete-"]').forEach(btn => { btn.removeEventListener('click', handleDeleteButtonClick); btn.addEventListener('click', handleDeleteButtonClick); }); // Trigger auto-generation if (autoGeneratedPrompts.length > 0) { setTimeout(() => { autoGeneratedPrompts.forEach(buttonId => { const btnToClick = document.getElementById(buttonId); // Find by ID now if (btnToClick && !isGenerating[getCacheKey(btnToClick.dataset.prompt)]) { console.log(`Auto-generating for prompt: ${btnToClick.dataset.prompt}`); handleGenerateButtonClick({ target: btnToClick }); } }); }, 100); } } else if (!element.dataset.comfyuiProcessed) { // If no matches were found in this scan and it wasn't previously marked, mark it now // to avoid rescanning unchanged text. element.dataset.comfyuiProcessed = 'true'; } }); // End of targetElements.forEach } // --- Event Handlers --- function handleGenerateButtonClick(event) { generateComfyUIImage(event.target); } function handleHideButtonClick(event) { toggleImageVisibility(event.target); } function handleDeleteButtonClick(event) { deleteImageAndCache(event.target); } // --- Start/Restart Scanning --- function startScanning() { if (scanSter) { clearInterval(scanSter); } scanAndInjectButtons(); // Run once immediately scanSter = setInterval(scanAndInjectButtons, 3000); // Then scan periodically } // --- Inject Connect Button --- function addConnectButton() { /* ... (remains the same) ... */ const targetAnchorSelector = '#option_toggle_AN'; const buttonId = 'connect_comfyui_settings_button'; const targetElement = document.querySelector(targetAnchorSelector); if (targetElement && !document.getElementById(buttonId)) { if (ster) { clearInterval(ster); ster = null; } const newElement = document.createElement('a'); newElement.id = buttonId; newElement.href = '#'; newElement.classList.add('button_image'); newElement.style.textDecoration = 'none'; newElement.style.marginLeft = '10px'; newElement.style.padding = '5px 8px'; newElement.appendChild(document.createTextNode('连接到ComfyUI')); targetElement.parentNode.insertBefore(newElement, targetElement.nextSibling); newElement.addEventListener('click', (event) => { event.preventDefault(); showComfyUIPanel(); }); } else if (targetElement) { if (ster) { clearInterval(ster); ster = null; } } } // --- Update Image Styles --- function updateImageStyles() { /* ... (remains the same) ... */ const styleId = 'comfyui-image-dynamic-style'; let styleElement = document.getElementById(styleId); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = styleId; document.head.appendChild(styleElement); } styleElement.textContent = ` .comfy-image-container img { max-width: ${comfyuiSettings.imageMaxWidth || '300px'} !important; height: auto !important; border: 1px solid #555; border-radius: 4px; background-color: #222; display: block; margin-top: 5px; } `; console.log("Image styles updated with max-width:", comfyuiSettings.imageMaxWidth); } // --- Script Initialization --- $(document).ready(function() { console.log("ComfyUI 图文生成脚本 (带缓存/控件/自动生成/随机种子) 启动..."); // 1. Inject Connect Button if (!ster) { addConnectButton(); ster = setInterval(addConnectButton, 2000); } // 2. Start Scanning startScanning(); // 3. Add Styles const baseStyle = document.createElement('style'); baseStyle.textContent = ` .button_image { /* Main and connect buttons */ padding: 3px 6px; font-size: 12px; font-weight: 600; color: #ffffff; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); border: none; border-radius: 4px; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(99, 102, 241, 0.2); display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; outline: none; -webkit-appearance: none; -moz-appearance: none; text-decoration: none; vertical-align: middle; margin: 0 1px; } .button_image:hover:not(:disabled) { opacity: 0.85; box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3); } .button_image:disabled { cursor: not-allowed; opacity: 0.5; background: #777 !important; box-shadow: none !important; } /* More prominent disabled style */ .button_hide, .button_delete { /* Control button specifics */ font-size: 10px !important; padding: 2px 5px !important; margin-left: 4px !important; } .button_delete { background: #f44336 !important; } .button_delete:hover:not(:disabled) { background: #d32f2f !important; } /* .button_hide:disabled, .button_delete:disabled { background: #777 !important; } /* Disabled style for controls */ #comfyui-settings-panel button { transition: background-color 0.3s, transform 0.1s; } #comfyui-settings-panel button:hover { opacity: 0.9; } #comfyui-settings-panel button:active { transform: scale(0.98); } #test-comfyui-connection:disabled { cursor: not-allowed; opacity: 0.7; } .comfy-image-container { /* Image container */ display: block; margin-top: 5px; position: relative; } /* Image styles are now in updateImageStyles */ .comfy-image-container i { color: #aaa; font-size: 0.9em; } /* Loading/error text */ .comfy-button-container { /* Button container */ display: inline-flex; align-items: center; gap: 3px; vertical-align: middle; } `; document.head.appendChild(baseStyle); updateImageStyles(); // Apply initial image width }); })(); // End IIFE
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址