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.

Versión del día 10/5/2025. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name         Perchance Image Replacer (Vietnamese Version) - Multi-Key
// @namespace    http://tampermonkey.net/
// @version      0.9.9
// @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: '(An 8k hyper-anime, HDR photo, highly detailed, split-complementary color palette, UHD, hyperrealistic, casting shadow style, vivid, clear shadows and highlights, intense, enhanced contrast, best quality, masterpiece, well-structured, vibrant colors, dynamic lighting, high resolution, sharp focus, crisp details, smooth texture, clean lines, highly polished, accurate proportions, age-appropriate features, natural skin tones, anime natural facial features, realistic age portrayal, subtle age details, refined character depth, soft, natural lighting, gentle, expressive shadows, nuanced facial contours, detailed, lifelike textures, artistically integrated age signs when applicable, flexible racial/spesies features.) NSFW, Uncensored Hentai Artwork, Ecchi, Explicit Scenes, Adult Content, Explicit Sexual Acts, Visual Hentai',
        negative: '(low quality, worst quality:1.3), text, watermark, signature, title, overused, generic, cliché, unoriginal, bland, uninspired, conventional, inconsistent style, mismatched elements, clashing colors, disjointed composition, uneven proportions, incoherent design, unbalanced layout, disorganized appearance, randomized elements, asymmetrical features, overused, predictable, wrong sex, wrong gender, wrong species, wrong age, misleading gender presentation, incorrect intimacy positioning, unnatural intimacy, distorted features, harsh lighting, overly saturated colors, cluttered background, unrealistic proportions, lack of detail, exaggerated expressions, pixelated or low-quality image, anatomical inaccuracies, mutations, deformities, disfigurements, grotesque elements, unnatural body proportions, blurred, jpeg artifacts, cropped, cut-off, flat shading, unnatural line integration with background, stiff poses, unnatural skin textures, overexposed areas, underexposed areas, excessive noise, artificial-looking shadows, disproportionate hands, large or creepy fingers and toes, deformities in close-up shots'
    };
    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': 'ai-text-to-image-generators',
            '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));
    }

})();