您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
按回车或点击发送按钮时,翻译输入框内容(中/缅->指定风格英文)并自动替换和发送。拦截中文/缅甸语原文发送。新风格:温柔女性化美式英语。
当前为
// ==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 } })();