Telegram 输入框翻译并发送 (v2.1 - 支持回车和按钮)

按回车或点击发送按钮时,翻译输入框内容(中/缅->指定风格英文)并自动替换和发送。拦截中文/缅甸语原文发送。

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

// ==UserScript==
// @name         Telegram 输入框翻译并发送 (v2.1 - 支持回车和按钮)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  按回车或点击发送按钮时,翻译输入框内容(中/缅->指定风格英文)并自动替换和发送。拦截中文/缅甸语原文发送。
// @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"; // 你的 OhMyGPT API Key
    const OHMYGPT_API_ENDPOINT = "https://api.ohmygpt.com/v1/chat/completions";
    const INPUT_TRANSLATE_MODEL = "gpt-4o-mini"; // 输入框翻译模型

    const TRANSLATION_PROMPT = `Ur a pro translator, & u must strictly follow these rules, understood? I’ll send Chinese, & u will translate to US-style English with abbreviations, no period at the end. Keep the full meaning & question marks for questions. Use fluent and sophisticated English to ensure the expression is polished, articulate, and of a high linguistic standard.Only use letter-based abbreviations like "u" for "you," "ur" for "your," "r" for "are," "thx" for "thanks," & but ensure the language still reflects a high English level with polished & sophisticated word choices, got it? Use & for And. U r absolutely forbidden from using numbers in abbreviations—no "2" for "to," no "4" for "for," no "b4" for "before," or any number ever! Only use letters: "to," "for," "bfr" for "before," "frst" for "first," "tmrw" for "tomorrow," "nxt" for "next." U must double-check ur output to ensure no numbers like "2" or "4" appear in abbreviations, replacing them with "to" or "for," u can't use number abbreviations in English words, clear?

Chinese text to translate:
{text_to_translate}`;

    // Selectors
    const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
    const SEND_BUTTON_SELECTOR = 'button.btn-send'; // 请根据需要检查并调整此选择器

    // Input Translation Overlay (For status/error feedback)
    const INPUT_OVERLAY_ID = 'custom-input-translate-overlay';

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

    // State Variables
    let inputTranslationOverlayElement = null;
    let currentInputApiXhr = null;
    let isTranslatingAndSending = false; // Flag to prevent conflicts/loops
    let sendButtonClickListenerAttached = false; // Track if click listener is attached

    // --- CSS Styles (Only for Overlay) ---
    GM_addStyle(`
        #${INPUT_OVERLAY_ID} { /* ... Overlay styles unchanged ... */ position: absolute; bottom: 100%; left: 10px; right: 10px; background-color: rgba(30, 30, 30, 0.9); backdrop-filter: blur(3px); border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none; padding: 4px 8px; font-size: 13px; color: #e0e0e0; border-radius: 6px 6px 0 0; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); z-index: 150; display: none; max-height: 60px; overflow-y: auto; line-height: 1.3; text-align: left; }
        #${INPUT_OVERLAY_ID}.visible { display: block; }
        #${INPUT_OVERLAY_ID} .status { font-style: italic; color: #aaa; }
        #${INPUT_OVERLAY_ID} .error { font-weight: bold; color: #ff8a8a; }
    `);

    // --- 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) { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); element.focus(); }
    function ensureInputOverlayExists(inputMainContainer) { if (!inputMainContainer) return; if (!inputTranslationOverlayElement || !document.body.contains(inputTranslationOverlayElement)) { inputTranslationOverlayElement = document.createElement('div'); inputTranslationOverlayElement.id = INPUT_OVERLAY_ID; inputMainContainer.style.position = 'relative'; inputMainContainer.appendChild(inputTranslationOverlayElement); console.log("[InputTranslate] Overlay element created."); } }
    function updateInputOverlay(content, type = 'status', duration = 0) { if (!inputTranslationOverlayElement) { const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main'); ensureInputOverlayExists(inputContainer); if(!inputTranslationOverlayElement) return; } inputTranslationOverlayElement.innerHTML = `<span class="${type}">${content}</span>`; inputTranslationOverlayElement.classList.add('visible'); inputTranslationOverlayElement.scrollTop = inputTranslationOverlayElement.scrollHeight; if (duration > 0) { setTimeout(hideInputOverlay, duration); } }
    function hideInputOverlay() { if (inputTranslationOverlayElement) { inputTranslationOverlayElement.classList.remove('visible'); inputTranslationOverlayElement.textContent = ''; } }

    // --- Shared Translate -> Replace -> Send Logic ---
    function translateAndSend(originalText, inputElement, sendButton) {
        if (isTranslatingAndSending) {
            console.warn("[InputTranslate] Already processing, ignoring translateAndSend call.");
            return;
        }
        if (!inputElement || !sendButton) {
            console.error("[InputTranslate] Input element or send button missing in translateAndSend.");
            return;
        }

        isTranslatingAndSending = true;
        hideInputOverlay(); // Clear previous status
        updateInputOverlay("翻译中...", 'status');

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

        console.log(`[InputTranslate] Calling API (${INPUT_TRANSLATE_MODEL}) for translateAndSend`);

        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 {
                    const data = JSON.parse(response.responseText);
                    const translation = data.choices?.[0]?.message?.content?.trim();
                    if (translation) {
                        console.log("[InputTranslate] Success:", translation);
                        inputElement.textContent = translation; // Replace content
                        setCursorToEnd(inputElement);          // Move cursor
                        inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); // Trigger input event

                        // Use a small delay before clicking send
                        setTimeout(() => {
                            console.log("[InputTranslate] Programmatically clicking send button.");
                            sendButton.click();
                            hideInputOverlay(); // Clear "翻译中..."
                            isTranslatingAndSending = false; // Reset flag *after* initiating send
                        }, 150); // Increased delay slightly

                    } else {
                        let errorMsg = data.error?.message || "API返回空内容";
                        throw new Error(errorMsg); // Treat as error
                    }
                } catch (e) {
                    console.error("[InputTranslate] API/Parse Error:", e);
                    updateInputOverlay(`翻译失败: ${e.message.substring(0, 50)}`, 'error', 4000);
                    isTranslatingAndSending = false; // Reset flag on error
                }
            },
            onerror: function(response) { /* ... error handling ... */ currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateInputOverlay(`翻译失败: 网络错误 (${response.status})`, 'error', 4000); isTranslatingAndSending = false; },
            ontimeout: function() { /* ... error handling ... */ currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateInputOverlay("翻译失败: 请求超时", 'error', 4000); isTranslatingAndSending = false; },
            onabort: function() { /* ... error handling ... */ currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); /* Don't reset flag here */ },
            timeout: 30000
        });
    }

    // --- Event Listeners ---
    function handleInputKeyDown(event) {
        if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) {
            if (isTranslatingAndSending) {
                console.log("[InputTranslate][Enter] Ignored, already processing.");
                event.preventDefault();
                event.stopPropagation();
                return;
            }

            const inputElement = event.target;
            const text = inputElement.textContent?.trim() || "";
            const detectedLang = detectLanguage(text);

            if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
                console.log(`[InputTranslate][Enter] Detected ${detectedLang}. Translating & sending...`);
                event.preventDefault(); // <<< PREVENT default Enter action (sending original)
                event.stopPropagation();

                const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
                if (!sendButton) {
                    updateInputOverlay("错误: 未找到发送按钮!", 'error', 5000);
                    return;
                }
                translateAndSend(text, inputElement, sendButton); // Use the shared function
            } else {
                console.log(`[InputTranslate][Enter] Allowing normal send for ${detectedLang || 'empty'}.`);
                hideInputOverlay();
                // Allow default action (send original text or do nothing if empty)
            }
        } else {
             if (!['Shift', 'Control', 'Alt', 'Meta', 'Enter'].includes(event.key)) {
                  hideInputOverlay();
                  if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') {
                      currentInputApiXhr.abort();
                      isTranslatingAndSending = false; // Reset if user types other keys during processing
                  }
             }
        }
    }

    function handleSendButtonClick(event) {
         // Check if the click target *is* the send button we are tracking
         const sendButton = event.target.closest(SEND_BUTTON_SELECTOR);
         if (!sendButton) {
             return; // Click was not on the send button or its child
         }

         if (isTranslatingAndSending) {
            console.log("[InputTranslate][Click] Ignored, already processing.");
            // Important: Don't prevent default here, allow the *second*, programmatic click to go through
            return;
        }

        const inputElement = document.querySelector(INPUT_SELECTOR);
        if (!inputElement) {
             console.error("[InputTranslate][Click] Input element not found.");
             return; // Allow default click action? Or prevent? Better allow.
        }

        const text = inputElement.textContent?.trim() || "";
        const detectedLang = detectLanguage(text);

        if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
            console.log(`[InputTranslate][Click] Detected ${detectedLang}. Translating & sending...`);
            event.preventDefault(); // <<< PREVENT the *first* default click action (sending original)
            event.stopPropagation();

            translateAndSend(text, inputElement, sendButton); // Use the shared function
        } else {
            console.log(`[InputTranslate][Click] Allowing normal send for ${detectedLang || 'empty'}.`);
            hideInputOverlay();
            // Allow default click action (send original text or do nothing if empty)
        }
    }

     function handleInputBlur(event) {
         // Optional: More advanced blur handling if needed
         // setTimeout(() => { /* ... Check activeElement ... */ }, 200);
     }


    // --- Initialization & Attaching Listeners ---
    function initialize() {
        console.log("[Telegram Input Translator v2.1] Initializing...");

        const inputElement = document.querySelector(INPUT_SELECTOR);

        // Attach listener to input field for Enter key
        if (inputElement && !inputElement.dataset.customInputTranslateListener) {
            console.log("[Telegram Input Translator] Attaching Keydown listener to input field.");
            inputElement.addEventListener('keydown', handleInputKeyDown, true); // Use capture
            // inputElement.addEventListener('blur', handleInputBlur); // Blur listener might be less critical now
            inputElement.dataset.customInputTranslateListener = 'true';
            const inputContainer = inputElement.closest('.chat-input-main');
            ensureInputOverlayExists(inputContainer);
        } else if (!inputElement) {
             console.log("[Telegram Input Translator] Input field not found yet, retrying init...");
             setTimeout(initialize, 1500); // Retry initialization
             return;
        }

        // Attach listener to Send Button (using periodic check for simplicity)
        // A MutationObserver would be more efficient but adds complexity
        if (!sendButtonClickListenerAttached) {
             setInterval(() => {
                 const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
                 // Check if button exists and listener is not already attached (using a data attribute)
                 if (sendButton && !sendButton.dataset.customSendClickListener) {
                     console.log("[Telegram Input Translator] Attaching Click listener to Send button.");
                     // Use capture phase for the click listener too, to intercept early
                     sendButton.addEventListener('click', handleSendButtonClick, true);
                     sendButton.dataset.customSendClickListener = 'true';
                     sendButtonClickListenerAttached = true; // Mark as attached globally (though interval continues)
                 } else if (!sendButton && sendButtonClickListenerAttached) {
                     // If button disappears, reset flag so we re-attach if it reappears
                     console.log("[Telegram Input Translator] Send button lost, listener flag reset.");
                     sendButtonClickListenerAttached = false;
                 }
             }, 1000); // Check every second
        }


        console.log("[Telegram Input Translator v2.1] Initialization complete. Ready.");
    }

    // Wait for the UI
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 2000));
    } else {
        setTimeout(initialize, 2000);
    }

})();