Telegram 输入框翻译并发送 (v2.9 - 重试缩写+缓存+内部状态栏)

v2.8基础: 增加"重试缩写"按钮再次处理已翻译文本; 增加内存缓存; 状态栏移到输入框内部; 增加翻译重试按钮.

目前為 2025-04-29 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Telegram 输入框翻译并发送 (v2.9 - 重试缩写+缓存+内部状态栏)
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  v2.8基础: 增加"重试缩写"按钮再次处理已翻译文本; 增加内存缓存; 状态栏移到输入框内部; 增加翻译重试按钮.
// @author       Your Name / AI Assistant
// @match        https://web.telegram.org/k/*
// @match        https://web.telegram.org/a/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      api.ohmygpt.com
// @icon         https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Telegram_logo.svg/48px-Telegram_logo.svg.png
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const OHMYGPT_API_KEY = "sk-RK1MU6Cg6a48fBecBBADT3BlbKFJ4C209a954d3b4428b54b"; // Replace with your actual key if different
    const OHMYGPT_API_ENDPOINT = "https://api.ohmygpt.com/v1/chat/completions";
    const INPUT_TRANSLATE_MODEL = "gpt-4o-mini";
    const MAX_CACHE_SIZE = 100;

    // --- Translation Prompt (Unchanged) ---
    const TRANSLATION_PROMPT = `
Role: You are a professional translator executing a specific task.
Task: Translate the user's Chinese or Burmese text, OR existing English text, into US-style English, adhering strictly to the output requirements below. Apply the abbreviation rules even if the input is already English.
Strict Output Requirements:
1.  **Style:** Use US-style English with common letter-based abbreviations (e.g., u, ur, r, thx, &, bfr, frst, tmrw, nxt).
2.  **Sophistication:** Maintain a high English level with polished, articulate, and sophisticated word choices despite abbreviations.
3.  **Meaning:** Preserve the full original meaning. Include question marks if the original is a question.
4.  **Punctuation:** Do NOT end the translation with a period (.).
5.  **Abbreviations:** ONLY use letter-based abbreviations. ABSOLUTELY NO number-based abbreviations (NO "2" for "to", NO "4" for "for"). Use "to", "for". Double-check your output for numbers in abbreviations.
6.  **Format:** Output ONLY the translated text. NO explanations, NO notes, NO apologies, NO preliminary remarks. If translation/abbreviation is unnecessary (e.g., proper nouns, codes), return the original text unmodified.

Input Text to Translate/Abbreviate:
{text_to_translate}
`; // Updated prompt description slightly

    // Selectors
    const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
    const SEND_BUTTON_SELECTOR = 'button.btn-send';
    const INPUT_AREA_CONTAINER_SELECTOR = '.chat-input-main';

    // UI Element IDs
    const STATUS_BAR_ID = 'custom-input-status-bar';
    const AUTO_SEND_TOGGLE_ID = 'custom-auto-send-toggle';
    const RETRY_BUTTON_ID = 'custom-translate-retry-button'; // Retries ORIGINAL text
    const RETRY_ABBREVIATION_BUTTON_ID = 'custom-abbreviation-retry-button'; // Retries CURRENT text

    // Language Detection Regex
    const CHINESE_REGEX = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/;
    const BURMESE_REGEX = /[\u1000-\u109F]/;

    // State Variables
    let statusBarElement = null;
    let autoSendToggleElement = null;
    let currentInputApiXhr = null;
    let isTranslatingAndSending = false;
    let sendButtonClickListenerAttached = false;
    let autoSendEnabled = false;
    let lastOriginalText = null; // Stores the initial Chinese/Burmese text
    const translationCache = new Map();

    // --- CSS Styles ---
    GM_addStyle(`
        /* Container */
        ${INPUT_AREA_CONTAINER_SELECTOR} { position: relative !important; overflow: visible !important; }

        /* Status Bar */
        #${STATUS_BAR_ID} {
            position: absolute; bottom: 2px; left: 8px; right: 8px; display: none;
            padding: 4px 8px; font-size: 12px; color: #ccc;
            background-color: rgba(20, 20, 20, 0.85); backdrop-filter: blur(2px);
            border-top: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px;
            z-index: 149; line-height: 1.3; text-align: left;
            transition: opacity 0.2s ease-in-out, bottom 0.2s ease-in-out;
            opacity: 0; pointer-events: none;
        }
        #${STATUS_BAR_ID}.visible {
            display: flex; justify-content: space-between; align-items: center;
            opacity: 1; pointer-events: auto;
        }
        #${STATUS_BAR_ID} .status-text { /* Container for the text message */
             flex-grow: 1; margin-right: 8px;
        }
        #${STATUS_BAR_ID} .status-buttons { /* Container for buttons */
            display: flex; gap: 5px; /* Add gap between buttons */
            flex-shrink: 0; /* Prevent buttons container from shrinking */
        }

        /* Text Status Types */
        #${STATUS_BAR_ID} .status { font-style: italic; color: #a0a0a0; }
        #${STATUS_BAR_ID} .info { font-style: italic; color: #87cefa; } /* Light blue for cache */
        #${STATUS_BAR_ID} .error { font-weight: bold; color: #ff8a8a; }
        #${STATUS_BAR_ID} .success { font-weight: bold; color: #8ade8a; }

        /* Retry Buttons Style (shared) */
        #${RETRY_BUTTON_ID}, #${RETRY_ABBREVIATION_BUTTON_ID} {
            padding: 2px 6px; font-size: 11px; font-weight: bold; color: #d0d0d0;
            background-color: rgba(80, 80, 80, 0.9);
            border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px;
            cursor: pointer; flex-shrink: 0; /* Prevent shrinking */
            transition: background-color 0.2s ease, color 0.2s ease;
            white-space: nowrap; /* Prevent button text wrapping */
        }
        #${RETRY_BUTTON_ID}:hover, #${RETRY_ABBREVIATION_BUTTON_ID}:hover {
            background-color: rgba(100, 100, 100, 0.9); color: #fff;
        }
         #${RETRY_BUTTON_ID}:active, #${RETRY_ABBREVIATION_BUTTON_ID}:active {
            background-color: rgba(60, 60, 60, 0.9);
        }

        /* Auto-Send Toggle Button */
        #${AUTO_SEND_TOGGLE_ID} {
            position: absolute; bottom: 100%; margin-bottom: 1px; right: 10px;
            z-index: 151; padding: 4px 10px; font-size: 12px; font-weight: bold;
            background-color: rgba(80, 80, 80, 0.9); color: #ccc;
            border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none;
            border-radius: 6px 6px 0 0; cursor: pointer; user-select: none;
            transition: background-color 0.2s ease, color 0.2s ease;
        }
        #${AUTO_SEND_TOGGLE_ID}.autosend-on { background-color: rgba(70, 130, 180, 0.95); color: #fff; }
        #${AUTO_SEND_TOGGLE_ID}:hover { filter: brightness(1.1); }
    `);

    // --- Helper Functions ---
    function detectLanguage(text) { if (!text) return null; if (CHINESE_REGEX.test(text)) return 'Chinese'; if (BURMESE_REGEX.test(text)) return 'Burmese'; return 'Other'; }
    function setCursorToEnd(element) { try { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); element.focus(); } catch (e) { console.warn("[InputTranslate] Error setting cursor:", e); } }
    function ensureControlsExist() { /* ... (unchanged from v2.8) ... */
        const inputMainContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
        if (!inputMainContainer) return;
        if (window.getComputedStyle(inputMainContainer).position !== 'relative') { inputMainContainer.style.position = 'relative'; }
        if (!autoSendToggleElement || !inputMainContainer.contains(autoSendToggleElement)) {
            autoSendToggleElement = document.createElement('button');
            autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID;
            updateAutoSendButtonVisual();
            autoSendToggleElement.addEventListener('click', toggleAutoSend);
            inputMainContainer.appendChild(autoSendToggleElement);
            console.log("[InputTranslate] Auto-send toggle button created.");
        }
        if (!statusBarElement || !inputMainContainer.contains(statusBarElement)) {
            statusBarElement = document.createElement('div');
            statusBarElement.id = STATUS_BAR_ID;
            inputMainContainer.appendChild(statusBarElement);
            console.log("[InputTranslate] Status bar element created.");
        }
    }

    // Updated status display to handle both retry buttons
    function updateStatusDisplay(content, type = 'status', duration = 0, showRetryButton = false, showRetryAbbreviationButton = false) {
        ensureControlsExist();
        if (!statusBarElement) { console.error("[InputTranslate] Status bar element not found for update."); return; }

        let buttonsHtml = '';
        // Build buttons HTML string
        if (showRetryButton && lastOriginalText) {
             buttonsHtml += `<button id="${RETRY_BUTTON_ID}" type="button">重试原文</button>`; // Retry Original
        }
        if (showRetryAbbreviationButton) { // Condition to show the new button
             // Add a check for input element content? Maybe not needed here, handler will check.
             buttonsHtml += `<button id="${RETRY_ABBREVIATION_BUTTON_ID}" type="button">重试缩写</button>`; // Retry Abbreviation
        }

        // Use flex structure with separate containers for text and buttons
        statusBarElement.innerHTML = `
            <span class="status-text ${type}">${content}</span>
            ${buttonsHtml ? `<div class="status-buttons">${buttonsHtml}</div>` : ''}
        `;
        statusBarElement.classList.add('visible');

        // Attach listeners AFTER adding buttons to DOM
        if (showRetryButton && lastOriginalText) {
            const retryBtn = statusBarElement.querySelector(`#${RETRY_BUTTON_ID}`);
            if (retryBtn) retryBtn.addEventListener('click', handleRetryOriginalClick);
        }
        if (showRetryAbbreviationButton) {
            const retryAbbrBtn = statusBarElement.querySelector(`#${RETRY_ABBREVIATION_BUTTON_ID}`);
            if (retryAbbrBtn) retryAbbrBtn.addEventListener('click', handleRetryAbbreviationClick);
        }

        // Auto-hide logic (unchanged)
        if (statusBarElement.hideTimeout) clearTimeout(statusBarElement.hideTimeout);
        statusBarElement.hideTimeout = duration > 0 ? setTimeout(hideStatusDisplay, duration) : null;
    }

    function hideStatusDisplay() { /* ... (unchanged from v2.8) ... */
        if (statusBarElement) {
            if (statusBarElement.hideTimeout) clearTimeout(statusBarElement.hideTimeout);
            statusBarElement.hideTimeout = null;
            statusBarElement.classList.remove('visible');
            setTimeout(() => { if (statusBarElement && !statusBarElement.classList.contains('visible')) { statusBarElement.innerHTML = ''; } }, 250);
        }
    }

    // --- Retry Button Handlers ---

    // Renamed original retry handler slightly for clarity
    function handleRetryOriginalClick(event) {
         event.preventDefault(); event.stopPropagation();
         console.log("[InputTranslate] Retry Original button clicked.");
         if (isTranslatingAndSending) { console.warn("[InputTranslate] Already translating, ignoring retry original click."); return; }
         if (!lastOriginalText) { console.warn("[InputTranslate] No original text stored for retry."); hideStatusDisplay(); return; }
         const inputElement = document.querySelector(INPUT_SELECTOR);
         const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
         if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, false); return; } // Show only original retry on element error
         if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, false); return; }
         console.log("[InputTranslate] Retrying original translation for:", lastOriginalText);
         // Call the main translation function with the *original* text, force API
         translateAndSend(lastOriginalText, inputElement, sendButton, true);
    }

    // << NEW >> Handler for the "Retry Abbreviation" button
    function handleRetryAbbreviationClick(event) {
        event.preventDefault(); event.stopPropagation();
        console.log("[InputTranslate] Retry Abbreviation button clicked.");
        if (isTranslatingAndSending) { console.warn("[InputTranslate] Already translating, ignoring retry abbreviation click."); return; }

        const inputElement = document.querySelector(INPUT_SELECTOR);
        const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
        if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, false); return; } // Show only original retry on element error

        const currentText = inputElement.textContent?.trim();
        if (!currentText) {
            console.warn("[InputTranslate] Input field is empty, cannot retry abbreviation.");
            hideStatusDisplay();
            return;
        }
        if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, true); return; } // Show both buttons maybe? Or just original? Let's show both if possible.

        console.log("[InputTranslate] Retrying abbreviation/translation for current text:", currentText);
        // Call the main translation function with the *current* text from input, force API
        // The prompt is designed to handle English input too for re-abbreviation.
        translateAndSend(currentText, inputElement, sendButton, true);
    }


    // --- Auto Send Toggle Logic (Unchanged) ---
    function updateAutoSendButtonVisual() { /* ... (unchanged from v2.8) ... */
        if (!autoSendToggleElement) return;
        autoSendToggleElement.textContent = autoSendEnabled ? "自动发送: 开" : "自动发送: 关";
        autoSendToggleElement.className = autoSendEnabled ? 'autosend-on' : '';
        autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID;
    }
    function toggleAutoSend() { /* ... (unchanged from v2.8) ... */
        autoSendEnabled = !autoSendEnabled;
        console.log(`[InputTranslate] Auto Send Toggled: ${autoSendEnabled ? 'ON' : 'OFF'}`);
        updateAutoSendButtonVisual();
        updateStatusDisplay(`自动发送已${autoSendEnabled ? '开启' : '关闭'}`, 'status', 2000);
    }

    // --- Main Translation Logic with Cache & Updated Status Calls ---
    function translateAndSend(textToProcess, inputElement, sendButton, forceApi = false) {
        if (isTranslatingAndSending) { console.warn("[InputTranslate] Already processing, ignoring translateAndSend call."); return; }
        if (!inputElement || !sendButton) {
            updateStatusDisplay("错误: 无法找到输入或发送按钮", 'error', 4000, true, false); // Show only original retry
            return;
        }

        isTranslatingAndSending = true;
        // Only update lastOriginalText if the input seems like Chinese/Burmese (heuristic)
        const detectedLang = detectLanguage(textToProcess);
        if (detectedLang === 'Chinese' || detectedLang === 'Burmese') {
            lastOriginalText = textToProcess;
        }
        // If textToProcess is already English (likely from Retry Abbreviation), lastOriginalText remains unchanged.

        hideStatusDisplay();

        // --- Cache Check --- (Only check cache if not forcing API and input looks like source lang)
        const useCache = !forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese');
        if (useCache && translationCache.has(textToProcess)) {
            const cachedTranslation = translationCache.get(textToProcess);
            console.log("[InputTranslate] Cache Hit:", textToProcess);
            updateStatusDisplay("已从缓存加载 ✓", 'info', 3000, false, !autoSendEnabled); // Show retry abbr if manual send

            inputElement.textContent = cachedTranslation;
            setCursorToEnd(inputElement);
            inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));

            if (autoSendEnabled) {
                const sendDelay = 50;
                console.log(`[InputTranslate][Cache] Auto-sending ON. Setting timeout (${sendDelay}ms) for click.`);
                setTimeout(() => {
                    if (!isTranslatingAndSending) { console.log("[InputTranslate][CacheTimeout] Sending aborted."); isTranslatingAndSending = false; return; }
                    if (sendButton && sendButton.isConnected && !sendButton.disabled) {
                        sendButton.click(); hideStatusDisplay();
                    } else {
                        updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); // Show both retries on send fail
                    }
                    isTranslatingAndSending = false;
                }, sendDelay);
            } else {
                console.log("[InputTranslate][Cache] Auto-sending OFF.");
                // Status already shown above with Retry Abbreviation button
                isTranslatingAndSending = false;
            }
            return; // <<< Return after cache hit handled
        }
        // --- End Cache Check ---

        // --- API Call ---
        console.log(`[InputTranslate] ${forceApi ? 'Forcing API call' : 'Cache Miss'}. Calling API (${INPUT_TRANSLATE_MODEL}) for: "${textToProcess.substring(0, 30)}..."`);
        updateStatusDisplay("翻译/缩写中...", 'status'); // No buttons during processing

        const finalPrompt = TRANSLATION_PROMPT.replace('{text_to_translate}', textToProcess);
        const requestBody = { model: INPUT_TRANSLATE_MODEL, messages: [{"role": "user", "content": finalPrompt }], temperature: 0.6 };

        if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); }

        currentInputApiXhr = GM_xmlhttpRequest({
            method: "POST", url: OHMYGPT_API_ENDPOINT,
            headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OHMYGPT_API_KEY}` },
            data: JSON.stringify(requestBody),
            onload: function(response) {
                currentInputApiXhr = null;
                try {
                    if (response.status >= 200 && response.status < 300) {
                        const data = JSON.parse(response.responseText);
                        const translation = data.choices?.[0]?.message?.content?.trim();

                        if (translation) {
                            console.log("[InputTranslate] API Success:", translation);

                            // Cache the result only if it was an initial translation (not forced)
                            if (!forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
                                if (translationCache.size >= MAX_CACHE_SIZE) {
                                    const oldestKey = translationCache.keys().next().value;
                                    translationCache.delete(oldestKey);
                                }
                                translationCache.set(textToProcess, translation); // Cache original -> translation
                                console.log("[InputTranslate] Cached:", textToProcess);
                            }

                            inputElement.textContent = translation;
                            setCursorToEnd(inputElement);
                            inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));

                            if (autoSendEnabled) {
                                const sendDelay = 150;
                                console.log(`[InputTranslate][API] Auto-sending ON. Setting timeout (${sendDelay}ms) for click.`);
                                setTimeout(() => {
                                    if (!isTranslatingAndSending) { console.log("[InputTranslate][APITimeout] Sending aborted."); isTranslatingAndSending = false; return; }
                                    if (sendButton && sendButton.isConnected && !sendButton.disabled) {
                                        sendButton.click(); hideStatusDisplay();
                                    } else {
                                        updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); // Show both retries on send fail
                                    }
                                    isTranslatingAndSending = false;
                                }, sendDelay);
                            } else {
                                console.log("[InputTranslate][API] Auto-sending OFF.");
                                // Show success message WITH BOTH retry buttons
                                updateStatusDisplay("完成 ✓ (请手动发送或重试)", 'success', 5000, true, true);
                                isTranslatingAndSending = false;
                            }
                        } else { // API returned 2xx but no content
                             throw new Error(`API Error: No translation content (Choice: ${data.choices?.[0]?.finish_reason || 'N/A'})`);
                        }
                    } else { // API returned non-2xx status
                         let errorDetail = `HTTP ${response.status}: ${response.statusText}`;
                         try { const errData = JSON.parse(response.responseText); errorDetail = errData.error?.message || errorDetail; } catch (e) { /* ignore */ }
                         throw new Error(errorDetail);
                    }
                } catch (e) { // Catch JSON parse errors or errors thrown above
                    console.error("[InputTranslate] API/Parse Error:", e);
                    // Show error message WITH ONLY original retry button
                    updateStatusDisplay(`处理失败: ${e.message.substring(0, 60)}`, 'error', 5000, true, false);
                    isTranslatingAndSending = false;
                }
            },
            onerror: function(response) { currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateStatusDisplay(`处理失败: 网络错误 (${response.status || 'N/A'})`, 'error', 5000, true, false); isTranslatingAndSending = false; },
            ontimeout: function() { currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateStatusDisplay("处理失败: 请求超时", 'error', 5000, true, false); isTranslatingAndSending = false; },
            onabort: function() { currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); hideStatusDisplay(); isTranslatingAndSending = false; },
            timeout: 30000
        });
    }


    // --- Event Listeners ---
    function handleInputKeyDown(event) {
        const inputElement = event.target;
        if (!inputElement || !inputElement.matches(INPUT_SELECTOR)) return;

        if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) {
             // Allow normal send if status shows success/info and not processing + manual send
             if (statusBarElement && statusBarElement.classList.contains('visible') && !isTranslatingAndSending && !autoSendEnabled) {
                  const nonBlockingStatus = statusBarElement.querySelector('span.success, span.info');
                  if (nonBlockingStatus) {
                       console.log("[InputTranslate][Enter] Allowing manual send.");
                       hideStatusDisplay(); return;
                  }
             }
             if (isTranslatingAndSending) { event.preventDefault(); event.stopPropagation(); return; }

             const text = inputElement.textContent?.trim() || "";
             const detectedLang = detectLanguage(text);
             if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
                 console.log(`[InputTranslate][Enter] Detected ${detectedLang}. Processing...`);
                 event.preventDefault(); event.stopPropagation();
                 const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
                 if (!sendButton) { updateStatusDisplay("错误: 未找到发送按钮!", 'error', 5000, true, false); return; }
                 if (sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, false); return; }
                 translateAndSend(text, inputElement, sendButton); // Initial translation call
             } else { hideStatusDisplay(); } // Allow normal send
        }
        // Abort logic (unchanged)
        else if (isTranslatingAndSending && !['Shift', 'Control', 'Alt', 'Meta', 'Enter'].includes(event.key)) {
             hideStatusDisplay();
             if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); }
             else { isTranslatingAndSending = false; }
        } else if (!isTranslatingAndSending) {
             if (statusBarElement && statusBarElement.classList.contains('visible')) {
                 const statusSpan = statusBarElement.querySelector('span.status'); // Only hide non-"Translating..." messages on typing
                 if (!statusSpan) { hideStatusDisplay(); }
             }
        }
    }

    function handleSendButtonClick(event) {
         const sendButton = event.target.closest(SEND_BUTTON_SELECTOR);
         if (!sendButton) return;

         // Allow normal send if status shows success/info and not processing + manual send
         if (statusBarElement && statusBarElement.classList.contains('visible') && !isTranslatingAndSending && !autoSendEnabled) {
             const nonBlockingStatus = statusBarElement.querySelector('span.success, span.info');
             if (nonBlockingStatus) {
                  console.log("[InputTranslate][Click] Allowing manual send.");
                  hideStatusDisplay(); return;
             }
         }

         const inputElement = document.querySelector(INPUT_SELECTOR);
         if (!inputElement) return;
         const text = inputElement.textContent?.trim() || "";
         const detectedLang = detectLanguage(text);

         if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
             if (isTranslatingAndSending) { event.preventDefault(); event.stopPropagation(); return; }
             console.log(`[InputTranslate][Click] Detected ${detectedLang}. Processing...`);
             event.preventDefault(); event.stopPropagation();
             if (sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, false); return; }
             translateAndSend(text, inputElement, sendButton); // Initial translation call
         } else {
             if (!isTranslatingAndSending) { hideStatusDisplay(); } // Allow normal send
         }
    }

    // --- Initialization & Attaching Listeners (Unchanged structure) ---
    function initialize() {
        console.log("[Telegram Input Translator v2.9 - Retry Abbr + Cache + Status Inside] Initializing...");
        // ... (Observer logic is the same as v2.8) ...
        const observer = new MutationObserver(mutations => {
             let controlsNeedCheck = false;
             mutations.forEach(mutation => {
                if (mutation.addedNodes) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType !== 1) return;
                        const containerNode = node.matches(INPUT_AREA_CONTAINER_SELECTOR) ? node : node.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
                        if(containerNode) controlsNeedCheck = true;
                        const inputElementNode = node.matches(INPUT_SELECTOR) ? node : node.querySelector(INPUT_SELECTOR);
                        if (inputElementNode && !inputElementNode.dataset.customInputTranslateListener) {
                            attachInputListeners(inputElementNode);
                            controlsNeedCheck = true;
                        }
                    });
                }
                 if (mutation.target && mutation.target.matches && mutation.target.matches(INPUT_AREA_CONTAINER_SELECTOR)) {
                     controlsNeedCheck = true;
                 }
            });
             if (controlsNeedCheck) { ensureControlsExist(); }
            if (!sendButtonClickListenerAttached) { const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (sendButton && !sendButton.dataset.customSendClickListener) { attachSendButtonListener(sendButton); } }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Initial check
        const initialContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
        if (initialContainer) { ensureControlsExist(); }
        const initialInputElement = document.querySelector(INPUT_SELECTOR);
        if (initialInputElement && !initialInputElement.dataset.customInputTranslateListener) { attachInputListeners(initialInputElement); }
        const initialSendButton = document.querySelector(SEND_BUTTON_SELECTOR);
        if(initialSendButton && !initialSendButton.dataset.customSendClickListener) { attachSendButtonListener(initialSendButton); }

        console.log("[Telegram Input Translator v2.9] Observer active.");
    }
    function attachInputListeners(inputElement) { /* ... (unchanged from v2.8) ... */
         if (inputElement.dataset.customInputTranslateListener) return;
         console.log("[InputTranslate] Attaching Keydown listener to input:", inputElement);
         inputElement.addEventListener('keydown', handleInputKeyDown, true);
         inputElement.dataset.customInputTranslateListener = 'true';
         ensureControlsExist();
    }
    function attachSendButtonListener(sendButton) { /* ... (unchanged from v2.8) ... */
        if (sendButton.dataset.customSendClickListener) return;
         console.log("[InputTranslate] Attaching Click listener to Send button:", sendButton);
         sendButton.addEventListener('click', handleSendButtonClick, true);
         sendButton.dataset.customSendClickListener = 'true';
         sendButtonClickListenerAttached = true;
         const buttonObserver = new MutationObserver(() => {
             if (!sendButton.isConnected) {
                 console.log("[InputTranslate] Send button removed. Resetting listener flag.");
                 buttonObserver.disconnect();
                 if (sendButton.dataset.customSendClickListener) { delete sendButton.dataset.customSendClickListener; }
                 sendButtonClickListenerAttached = false;
             }
         });
         if (sendButton.parentNode) { buttonObserver.observe(sendButton.parentNode, { childList: true, subtree: false }); }
         else { console.warn("[InputTranslate] Send button parent node not found for observer."); }
    }

    // --- Start Initialization ---
    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1800)); }
    else { setTimeout(initialize, 1800); }

})();