酒馆ComfyUI插图脚本

用于酒馆SillyTavern的ai插图脚本,使用酒馆世界书生成的prompt通过ComfyUI API生图

目前为 2025-05-12 提交的版本。查看 最新版本

// ==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或关注我们的公众号极客氢云获取最新地址