Perchance Image Replacer (Vietnamese Version) - Multi-Key

Tìm prompt, hiển thị nút "Tạo", tạo ảnh, có nút "Tạo lại", cache IndexedDB, tự động lấy và quản lý nhiều userKey.

La data de 09-05-2025. Vezi ultima versiune.

// ==UserScript==
// @name         Perchance Image Replacer (Vietnamese Version) - Multi-Key
// @namespace    http://tampermonkey.net/
// @version      0.9.7
// @description  Tìm prompt, hiển thị nút "Tạo", tạo ảnh, có nút "Tạo lại", cache IndexedDB, tự động lấy và quản lý nhiều userKey.
// @author       Dựa trên ý tưởng của bạn & Gemini & Claude
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @connect      image-generation.perchance.org
// @connect      perchance.org
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const PROMPT_REGEX = /(?:image|img)###([^#]+)###/gi;
    const CREATE_URL = 'https://image-generation.perchance.org/api/generate';
    const DOWNLOAD_URL = 'https://image-generation.perchance.org/api/downloadTemporaryImage';
    const VERIFY_KEY_URL = 'https://image-generation.perchance.org/api/checkUserVerificationStatus';
    const KEY_FETCH_PAGE = 'https://perchance.org/ai-text-to-image-generator';
    const PERCHANCE_IFRAME_PANEL_PATH_FALLBACK = 'ai-image-generator-panel';

    const USER_KEYS_STORAGE = 'perchanceUserKeys_v4_multi'; // Changed for multi-key
    const MAX_STORED_KEYS = 5;

    const ANIME_STYLE_DEFINITION = {
        positive: 'digital art, anime drawing, illustration, bold linework, cel shaded, painterly style, hyper-anime, intricate detail, 8k, fluid motion, stunning shading, highly detailed, realistic, dramatic lighting, cinematic, cinematic lighting, sharp focus, award-winning, masterpiece, breathtaking, exquisite, exceptional, premium, great attention to skin and eyes, acclaimed, viral, popular, buzzworthy, up-and-coming, emerging, promising',
        negative: 'Extra fingers, weird hands, extra hands, extra arms, weird arms, weird fingers, boring flat infographic, oversaturated, bad photo, terrible 3D render, bad anatomy, worst quality, greyscale, black and white, disfigured, deformed, glitch, cross-eyed, lazy eye, ugly, deformed, distorted, glitched, lifeless, low quality, bad proportions, Extra fingers, weird hands, extra hands, extra arms, weird arms, weird fingers, photography, low-quality, deformed, (text), blurry, bad art, (logo), watermark, blurred, cut off, extra fingers, bad quality, distortion of proportions, deformed fingers, elongated body, cropped image, deformed hands, deformed legs, deformed face, twisted fingers, double image, long neck, extra limb, plastic, disfigured, mutation, sloppy, ugly, pixelated, bad hands, aliasing, overexposed, oversaturated, burnt image, fuzzy, poor quality, extra limbs, deformed arms, Mosaic'
    };
    const DEFAULT_NEGATIVE_PROMPT_BASE = '';

    const DB_NAME = 'perchanceImageCacheDB_multi'; // Changed for multi-key version
    const DB_VERSION = 1;
    const IMAGE_STORE_NAME = 'images';
    let db;
    let activeRequests = new Set();
    let mutationObserver = null;

    // --- Quản lý User Key ---
    function getStoredUserKeys() {
        const keysJson = GM_getValue(USER_KEYS_STORAGE, JSON.stringify([]));
        try {
            const keys = JSON.parse(keysJson);
            return Array.isArray(keys) ? keys.filter(key => typeof key === 'string' && /^[a-f0-9]{64}$/.test(key)) : [];
        } catch (e) {
            console.error("Lỗi phân tích JSON từ UserKeys storage:", e);
            return [];
        }
    }

    function saveUserKeys(keys) {
        // Đảm bảo không trùng lặp và giới hạn số lượng
        const uniqueKeys = [...new Set(keys)].filter(key => typeof key === 'string' && /^[a-f0-9]{64}$/.test(key)); // Lọc key hợp lệ
        if (uniqueKeys.length > MAX_STORED_KEYS) {
            // Giữ lại MAX_STORED_KEYS key mới nhất (cuối danh sách)
            uniqueKeys.splice(0, uniqueKeys.length - MAX_STORED_KEYS);
        }
        GM_setValue(USER_KEYS_STORAGE, JSON.stringify(uniqueKeys));
        // console.log("Đã lưu danh sách keys:", uniqueKeys.map(k => k.slice(-6)));
        return uniqueKeys;
    }

    function addAndSaveUserKey(newKey) {
        if (!newKey || typeof newKey !== 'string' || !/^[a-f0-9]{64}$/.test(newKey)) {
            console.warn("Key không hợp lệ, không thêm vào danh sách:", newKey);
            return getStoredUserKeys();
        }
        let keys = getStoredUserKeys();
        const existingIndex = keys.indexOf(newKey);
        if (existingIndex > -1) { // Nếu key đã tồn tại, xóa nó để thêm lại vào cuối (đánh dấu là mới dùng)
            keys.splice(existingIndex, 1);
        }
        keys.push(newKey); // Thêm vào cuối (mới nhất)
        return saveUserKeys(keys);
    }

    function removeAndSaveUserKey(keyToRemove) {
        let keys = getStoredUserKeys();
        const index = keys.indexOf(keyToRemove);
        if (index > -1) {
            keys.splice(index, 1);
            keys = saveUserKeys(keys);
            console.log(`Đã xóa key ...${keyToRemove.slice(-6)} khỏi danh sách.`);
        }
        return keys;
    }

    // --- Khởi tạo IndexedDB ---
    async function initDB() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);
            request.onerror = (event) => { console.error("Lỗi mở IndexedDB:", event.target.error); reject(event.target.error); };
            request.onsuccess = (event) => { db = event.target.result; console.log("IndexedDB đã mở."); resolve(db); };
            request.onupgradeneeded = (event) => {
                const store = event.target.result.createObjectStore(IMAGE_STORE_NAME, { keyPath: 'promptKey' });
                store.createIndex('timestamp', 'timestamp', { unique: false });
            };
        });
    }

    // --- Lưu ảnh vào IndexedDB ---
    async function storeImage(promptKey, base64Image) {
        if (!db) { console.error("DB chưa sẵn sàng để lưu."); return; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readwrite');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const request = store.put({ promptKey: promptKey, image: base64Image, timestamp: Date.now() });
            request.onsuccess = () => resolve();
            request.onerror = (event) => { console.error(`Lỗi lưu ảnh ${promptKey.substring(0,10)}...:`, event.target.error); reject(event.target.error); };
        });
    }

    // --- Lấy ảnh từ IndexedDB ---
    async function getCachedImage(promptKey) {
        if (!db) { console.error("DB chưa sẵn sàng để lấy cache."); return null; }
        return new Promise((resolve, reject) => {
            const transaction = db.transaction([IMAGE_STORE_NAME], 'readonly');
            const store = transaction.objectStore(IMAGE_STORE_NAME);
            const request = store.get(promptKey);
            request.onsuccess = (event) => resolve(event.target.result ? event.target.result.image : null);
            request.onerror = (event) => { console.error(`Lỗi lấy cache ${promptKey.substring(0,10)}...:`, event.target.error); reject(event.target.error); };
        });
    }

    // --- Chuyển Blob sang Base64 ---
    function blobToBase64(blob) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        });
    }

    // --- Tự động lấy User Key ---
    async function fetchUserKeyAutomatically(showMessages = false) {
        if (showMessages) console.log(`Đang thử tự động lấy userKey từ ${KEY_FETCH_PAGE}...`);
        try {
            const mainPageResponse = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: KEY_FETCH_PAGE, onload: resolve, onerror: () => reject(new Error('Lỗi mạng (trang chính).')), ontimeout: () => reject(new Error('Timeout (trang chính).')) }));
            if (mainPageResponse.status !== 200) throw new Error(`Lỗi tải trang lấy key: ${mainPageResponse.status}`);

            const parser = new DOMParser();
            const mainDoc = parser.parseFromString(mainPageResponse.responseText, "text/html");
            let iframeSrc;
            const mainIframe = mainDoc.querySelector('iframe#main');
            if (mainIframe && mainIframe.getAttribute('src')) {
                 iframeSrc = new URL(mainIframe.getAttribute('src'), KEY_FETCH_PAGE).href;
            } else {
                const panelIframe = mainDoc.querySelector(`iframe[src*="${PERCHANCE_IFRAME_PANEL_PATH_FALLBACK}"]`);
                iframeSrc = panelIframe ? new URL(panelIframe.getAttribute('src'), KEY_FETCH_PAGE).href : new URL(PERCHANCE_IFRAME_PANEL_PATH_FALLBACK, new URL(KEY_FETCH_PAGE).origin).href;
            }
            if (!iframeSrc) throw new Error('Không thể xác định URL iframe chứa userKey.');

            const iframeResponse = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: iframeSrc, onload: resolve, onerror: () => reject(new Error(`Lỗi mạng (iframe ${iframeSrc}).`)), ontimeout: () => reject(new Error(`Timeout (iframe ${iframeSrc}).`)) }));
            if (iframeResponse.status !== 200) throw new Error(`Lỗi tải iframe (${iframeSrc}): ${iframeResponse.status}`);

            const iframeContent = iframeResponse.responseText;
            const keyRegex = /userKey(?:["']?:["']?|\s*=\s*['"]?)([a-f0-9]{64})['"]?/gi;
            let regexMatch;
            const potentialKeys = new Set();
            while ((regexMatch = keyRegex.exec(iframeContent)) !== null) potentialKeys.add(regexMatch[1]);

            if (potentialKeys.size === 0 && showMessages) console.warn(`Không tìm thấy userKey nào trong iframe từ ${iframeSrc}.`);

            for (const potentialKey of potentialKeys) {
                const verificationParams = new URLSearchParams({ 'userKey': potentialKey, '__cacheBust': Math.random().toString() });
                try {
                    const verificationResponseText = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: `${VERIFY_KEY_URL}?${verificationParams.toString()}`, onload: (r) => r.status === 200 ? resolve(r.responseText) : reject(new Error(`Trạng thái ${r.status}`)), onerror: () => reject(new Error('Lỗi mạng (xác minh).')), ontimeout: () => reject(new Error('Timeout (xác minh).')) }));
                    if (verificationResponseText && verificationResponseText.includes('verified') && !verificationResponseText.includes('not_verified')) {
                        if (showMessages) console.log(`UserKey tự động: ...${potentialKey.slice(-6)} (Đã xác minh từ ${iframeSrc})`);
                        addAndSaveUserKey(potentialKey); // Lưu key đã xác minh
                        return potentialKey;
                    } else if (showMessages) console.log(`Key ...${potentialKey.slice(-6)} xác minh thất bại. Resp:`, verificationResponseText.substring(0,100));
                } catch (verifyError) { if (showMessages) console.warn(`Lỗi xác minh key ...${potentialKey.slice(-6)}: ${verifyError.message}`); }
            }
            if (showMessages) console.warn(`Không tìm thấy userKey hợp lệ tự động từ ${iframeSrc}.`);
            return null;
        } catch (error) {
            if (showMessages) console.error("Lỗi tự động lấy userKey:", error.message);
            return null;
        }
    }

    // --- Hỏi người dùng User Key ---
    async function promptForUserKey(message) {
        const userKeyInput = prompt(message + `\n\n(Mẹo: Truy cập ${KEY_FETCH_PAGE}, mở Developer Tools (F12) > Network tab, thử tạo một hành động nào đó trên trang. Tìm request có 'userKey=xxx...' trong URL hoặc payload, copy phần giá trị 64 ký tự của userKey đó.)`);
        if (userKeyInput) {
            const trimmedKey = userKeyInput.trim();
            if (/^[a-f0-9]{64}$/.test(trimmedKey)) {
                addAndSaveUserKey(trimmedKey); // Lưu key người dùng nhập
                console.log(`Đã lưu userKey người dùng nhập: ...${trimmedKey.slice(-6)}`);
                return trimmedKey;
            } else {
                alert("Định dạng userKey không đúng (phải là 64 ký tự hexa).");
            }
        }
        return null;
    }

    // --- Gọi API tạo ảnh ---
    async function generateImageApi(promptText, negativePromptText, userKeyToUse, resolution = '512x768', guidanceScale = '7') {
        const createParams = new URLSearchParams({
            'prompt': promptText, 'negativePrompt': negativePromptText, 'userKey': userKeyToUse,
            '__cache_bust': Math.random().toString(), 'seed': '-1', 'resolution': resolution,
            'guidanceScale': guidanceScale.toString(), 'channel': 'image-generator-professional',
            'subChannel': 'public', 'requestId': Math.random().toString()
        });
        // console.log(`API Tạo ảnh: "${promptText.substring(0,20)}...", key: ...${userKeyToUse.slice(-6)}`);
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: `${CREATE_URL}?${createParams.toString()}`,
                headers: { "User-Agent": "Mozilla/5.0", "Accept": "application/json, text/plain, */*", "Referer": "https://perchance.org/" },
                onload: async function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.status === 'success' && data.imageId) resolve(data.imageId);
                        else if (data.status === 'invalid_key' || (data.error && data.error.includes('invalid_key'))) {
                            console.error(`UserKey ...${userKeyToUse.slice(-6)} không hợp lệ theo API.`);
                            reject(new Error('INVALID_KEY_API_ERROR')); // Lỗi cụ thể để xử lý việc thử key khác
                        } else reject(new Error(data.message || data.error || 'Lỗi không rõ từ API Perchance.'));
                    } catch (e) { reject(new Error(response.responseText.includes("<!doctype html>") ? 'API Perchance trả về HTML (có thể bị Cloudflare chặn).' : `Lỗi phân tích JSON từ API: ${e.message}`)); }
                },
                onerror: (err) => { console.error("GM_xmlhttpRequest error:", err); reject(new Error('Lỗi mạng khi tạo ảnh.')); },
                ontimeout: () => reject(new Error('Yêu cầu tạo ảnh đã hết thời gian chờ.'))
            });
        });
    }

    // --- Gọi API tải ảnh ---
    async function downloadImageApi(imageId) {
        // console.log(`API Tải ảnh ID: ${imageId}`);
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url: `${DOWNLOAD_URL}?imageId=${imageId}`, responseType: 'blob',
                headers: { "User-Agent": "Mozilla/5.0", "Accept": "image/webp,image/jpeg,image/png,*/*", "Referer": "https://perchance.org/" },
                onload: (r) => (r.status === 200 && r.response) ? resolve(r.response) : reject(new Error(`Không thể tải xuống ảnh. Trạng thái: ${r.status}`)),
                onerror: (err) => { console.error("GM_xmlhttpRequest error:", err); reject(new Error('Lỗi mạng khi tải ảnh.')); },
                ontimeout: () => reject(new Error('Yêu cầu tải ảnh đã hết thời gian chờ.'))
            });
        });
    }

    // --- Xử lý khi nhấp nút Tạo/Tạo lại ---
    async function handleGenerateClick(event) {
        const button = event.target;
        const basePromptText = button.dataset.prompt;
        const promptKey = button.dataset.promptKey; // Dùng cho cache IndexedDB
        const promptWrapper = button.closest('.perchance-prompt-container');
        const imagePlaceholder = promptWrapper ? promptWrapper.querySelector('.perchance-image-placeholder') : null;

        if (!basePromptText || !promptWrapper || !imagePlaceholder) {
            console.error("Thiếu thông tin cần thiết để tạo ảnh."); return;
        }

        const action = button.dataset.action || 'generate';
        button.disabled = true;
        activeRequests.add(promptKey); // Thêm vào active requests để tránh click đúp nhanh

        // Nếu là 'generate' (không phải 'regenerate') và có cache, hiển thị cache trước
        if (action === 'generate') {
            const cachedImage = await getCachedImage(promptKey);
            if (cachedImage) {
                imagePlaceholder.innerHTML = '';
                const img = document.createElement('img');
                img.src = cachedImage; img.alt = `${basePromptText} (từ cache)`; img.title = img.alt;
                Object.assign(img.style, { maxWidth: "100%", display: "block", border: "1px solid cyan", borderRadius: "3px" });
                imagePlaceholder.appendChild(img);
                button.textContent = 'Tạo lại'; button.dataset.action = 'regenerate';
                button.disabled = false; activeRequests.delete(promptKey);
                console.log(`Ảnh cho "${basePromptText.substring(0,20)}..." tải từ cache.`);
                return;
            }
        }
        // Nếu không có cache hoặc là 'regenerate', tiếp tục tạo mới
        imagePlaceholder.innerHTML = '<span style="font-size:0.8em; color:gray;">Đang chuẩn bị...</span>';

        let currentKeysInStorage = getStoredUserKeys(); // Lấy danh sách key hiện tại (sắp xếp từ cũ nhất đến mới nhất)
        let keysAttemptedInThisRun = new Set(); // Theo dõi các key đã thử trong lần click này
        let newKeyFetchedInThisRun = false; // Cờ đánh dấu đã thử lấy key mới trong lần click này chưa

        while (true) {
            let keyToTry = null;

            // Ưu tiên thử các key từ bộ nhớ chưa được thử trong lần chạy này (thử từ key mới nhất -> cũ nhất)
            for (let i = currentKeysInStorage.length - 1; i >= 0; i--) {
                const storedKey = currentKeysInStorage[i];
                if (!keysAttemptedInThisRun.has(storedKey)) {
                    keyToTry = storedKey;
                    break;
                }
            }

            // Nếu không còn key nào trong bộ nhớ để thử HOẶC chưa thử lấy key mới trong lần chạy này
            if (!keyToTry && !newKeyFetchedInThisRun) {
                newKeyFetchedInThisRun = true; // Đánh dấu đã thử lấy key mới
                imagePlaceholder.innerHTML = '<span style="font-size:0.8em; color:gray;">Đang lấy key mới...</span>';
                const autoKey = await fetchUserKeyAutomatically(true); // Hàm này sẽ lưu key nếu thành công
                if (autoKey) {
                    // Chỉ thử key mới lấy nếu nó chưa được thử trong lần chạy này (phòng trường hợp nó đã có trong storage và vừa fail)
                    if (!keysAttemptedInThisRun.has(autoKey)) keyToTry = autoKey;
                }
                if (!keyToTry) { // Nếu tự động lấy thất bại, hỏi người dùng
                    const promptedKey = await promptForUserKey("Tất cả key đã lưu không hợp lệ hoặc tự động lấy key thất bại. Vui lòng nhập Perchance userKey:");
                    if (promptedKey) {
                        if (!keysAttemptedInThisRun.has(promptedKey)) keyToTry = promptedKey;
                    }
                }
                currentKeysInStorage = getStoredUserKeys(); // Làm mới danh sách key sau khi có thể đã thêm key mới
            }

            if (!keyToTry) { // Không còn key nào để thử (tất cả đã thử, hoặc lấy mới thất bại)
                imagePlaceholder.innerHTML = `<p style="color:red;border:1px solid red;padding:3px;margin:0;font-size:0.8em;">Không còn key hợp lệ để thử hoặc không thể lấy key mới.</p>`;
                button.textContent = 'Hết key! Tạo lại';
                button.dataset.action = 'generate'; // Reset để lần sau có thể thử lại từ đầu
                break; // Thoát vòng lặp while
            }

            keysAttemptedInThisRun.add(keyToTry);
            button.textContent = `Tạo... (${keysAttemptedInThisRun.size})`;
            imagePlaceholder.innerHTML = `<span style="font-size:0.8em; color:gray;">Đang thử key ...${keyToTry.slice(-6)}</span>`;

            try {
                const finalPrompt = `${basePromptText}, ${ANIME_STYLE_DEFINITION.positive}`;
                const finalNegativePrompt = `${ANIME_STYLE_DEFINITION.negative}, ${DEFAULT_NEGATIVE_PROMPT_BASE}`;

                const imageId = await generateImageApi(finalPrompt, finalNegativePrompt, keyToTry);
                const imageBlob = await downloadImageApi(imageId);
                const base64Image = await blobToBase64(imageBlob);
                await storeImage(promptKey, base64Image);

                imagePlaceholder.innerHTML = ''; // Xóa thông báo đang tải/lỗi
                const img = document.createElement('img');
                img.src = base64Image; img.alt = `${basePromptText} (mới tạo)`; img.title = img.alt;
                Object.assign(img.style, { maxWidth: "100%", display: "block", border: "1px solid green", borderRadius: "3px" });
                imagePlaceholder.appendChild(img);
                button.textContent = 'Tạo lại'; button.dataset.action = 'regenerate';

                addAndSaveUserKey(keyToTry); // Key này hoạt động tốt, đảm bảo nó ở cuối danh sách (mới nhất)
                console.log(`Ảnh cho "${basePromptText.substring(0,20)}..." đã tạo thành công với key ...${keyToTry.slice(-6)}.`);
                break; // THÀNH CÔNG, thoát vòng lặp while

            } catch (error) {
                if (error.message === 'INVALID_KEY_API_ERROR') {
                    console.warn(`Key ...${keyToTry.slice(-6)} không hợp lệ. Đang xóa khỏi danh sách.`);
                    removeAndSaveUserKey(keyToTry); // Xóa key không hợp lệ
                    currentKeysInStorage = getStoredUserKeys(); // Cập nhật lại danh sách key
                    // Vòng lặp sẽ tiếp tục, thử key tiếp theo hoặc lấy key mới
                } else {
                    // Các lỗi khác (mạng, API thay đổi, v.v.)
                    console.error(`Lỗi khi tạo ảnh với key ...${keyToTry.slice(-6)}:`, error);
                    imagePlaceholder.innerHTML = `<p style="color:red;border:1px solid red;padding:3px;margin:0;font-size:0.8em;">Lỗi: ${error.message}.</p>`;
                    button.textContent = 'Lỗi! Tạo lại';
                    button.dataset.action = 'generate'; // Reset
                    break; // Thoát vòng lặp while đối với các lỗi không phải do key
                }
            }
        } // Kết thúc vòng lặp while(true)

        activeRequests.delete(promptKey);
        button.disabled = false;
        // Đảm bảo text của button phù hợp nếu vòng lặp kết thúc mà không thành công
        if (!imagePlaceholder.querySelector('img') && (button.textContent.startsWith('Tạo...') || button.textContent.startsWith('Hết key!'))) {
            if (!imagePlaceholder.innerHTML.includes('<p style="color:red')) { // Nếu chưa có thông báo lỗi cụ thể
                 imagePlaceholder.innerHTML = `<p style="color:orange;border:1px solid orange;padding:3px;margin:0;font-size:0.8em;">Không thể hoàn tất yêu cầu.</p>`;
            }
            if (button.textContent.startsWith('Tạo...')) button.textContent = 'Thất bại! Tạo lại';
            button.dataset.action = 'generate';
        }
    }


    // --- Xử lý các node trên trang để tìm prompt ---
    function processNode(node, targetDocument) {
        if (node.nodeType === Node.TEXT_NODE) {
            let match;
            let lastIndex = 0;
            const textContent = node.nodeValue;
            const parent = node.parentNode;

            if (!parent || parent.closest('textarea, script, style, input, button, .perchance-prompt-container, [data-perchance-processed-parent="true"]') || parent.isContentEditable) {
                return;
            }

            const fragment = targetDocument.createDocumentFragment();
            let replaced = false;
            PROMPT_REGEX.lastIndex = 0;

            while ((match = PROMPT_REGEX.exec(textContent)) !== null) {
                replaced = true;
                const basePromptText = match[1].trim();
                const promptKey = CryptoJS.MD5(basePromptText).toString();

                if (match.index > lastIndex) {
                    fragment.appendChild(targetDocument.createTextNode(textContent.substring(lastIndex, match.index)));
                }

                const promptWrapper = targetDocument.createElement('span');
                promptWrapper.className = 'perchance-prompt-container';
                Object.assign(promptWrapper.style, { display: 'inline-block', verticalAlign: 'middle', margin: '0 2px' });

                const generateButton = targetDocument.createElement('button');
                generateButton.textContent = 'Tạo';
                generateButton.dataset.prompt = basePromptText;
                generateButton.dataset.promptKey = promptKey;
                generateButton.dataset.action = 'generate';
                Object.assign(generateButton.style, {
                    padding: '1px 5px', fontSize: '12px', lineHeight: '1.2', cursor: 'pointer',
                    border: '1px solid #ababab', borderRadius: '3px', backgroundColor: '#e9e9e9',
                    color: '#333', marginRight: '3px', marginLeft: '2px'
                });
                generateButton.onmouseover = function() { this.style.backgroundColor = '#dcdcdc'; };
                generateButton.onmouseout = function() { this.style.backgroundColor = '#e9e9e9'; };
                generateButton.addEventListener('click', handleGenerateClick);
                promptWrapper.appendChild(generateButton);

                const imagePlaceholder = targetDocument.createElement('div');
                imagePlaceholder.className = 'perchance-image-placeholder';
                Object.assign(imagePlaceholder.style, { marginTop: '3px', minHeight: '20px', textAlign: 'left' });
                promptWrapper.appendChild(imagePlaceholder);

                fragment.appendChild(promptWrapper);
                lastIndex = PROMPT_REGEX.lastIndex;
            }

            if (replaced) {
                if (lastIndex < textContent.length) {
                    fragment.appendChild(targetDocument.createTextNode(textContent.substring(lastIndex)));
                }
                parent.replaceChild(fragment, node);
                if (parent.nodeType === Node.ELEMENT_NODE) {
                    parent.dataset.perchanceProcessedParent = 'true';
                }
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'IFRAME', 'CANVAS', 'INPUT', 'BUTTON', 'A'].includes(node.tagName.toUpperCase()) ||
                node.isContentEditable || node.closest('.perchance-prompt-container') || node.dataset.perchanceProcessed === 'true') {
                return;
            }
            Array.from(node.childNodes).forEach(child => processNode(child, targetDocument));
            node.dataset.perchanceProcessed = 'true';
        }
    }

    function setupObserver(targetDoc) {
        if (mutationObserver) mutationObserver.disconnect();
        mutationObserver = new MutationObserver(async (mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(newNode => {
                        if ((newNode.nodeType === Node.ELEMENT_NODE && newNode.dataset.perchanceProcessed === 'true') ||
                            (newNode.nodeType === Node.TEXT_NODE && newNode.parentNode && newNode.parentNode.dataset.perchanceProcessedParent === 'true')) return;
                        processNode(newNode, targetDoc);
                    });
                } else if (mutation.type === 'characterData') {
                     if (mutation.target.parentNode && mutation.target.parentNode.dataset.perchanceProcessedParent !== 'true') {
                        processNode(mutation.target.parentNode, targetDoc); // Xử lý lại parent node nếu text thay đổi
                     }
                }
            }
        });
        mutationObserver.observe(targetDoc.body, { childList: true, subtree: true, characterData: true });
        console.log("MutationObserver đã thiết lập cho:", targetDoc.location.href || "document chính");
    }

    // --- Hàm chính ---
    async function main() {
        console.log("Perchance Image Replacer (VN) - Multi-Key v0.9.5");
        try {
            await initDB();
        } catch (error) { /* Lỗi đã được log */ }

        // Kiểm tra và thông báo số lượng key đã lưu
        const initialKeys = getStoredUserKeys();
        if (initialKeys.length > 0) {
            console.log(`Tìm thấy ${initialKeys.length} userKey đã lưu. Key gần nhất: ...${initialKeys[initialKeys.length - 1].slice(-6)}`);
        } else {
            console.log("Không có userKey nào được lưu trữ. Sẽ thử lấy tự động hoặc hỏi khi cần.");
        }

        const outputIframe = document.querySelector('iframe#outputIframeEl');
        if (outputIframe) {
            const handleIframeLoad = () => {
                if (outputIframe.contentDocument && outputIframe.contentDocument.body) {
                    try {
                        console.log("iframe#outputIframeEl đã tải. Bắt đầu xử lý node...");
                        processNode(outputIframe.contentDocument.body, outputIframe.contentDocument);
                        setupObserver(outputIframe.contentDocument);
                    } catch(e) {
                        console.error("Lỗi khi xử lý hoặc quan sát iframe:", e, ". Fallback về document.body.");
                        processNode(document.body, document); setupObserver(document);
                    }
                } else {
                    console.error("Không thể truy cập contentDocument.body của iframe. Fallback về document.body.");
                    processNode(document.body, document); setupObserver(document);
                }
            };

            if (outputIframe.contentDocument && outputIframe.contentDocument.readyState === 'complete') {
                handleIframeLoad();
            } else {
                 console.log("Đang chờ iframe#outputIframeEl tải...");
                outputIframe.addEventListener('load', handleIframeLoad, { once: true });
            }
        } else {
            console.warn("Không tìm thấy iframe#outputIframeEl. Sẽ xử lý trên document.body chính.");
            processNode(document.body, document);
            setupObserver(document);
        }
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(main, 500); // Thêm độ trễ nhỏ để đảm bảo iframe (nếu có) kịp khởi tạo
    } else {
        window.addEventListener('DOMContentLoaded', () => setTimeout(main, 500));
    }

})();