酒馆ComfyUI插图脚本

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

当前为 2025-05-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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