Telegram 输入框翻译并发送 (v2.2 - 新翻译风格)

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

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

// ==UserScript==
// @name         Telegram 输入框翻译并发送 (v2.2 - 新翻译风格)
// @namespace    http://tampermonkey.net/
// @version      2.2
// @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"; // 输入框翻译模型

    // --- NEW TRANSLATION PROMPT ---
    const TRANSLATION_PROMPT = `Act as a professional translator. Your task is to translate the user's text according to these rules:
1.  **Target Language & Style:** Translate into authentic, standard American English.
2.  **Tone:** Use a gentle, kind, and polite tone. Aim for natural, conversational warmth often associated with polite female speech, but maintain standard grammar and avoid slang or overly casual abbreviations.
3.  **Fluency:** Ensure the translation sounds natural and fluent, avoiding any stiffness or "machine translation" feel.
4.  **Punctuation:**
    *   Do NOT end sentences with a period (.).
    *   RETAIN the question mark (?) if the original is a question.
5.  **Output:** Provide ONLY the final translated text. No explanations, introductions, or labels.

Text to translate:
{text_to_translate}`;

    // Selectors
    const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
    const SEND_BUTTON_SELECTOR = 'button.btn-send'; // Please double-check this selector in Telegram Web UI if needed

    // 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} { 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'; /* Ensure container allows absolute positioning */ inputMainContainer.appendChild(inputTranslationOverlayElement); console.log("[InputTranslate] Overlay element created."); } }
    function updateInputOverlay(content, type = 'status', duration = 0) { const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main'); if (!inputTranslationOverlayElement && inputContainer) { ensureInputOverlayExists(inputContainer); } if(!inputTranslationOverlayElement) { console.warn("[InputTranslate] Could not find/create overlay."); 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.");
            updateInputOverlay("错误: 输入框或发送按钮丢失", 'error', 4000);
            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.7 }; // Slightly higher temp for potentially more natural tone variation

        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 {
                    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] 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(() => {
                                 if (!isTranslatingAndSending) { // Check if aborted by user typing
                                      console.log("[InputTranslate] Sending aborted before programmatic click.");
                                      return;
                                 }
                                 console.log("[InputTranslate] Programmatically clicking send button.");
                                 sendButton.click();
                                 hideInputOverlay(); // Clear "翻译中..."
                                 isTranslatingAndSending = false; // Reset flag *after* initiating send
                             }, 150); // Delay before click

                         } else {
                             let errorMsg = data.error?.message || "API返回空内容";
                             console.error("[InputTranslate] API Error (Empty Content):", response.responseText);
                             throw new Error(errorMsg);
                         }
                    } else {
                         console.error("[InputTranslate] API Error (Status):", response.status, response.statusText, response.responseText);
                         let errorDetail = `HTTP ${response.status}: ${response.statusText}`;
                         try {
                             const errData = JSON.parse(response.responseText);
                             errorDetail = errData.error?.message || errorDetail;
                         } catch (e) { /* ignore parse error */ }
                         throw new Error(errorDetail);
                    }
                } catch (e) {
                    console.error("[InputTranslate] API/Parse Error:", e);
                    updateInputOverlay(`翻译失败: ${e.message.substring(0, 60)}`, 'error', 5000);
                    isTranslatingAndSending = false; // Reset flag on error
                }
            },
            onerror: function(response) { currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateInputOverlay(`翻译失败: 网络错误 (${response.status || 'N/A'})`, 'error', 4000); isTranslatingAndSending = false; },
            ontimeout: function() { currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateInputOverlay("翻译失败: 请求超时", 'error', 4000); isTranslatingAndSending = false; },
            onabort: function() { currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); hideInputOverlay(); isTranslatingAndSending = false; }, // Also reset flag on abort
            timeout: 30000 // 30 seconds
        });
    }

    // --- Event Listeners ---
    function handleInputKeyDown(event) {
        const inputElement = event.target;

        // Handle Enter Key
        if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) {
            if (isTranslatingAndSending) {
                console.log("[InputTranslate][Enter] Ignored, already processing.");
                event.preventDefault(); // Prevent potential duplicate processing
                event.stopPropagation();
                return;
            }

            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 || sendButton.disabled) { // Check if button exists and is enabled
                    updateInputOverlay("错误: 发送按钮不可用!", 'error', 5000);
                    // Optionally abort if button is missing/disabled
                    // isTranslatingAndSending = false; // Already false here
                    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)
            }
        }
        // Handle other key presses (abort translation if user types more)
        else if (!['Shift', 'Control', 'Alt', 'Meta', 'Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
             // If a translation is in progress and user types something else, abort it
             if (isTranslatingAndSending && currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') {
                  console.log("[InputTranslate] User typed, aborting translation.");
                  currentInputApiXhr.abort(); // Abort the API call
                  // updateInputOverlay("翻译已取消", 'status', 1500); // Optional feedback
                  // isTranslatingAndSending = false; // Flag reset is handled by onabort
             } else {
                  // If not translating, hide any previous overlay messages on new input
                  hideInputOverlay();
             }
        }
    }

    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
         }

         // Check if this click is the *programmatic* one we trigger after translation
         // If isTranslatingAndSending is true *when the handler starts*, it means this might be the FIRST click
         // that INITIATED the translation. The SECOND, programmatic click happens *after* isTranslatingAndSending is set back to false (or should).
         // However, due to timing, let's refine the logic:
         // We only want to intercept the *first* manual click if translation is needed.
         // If translateAndSend was called, it sets isTranslatingAndSending=true.
         // If the programmatic click happens while that flag is still true (unlikely with delay, but possible), we should let it pass.

         // Let's re-evaluate: The core issue is preventing the ORIGINAL text from sending via the FIRST click.
         // The `isTranslatingAndSending` flag helps prevent *starting* a new translation while one is running.

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

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

         // Check if translation is needed AND we are not *already* in the middle of processing this specific text
         if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
             // If we are already translating (e.g., Enter was pressed just before click), prevent default but don't start another translation
             if (isTranslatingAndSending) {
                 console.log("[InputTranslate][Click] Intercepted, translation already in progress.");
                 event.preventDefault();
                 event.stopPropagation();
                 return;
             }

             // If not already translating, start the process and prevent the default send
             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 {
             // Allow normal send if language is not targeted or text is empty
             if (!isTranslatingAndSending) { // Only log if not currently processing
                 console.log(`[InputTranslate][Click] Allowing normal send for ${detectedLang || 'empty'}.`);
                 hideInputOverlay();
             }
             // Allow default click action
         }
    }


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

        let inputElement = document.querySelector(INPUT_SELECTOR);

        function attachInputListeners() {
             inputElement = document.querySelector(INPUT_SELECTOR);
             if (inputElement && !inputElement.dataset.customInputTranslateListener) {
                 console.log("[Telegram Input Translator] Attaching Keydown listener to input field.");
                 inputElement.addEventListener('keydown', handleInputKeyDown, true); // Use capture phase
                 inputElement.dataset.customInputTranslateListener = 'true';
                 const inputContainer = inputElement.closest('.chat-input-main');
                 if (inputContainer) {
                     ensureInputOverlayExists(inputContainer);
                 } else {
                     console.warn("[InputTranslate] Could not find '.chat-input-main' container for overlay.");
                     // Attempt to attach overlay later if needed
                 }
                 return true; // Listener attached
             }
             return false; // Not attached
        }

        function attachSendButtonListener() {
             const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
             if (sendButton && !sendButton.dataset.customSendClickListener) {
                 console.log("[Telegram Input Translator] Attaching Click listener to Send button.");
                 sendButton.addEventListener('click', handleSendButtonClick, true); // Use capture phase
                 sendButton.dataset.customSendClickListener = 'true';
                 sendButtonClickListenerAttached = true; // Mark globally
                 return true; // Listener attached
             }
             if (!sendButton && sendButtonClickListenerAttached) {
                  console.log("[Telegram Input Translator] Send button lost, listener flag reset.");
                  sendButtonClickListenerAttached = false; // Reset if button disappears
             }
             return sendButtonClickListenerAttached; // Return current status
        }

        // Initial attempt
        let inputReady = attachInputListeners();
        let buttonReady = attachSendButtonListener();

        // If not immediately found, use MutationObserver for robustness
        if (!inputReady || !buttonReady) {
            console.log("[Telegram Input Translator] Input or Send button not found immediately, setting up observer...");
            const observer = new MutationObserver((mutationsList, observer) => {
                if (!inputReady) {
                    inputReady = attachInputListeners();
                }
                if (!buttonReady) {
                    buttonReady = attachSendButtonListener();
                }
                // If both are found, no need to observe anymore (though Telegram might rebuild UI)
                // Keeping observer active is safer for dynamic UIs like Telegram Web A/K
                // if (inputReady && buttonReady) {
                //     console.log("[Telegram Input Translator] Both listeners attached via observer.");
                //     observer.disconnect();
                // }

                 // Ensure overlay exists if input is ready
                 if(inputReady && !inputTranslationOverlayElement) {
                     const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main');
                      if (inputContainer) ensureInputOverlayExists(inputContainer);
                 }
            });

            observer.observe(document.body, { childList: true, subtree: true });
            console.log("[Telegram Input Translator] Observer active.");
        } else {
             console.log("[Telegram Input Translator] Initial listeners attached successfully.");
        }

        console.log("[Telegram Input Translator v2.2] Initialization sequence complete. Monitoring for elements.");
    }

    // Wait for the UI - use a slight delay after DOMContentLoaded or run directly if already loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1500)); // Delay after DOM ready
    } else {
        setTimeout(initialize, 1500); // Delay even if already loaded
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址