您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
v3.1.2基础: 更换模型为Gemini, 更新翻译提示词以更好地处理缅甸语和格式要求, 区分标准和缩写模式的处理流程。
// ==UserScript== // @name Telegram 输入框翻译并发送 (v3.9.1 - Gemini模型中文语优化) // @namespace http://tampermonkey.net/ // @version 3.9.1 // @description v3.1.2基础: 更换模型为Gemini, 更新翻译提示词以更好地处理缅甸语和格式要求, 区分标准和缩写模式的处理流程。 // @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.lhfcb.com // @icon https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Telegram_logo.svg/48px-Telegram_logo.svg.png // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const SCRIPT_VERSION = '3.2.0'; // << MODIFIED v3.2.0 >> 脚本版本 const OHMYGPT_API_KEY = "sk-1zm9YotucF60cHkzLgf9fDHHU9qAGAqzwGv4N7cLkJVfl0rU"; // 如有不同,请替换为你的 API Key (如果需要,根据新模型调整) const OHMYGPT_API_ENDPOINT = "https://api.lhfcb.com/v1/chat/completions"; // 保持不变,假设此端点是代理或支持Gemini const INPUT_TRANSLATE_MODEL = "gemini-2.5-flash-preview-05-20-nothinking"; // << MODIFIED v3.2.0 >> 使用的模型 const MAX_CACHE_SIZE = 100; // 最大缓存条目数 const STORAGE_KEY_AUTOSEND = 'telegramTranslateAutoSendPref'; // 自动发送设置的 localStorage 键名 const STORAGE_KEY_MODE = 'telegramTranslateModePref'; // 翻译模式设置的 localStorage 键名 // --- 翻译模式常量 --- const MODE_ABBREVIATED = 'abbreviated'; const MODE_STANDARD = 'standard'; // --- << MODIFIED v4.1 >> 翻译提示词 (Parser-Safe Version) --- // 基础翻译提示词 (用于API调用,缩写模式在此基础上进行脚本后处理) const TRANSLATION_PROMPT = ` Role: Expert Linguist & Cultural Consultant Task: Translate the following Chinese text into high-fidelity, natural-sounding American English, adhering to a strict set of principles and rules. // --- Core Principles for High-Quality Translation --- // Before executing, internalize these guiding principles for handling Chinese. 1. Context-First, Anti-Literalism: Chinese is a high-context language. First, understand the complete intent, subtext, and logic of the source text. Your primary goal is to translate this *meaning*, not just the individual words. 2. Master Sentence Restructuring: Actively transform Chinese sentence structures into idiomatic English. * Topic-Comment -> SVO: Convert phrases like 'zhe jian yifu wo xihuan' (literally "this clothing, I like") into "I like this piece of clothing." * Add Implied Subjects: Chinese often omits subjects. Infer and add them. For example, 'zhi dao le' becomes "I understand" or "Got it." * Bridge Logical Gaps: Chinese uses implicit logic (known as 'yihe'). Add English conjunctions (because, so, while, which) to make the relationship between clauses explicit and clear. 3. Translate Cultural Nuance, Not Just Words: * Idioms ('chengyu'): Translate the underlying meaning. The idiom 'hua she tian zu' should become "to gild the lily" or "to do something superfluous," not "draw a snake and add feet." * Polite Phrases ('ketaohua'): Translate the social function. 'Xin ku le' is not "you've worked hard"; it's "Thank you for your hard work" or "I appreciate your efforts." 'Mafan ni le' is "Thank you for your help" or "Sorry to bother you." * Formality: Distinguish between the formal 'you' (pinyin: nin) and the casual 'you' (pinyin: ni) by adjusting the English tone (e.g., using "sir/ma'am," more structured sentences vs. a relaxed style). // --- Strict Execution Rules --- // After considering the principles above, apply these rules without deviation. 1. Language Detection: Prioritize translating Chinese text. If mixed with untranslatable content (code, IDs, numbers, proper nouns), translate the Chinese parts and integrate the rest seamlessly into the English sentence. 2. Formality: Maintain the original text's level of formality or informality as determined by the principles above. 3. Untranslatable Content: If parts of the input are untranslatable, keep them as they are within the translated sentence structure. 4. Fully Untranslatable Input: If the entire input is untranslatable (e.g., just numbers, code, already valid English, emojis only), return the original text unmodified. 5. Punctuation: DO NOT add a period (.) at the end of the translated sentence. However, if the original text ends with a different punctuation mark like a question mark (?) or an exclamation mark (!), you MUST preserve it. Ensure correct spacing. 6. Output: Return ONLY the final translated text. NO explanations, NO notes, NO apologies, NO introductory phrases like "Here is the translation:". Just the resulting text. Input Text: {text_to_translate} `; // --- 选择器 (保持不变) --- 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 元素 ID (保持不变) --- const STATUS_BAR_ID = 'custom-input-status-bar'; const CONTROLS_CONTAINER_ID = 'custom-input-controls-container'; const AUTO_SEND_TOGGLE_ID = 'custom-auto-send-toggle'; const MODE_SELECTOR_CONTAINER_ID = 'custom-mode-selector'; const MODE_BUTTON_ABBR_ID = 'custom-mode-button-abbr'; const MODE_BUTTON_STD_ID = 'custom-mode-button-std'; const RETRY_BUTTON_ID = 'custom-translate-retry-button'; const RETRY_PROCESSING_BUTTON_ID = 'custom-processing-retry-button'; // --- 语言检测正则 (保持不变) --- const CHINESE_REGEX = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/; const BURMESE_REGEX = /[\u1000-\u109F]/; // 缅甸语 Unicode 范围 // --- 状态变量 (保持不变) --- let statusBarElement = null; let controlsContainerElement = null; let autoSendToggleElement = null; let modeSelectorContainerElement = null; let currentInputApiXhr = null; let isTranslatingAndSending = false; let sendButtonClickListenerAttached = false; let lastOriginalText = null; const translationCache = new Map(); let justTranslated = false; // --- 自动发送状态 (保持不变) --- let autoSendEnabled = true; const savedAutoSendState = localStorage.getItem(STORAGE_KEY_AUTOSEND); if (savedAutoSendState !== null) { autoSendEnabled = savedAutoSendState === 'true'; console.log(`[输入翻译 v${SCRIPT_VERSION}] 已加载自动发送偏好: ${autoSendEnabled ? '开启' : '关闭'}`); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}] 未找到自动发送偏好,使用默认值: ${autoSendEnabled ? '开启' : '关闭'}`); } // --- 翻译模式状态 (保持不变) --- let currentTranslationMode = MODE_ABBREVIATED; // 默认缩写模式 const savedModeState = localStorage.getItem(STORAGE_KEY_MODE); if (savedModeState === MODE_STANDARD || savedModeState === MODE_ABBREVIATED) { currentTranslationMode = savedModeState; console.log(`[输入翻译 v${SCRIPT_VERSION}] 已加载翻译模式偏好: ${currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'}`); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}] 未找到翻译模式偏好,使用默认值: 缩写`); } // --- CSS 样式 (保持不变) --- GM_addStyle(` ${INPUT_AREA_CONTAINER_SELECTOR} { position: relative !important; overflow: visible !important; } #${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 { flex-grow: 1; margin-right: 8px; } #${STATUS_BAR_ID} .status-buttons { display: flex; gap: 5px; flex-shrink: 0; } #${STATUS_BAR_ID} .status { font-style: italic; color: #a0a0a0; } #${STATUS_BAR_ID} .info { font-style: italic; color: #87cefa; } #${STATUS_BAR_ID} .error { font-weight: bold; color: #ff8a8a; } #${STATUS_BAR_ID} .success { font-weight: bold; color: #8ade8a; } #${RETRY_BUTTON_ID}, #${RETRY_PROCESSING_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; transition: background-color 0.2s ease, color 0.2s ease; white-space: nowrap; } #${RETRY_BUTTON_ID}:hover, #${RETRY_PROCESSING_BUTTON_ID}:hover { background-color: rgba(100, 100, 100, 0.9); color: #fff; } #${RETRY_BUTTON_ID}:active, #${RETRY_PROCESSING_BUTTON_ID}:active { background-color: rgba(60, 60, 60, 0.9); } #${CONTROLS_CONTAINER_ID} { position: absolute; top: 0; right: 10px; display: flex; align-items: flex-end; gap: 0px; z-index: 151; pointer-events: none; height: 26px; } #${MODE_SELECTOR_CONTAINER_ID} { display: flex; margin-right: 5px; pointer-events: auto; border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none; border-radius: 6px 6px 0 0; overflow: hidden; background-color: rgba(80, 80, 80, 0.9); } #${MODE_BUTTON_ABBR_ID}, #${MODE_BUTTON_STD_ID} { padding: 4px 8px; font-size: 12px; font-weight: bold; color: #ccc; background-color: transparent; border: none; cursor: pointer; user-select: none; transition: background-color 0.2s ease, color 0.2s ease; line-height: 16px; height: 24px; box-sizing: border-box; } #${MODE_BUTTON_ABBR_ID}.active, #${MODE_BUTTON_STD_ID}.active { background-color: rgba(70, 130, 180, 0.95); color: #fff; } #${MODE_BUTTON_ABBR_ID}:hover:not(.active), #${MODE_BUTTON_STD_ID}:hover:not(.active) { background-color: rgba(100, 100, 100, 0.9); } #${AUTO_SEND_TOGGLE_ID} { 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; pointer-events: auto; line-height: 16px; height: 24px; box-sizing: border-box; } #${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); } `); // --- 辅助函数 (大部分保持不变) --- function detectLanguage(text) { if (!text) return null; if (BURMESE_REGEX.test(text)) return 'Burmese'; if (CHINESE_REGEX.test(text)) return 'Chinese'; 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(`[输入翻译 v${SCRIPT_VERSION}] 设置光标时出错:`, e); } } function ensureControlsExist() { const inputMainContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR); if (!inputMainContainer) return; if (window.getComputedStyle(inputMainContainer).position !== 'relative') { inputMainContainer.style.position = 'relative'; } if (!controlsContainerElement || !inputMainContainer.contains(controlsContainerElement)) { controlsContainerElement = document.createElement('div'); controlsContainerElement.id = CONTROLS_CONTAINER_ID; inputMainContainer.appendChild(controlsContainerElement); console.log(`[输入翻译 v${SCRIPT_VERSION}] 控制按钮容器已创建。`); } if (!modeSelectorContainerElement || !controlsContainerElement.contains(modeSelectorContainerElement)) { modeSelectorContainerElement = document.createElement('div'); modeSelectorContainerElement.id = MODE_SELECTOR_CONTAINER_ID; const abbrButton = document.createElement('button'); abbrButton.id = MODE_BUTTON_ABBR_ID; abbrButton.textContent = '缩写'; abbrButton.type = 'button'; abbrButton.addEventListener('click', () => switchMode(MODE_ABBREVIATED)); modeSelectorContainerElement.appendChild(abbrButton); const stdButton = document.createElement('button'); stdButton.id = MODE_BUTTON_STD_ID; stdButton.textContent = '标准'; stdButton.type = 'button'; stdButton.addEventListener('click', () => switchMode(MODE_STANDARD)); modeSelectorContainerElement.appendChild(stdButton); controlsContainerElement.insertBefore(modeSelectorContainerElement, controlsContainerElement.firstChild); updateModeButtonVisuals(); console.log(`[输入翻译 v${SCRIPT_VERSION}] 模式选择按钮已创建。`); } if (!autoSendToggleElement || !controlsContainerElement.contains(autoSendToggleElement)) { autoSendToggleElement = document.createElement('button'); autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID; autoSendToggleElement.type = 'button'; autoSendToggleElement.addEventListener('click', toggleAutoSend); controlsContainerElement.appendChild(autoSendToggleElement); updateAutoSendButtonVisual(); console.log(`[输入翻译 v${SCRIPT_VERSION}] 自动发送开关按钮已创建。`); } if (!statusBarElement || !inputMainContainer.contains(statusBarElement)) { statusBarElement = document.createElement('div'); statusBarElement.id = STATUS_BAR_ID; inputMainContainer.appendChild(statusBarElement); console.log(`[输入翻译 v${SCRIPT_VERSION}] 状态栏元素已创建。`); } } function updateStatusDisplay(content, type = 'status', duration = 0, showRetryButton = false, showRetryProcessingButton = false) { ensureControlsExist(); if (!statusBarElement) { console.error(`[输入翻译 v${SCRIPT_VERSION}] 更新状态时未找到状态栏元素。`); return; } let buttonsHtml = ''; if (showRetryButton && lastOriginalText) { buttonsHtml += `<button id="${RETRY_BUTTON_ID}" type="button">重试原文</button>`; } if (showRetryProcessingButton) { buttonsHtml += `<button id="${RETRY_PROCESSING_BUTTON_ID}" type="button">重试处理</button>`; } statusBarElement.innerHTML = `<span class="status-text ${type}">${content}</span>${buttonsHtml ? `<div class="status-buttons">${buttonsHtml}</div>` : ''}`; statusBarElement.classList.add('visible'); if (showRetryButton && lastOriginalText) { const retryBtn = statusBarElement.querySelector(`#${RETRY_BUTTON_ID}`); if (retryBtn) retryBtn.addEventListener('click', handleRetryOriginalClick); } if (showRetryProcessingButton) { const retryProcBtn = statusBarElement.querySelector(`#${RETRY_PROCESSING_BUTTON_ID}`); if (retryProcBtn) retryProcBtn.addEventListener('click', handleRetryProcessingClick); } if (statusBarElement.hideTimeout) clearTimeout(statusBarElement.hideTimeout); statusBarElement.hideTimeout = duration > 0 ? setTimeout(hideStatusDisplay, duration) : null; } function hideStatusDisplay() { 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); } } // --- << ADDED v3.5 >> 缩写处理函数 (保留Emoji) --- function performAbbreviationPreservingEmojis(inputElement, textToProcess) { // 1. 将表情符号<img>替换为占位符 const emojiMap = new Map(); let placeholderIndex = 0; const tempDiv = document.createElement('div'); tempDiv.innerHTML = inputElement.innerHTML; // --- FIX v3.5.1 START --- // First, find all emoji nodes without modifying the DOM tree during traversal. // This prevents the iterator from getting confused when the DOM is changed. const emojiNodes = []; const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_ELEMENT); let elementNode; while(elementNode = walker.nextNode()) { if (elementNode.tagName === 'IMG' && elementNode.classList.contains('emoji')) { emojiNodes.push(elementNode); } } // Now, iterate over the collected array of nodes and replace them. for (const emojiNode of emojiNodes) { const placeholder = `__EMOJI_PLACEHOLDER_${placeholderIndex++}__`; emojiMap.set(placeholder, emojiNode.outerHTML); const textNode = document.createTextNode(placeholder); if (emojiNode.parentNode) { emojiNode.parentNode.replaceChild(textNode, emojiNode); } } // --- FIX v3.5.1 END --- const textWithPlaceholders = tempDiv.textContent || ""; // 2. 应用缩写规则 let abbreviatedText = applyLetterAbbreviations(fixNumberAbbreviations(textWithPlaceholders)); // 3. 将占位符恢复为表情符号<img> // 为避免HTML注入,我们创建一个新div并逐个添加文本和表情节点 const resultFragment = document.createDocumentFragment(); let lastIndex = 0; const placeholderRegex = /__EMOJI_PLACEHOLDER_\d+__/g; let match; while ((match = placeholderRegex.exec(abbreviatedText)) !== null) { // 添加占位符之前的文本部分 if (match.index > lastIndex) { resultFragment.appendChild(document.createTextNode(abbreviatedText.substring(lastIndex, match.index))); } // 添加表情符号HTML const placeholder = match[0]; if (emojiMap.has(placeholder)) { const emojiHtml = emojiMap.get(placeholder); const tempContainer = document.createElement('span'); tempContainer.innerHTML = emojiHtml; resultFragment.appendChild(tempContainer.firstChild); } lastIndex = match.index + placeholder.length; } // 添加最后一个占位符之后的剩余文本 if (lastIndex < abbreviatedText.length) { resultFragment.appendChild(document.createTextNode(abbreviatedText.substring(lastIndex))); } // 4. 更新输入框内容并恢复光标 inputElement.innerHTML = ''; inputElement.appendChild(resultFragment); setCursorToEnd(inputElement); inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } // --- << MODIFIED v3.2.0 >> 缩写处理函数 (仅在缩写模式下由脚本调用) --- function fixNumberAbbreviations(text) { if (!text) return text; let originalText = text; // 严格应用提示词中提到的规则(示例,保持不变) text = text.replace(/\b2\b/gi, "to"); text = text.replace(/\b4\b/gi, "for"); text = text.replace(/\b(be?|b)4\b/gi, "before"); text = text.replace(/\b2day\b/gi, "today"); text = text.replace(/\b2nite\b/gi, "tonight"); text = text.replace(/\b2night\b/gi, "tonight"); text = text.replace(/\b2mrw\b/gi, "tomorrow"); text = text.replace(/\b2moro\b/gi, "tomorrow"); text = text.replace(/\bgr8\b/gi, "great"); text = text.replace(/\bl8r\b/gi, "later"); text = text.replace(/\bw8\b/gi, "wait"); text = text.replace(/\bh8\b/gi, "hate"); text = text.replace(/\bsk8\b/gi, "skate"); text = text.replace(/\bm8\b/gi, "mate"); if (text !== originalText) { console.log(`[输入翻译 v${SCRIPT_VERSION}][缩写后处理] 应用了数字/组合缩写修正: "${originalText}" -> "${text}"`); } return text; } function applyLetterAbbreviations(text) { if (!text) return text; text = text.replace(/\b[Tt]hank you\b/g, m => m.charAt(0) === 'T' ? 'Thx u' : 'thx u'); let originalText = text; let modifiedText = text; let initialCapitalizationApplied = false; let changesMade = false; // 严格应用提示词中提到的规则 (示例,保持不变) const abbrMap = { "you": "u", "your": "ur", "yours": "urs", "yourself": "urself", "are": "r", "thanks": "thx", "thank": "thx", "and": "&", "before": "bfr", "first": "frst", "tomorrow": "tmrw", "next": "nxt" }; const capitalizeAtStart = ["u", "ur", "urs","r", "thx", "bfr", "frst", "tmrw", "nxt", "urself"]; // (代码逻辑保持不变, 确保能正确应用缩写) let firstWordIndex = -1; let firstWord = ""; let leadingChars = ""; const match = modifiedText.match(/^(\s*[^a-zA-Z\s]*)?([a-zA-Z]+)/); if (match) { leadingChars = match[1] || ""; firstWord = match[2]; firstWordIndex = leadingChars.length; const lowerFirstWord = firstWord.toLowerCase(); if (abbrMap.hasOwnProperty(lowerFirstWord)) { const abbreviation = abbrMap[lowerFirstWord]; let replacementMade = false; if (capitalizeAtStart.includes(abbreviation)) { const capitalizedAbbr = abbreviation.charAt(0).toUpperCase() + abbreviation.slice(1); modifiedText = leadingChars + capitalizedAbbr + modifiedText.substring(firstWordIndex + firstWord.length); initialCapitalizationApplied = true; replacementMade = true; } else if (abbreviation === '&') { modifiedText = leadingChars + abbreviation + modifiedText.substring(firstWordIndex + firstWord.length); initialCapitalizationApplied = true; replacementMade = true; } // Handle '&' at start if (replacementMade) changesMade = true; } } const replaceRemaining = (fullWord, abbr) => { const regexLower = new RegExp(`\\b${fullWord}\\b`, 'g'); const regexUpper = new RegExp(`\\b${fullWord.charAt(0).toUpperCase() + fullWord.slice(1)}\\b`, 'g'); let startIndex = 0; if (initialCapitalizationApplied && firstWord.toLowerCase() === fullWord) { startIndex = firstWordIndex + (abbrMap[fullWord] ? abbrMap[fullWord].length : firstWord.length); } let targetStringPart = modifiedText.substring(startIndex); let prefix = modifiedText.substring(0, startIndex); let replacedPart = targetStringPart; let currentChangesMade = false; const originalLength = replacedPart.length; if (abbr === '&') { replacedPart = replacedPart.replace(/\b[Aa]nd\b/g, '&'); } else { replacedPart = replacedPart.replace(regexLower, abbr); replacedPart = replacedPart.replace(regexUpper, abbr); } if(replacedPart.length !== originalLength || replacedPart !== targetStringPart) currentChangesMade = true; modifiedText = prefix + replacedPart; if (currentChangesMade) changesMade = true; }; for (const word in abbrMap) { replaceRemaining(word, abbrMap[word]); } if (/^\s*&/.test(modifiedText)) { modifiedText = modifiedText.replace(/^(\s*)&/, '$1And'); } if (changesMade) { console.log(`[输入翻译 v${SCRIPT_VERSION}][缩写后处理] 应用了字母缩写。\n 输入: "${originalText}"\n 输出: "${modifiedText}"`); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}][缩写后处理] 未应用字母缩写。\n 输入: "${originalText}"`); } return modifiedText; } // --- 重试处理函数 (保持不变, 但日志会反映当前模式) --- function handleRetryOriginalClick(event) { event.preventDefault(); event.stopPropagation(); console.log(`[输入翻译 v${SCRIPT_VERSION}] "重试原文"按钮被点击。`); if (isTranslatingAndSending) { console.warn(`[输入翻译 v${SCRIPT_VERSION}] 正在处理中,忽略"重试原文"点击。`); return; } if (!lastOriginalText) { console.warn(`[输入翻译 v${SCRIPT_VERSION}] 没有存储原文可供重试。`); hideStatusDisplay(); return; } const inputElement = document.querySelector(INPUT_SELECTOR); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, true); return; } if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, true); return; } const currentModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; // << 获取当前模式文本 console.log(`[输入翻译 v${SCRIPT_VERSION}] 正在使用 [${currentModeText}] 模式重试原文翻译: "${lastOriginalText}"`); translateAndSend(lastOriginalText, inputElement, sendButton, true); // forceApi = true } function handleRetryProcessingClick(event) { event.preventDefault(); event.stopPropagation(); console.log(`[输入翻译 v${SCRIPT_VERSION}] "重试处理"按钮被点击。`); if (isTranslatingAndSending) { console.warn(`[输入翻译 v${SCRIPT_VERSION}] 正在处理中,忽略"重试处理"点击。`); return; } const inputElement = document.querySelector(INPUT_SELECTOR); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, true); return; } const currentText = inputElement.textContent?.trim(); if (!currentText) { console.warn(`[输入翻译 v${SCRIPT_VERSION}] 输入框为空,无法重试处理。`); hideStatusDisplay(); return; } if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, true); return; } const currentModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; // << 获取当前模式文本 console.log(`[输入翻译 v${SCRIPT_VERSION}] 正在使用 [${currentModeText}] 模式对当前文本重试处理: "${currentText}"`); translateAndSend(currentText, inputElement, sendButton, true); // forceApi = true } // --- UI 控制函数 (保持不变) --- function updateAutoSendButtonVisual() { if (!autoSendToggleElement) return; autoSendToggleElement.textContent = autoSendEnabled ? "自动:开" : "自动:关"; autoSendToggleElement.className = autoSendEnabled ? 'autosend-on' : ''; } function toggleAutoSend() { autoSendEnabled = !autoSendEnabled; const statusText = autoSendEnabled ? '开启' : '关闭'; console.log(`[输入翻译 v${SCRIPT_VERSION}] 自动发送切换为: ${statusText}`); updateAutoSendButtonVisual(); updateStatusDisplay(`自动发送已${statusText}`, 'status', 2000); try { localStorage.setItem(STORAGE_KEY_AUTOSEND, autoSendEnabled.toString()); console.log(`[输入翻译 v${SCRIPT_VERSION}] 已将自动发送偏好 (${statusText}) 保存到 localStorage。`); } catch (e) { console.error(`[输入翻译 v${SCRIPT_VERSION}] 保存自动发送偏好到 localStorage 时出错:`, e); updateStatusDisplay("无法保存自动发送设置", 'error', 3000); } } function updateModeButtonVisuals() { const abbrButton = document.getElementById(MODE_BUTTON_ABBR_ID); const stdButton = document.getElementById(MODE_BUTTON_STD_ID); if (!abbrButton || !stdButton) return; if (currentTranslationMode === MODE_ABBREVIATED) { abbrButton.classList.add('active'); stdButton.classList.remove('active'); } else { abbrButton.classList.remove('active'); stdButton.classList.add('active'); } } function switchMode(newMode) { if (newMode === currentTranslationMode) return; const oldModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; currentTranslationMode = newMode; const newModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; console.log(`[输入翻译 v${SCRIPT_VERSION}] 翻译模式切换为: ${newModeText}`); updateModeButtonVisuals(); updateStatusDisplay(`模式切换为: ${newModeText}`, 'status', 2000); try { localStorage.setItem(STORAGE_KEY_MODE, currentTranslationMode); console.log(`[输入翻译 v${SCRIPT_VERSION}] 已将翻译模式偏好 (${newModeText}) 保存到 localStorage。`); } catch (e) { console.error(`[输入翻译 v${SCRIPT_VERSION}] 保存翻译模式偏好到 localStorage 时出错:`, e); updateStatusDisplay("无法保存模式设置", 'error', 3000); } } // --- << MODIFIED v3.2.0 >> 主要翻译逻辑 (适配新提示词和模式处理) --- function translateAndSend(textToProcess, inputElement, sendButton, forceApi = false) { if (isTranslatingAndSending) { console.warn(`[输入翻译 v${SCRIPT_VERSION}] 已在处理中,忽略新的处理请求。`); return; } if (!inputElement || !sendButton) { updateStatusDisplay("错误: 无法找到输入框或发送按钮", 'error', 4000, true, true); return; } isTranslatingAndSending = true; hideStatusDisplay(); const detectedLang = detectLanguage(textToProcess); const currentModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; // 英文缩写仅模式 if (currentTranslationMode === MODE_ABBREVIATED && detectedLang !== 'Chinese' && detectedLang !== 'Burmese') { // << MODIFIED v3.5 >> 使用新函数处理缩写以保留表情符号 performAbbreviationPreservingEmojis(inputElement, textToProcess); updateStatusDisplay("替换缩写成功", 'success', 2000); showInlineStatus(inputElement, '替换缩写成功'); // 英文缩写模式始终自动发送 setTimeout(() => { justTranslated = true; if (sendButton && sendButton.isConnected && !sendButton.disabled) { sendButton.click(); } isTranslatingAndSending = false; hideStatusDisplay(); }, 50); return; } if (detectedLang === 'Chinese' || detectedLang === 'Burmese') { lastOriginalText = textToProcess; } // --- 缓存检查 --- const useCache = !forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese'); // << MODIFIED v3.2.0 >> 缓存键包含模式,因为后处理不同 const cacheKey = `${currentTranslationMode}::${textToProcess}`; if (useCache && translationCache.has(cacheKey)) { // 检查包含模式的缓存键 const cachedProcessedTranslation = translationCache.get(cacheKey); // 获取已处理过的翻译结果 console.log(`[输入翻译 v${SCRIPT_VERSION}][缓存命中] 找到原文 "${textToProcess.substring(0,30)}..." 在 [${currentModeText}] 模式下的缓存结果: "${cachedProcessedTranslation}"`); updateStatusDisplay(`[${currentModeText}] 已从缓存加载 ✓`, 'info', 3000, false, !autoSendEnabled); // 直接使用缓存中已经处理好的文本 const finalText = cachedProcessedTranslation; inputElement.textContent = finalText; setCursorToEnd(inputElement); inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); if (autoSendEnabled) { const sendDelay = 50; console.log(`[输入翻译 v${SCRIPT_VERSION}][缓存][自动发送] 自动发送已开启。将在 ${sendDelay}ms 后模拟点击发送。`); setTimeout(() => { if (!isTranslatingAndSending) { console.log(`[输入翻译 v${SCRIPT_VERSION}][缓存][发送超时] 发送已中止 (可能被新操作打断)。`); return; } if (sendButton && sendButton.isConnected && !sendButton.disabled) { console.log(`[输入翻译 v${SCRIPT_VERSION}][缓存][自动发送] 重置状态并尝试发送...`); isTranslatingAndSending = false; justTranslated = false; // 发送后重置 sendButton.click(); hideStatusDisplay(); } else { console.error(`[输入翻译 v${SCRIPT_VERSION}][缓存][自动发送] 发送失败,按钮不可用或已消失。`); updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); isTranslatingAndSending = false; } }, sendDelay); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}][缓存] 自动发送已关闭。`); updateStatusDisplay(`[${currentModeText}] 处理完成 ✓ (请手动发送)`, 'success', 0, true, true); // 保持状态栏直到用户操作 isTranslatingAndSending = false; justTranslated = true; // 标记为已翻译,等待手动发送 } return; // 缓存命中,结束函数 } // --- 缓存检查结束 --- // --- API 调用 --- // << MODIFIED v3.2.0 >> 使用统一的基础翻译提示词 const finalPrompt = TRANSLATION_PROMPT.replace('{text_to_translate}', textToProcess); console.log(`[输入翻译 v${SCRIPT_VERSION}] ${forceApi ? '强制 API 调用' : '缓存未命中'}。使用 [${currentModeText}] 模式调用 API (${INPUT_TRANSLATE_MODEL}) 处理: "${textToProcess.substring(0, 30)}..."`); updateStatusDisplay(`[${currentModeText}] 翻译处理中...`, 'status'); // << MODIFIED v3.2.0 >> model 使用新配置 const requestBody = { model: INPUT_TRANSLATE_MODEL, messages: [{"role": "user", "content": finalPrompt }], temperature: 0.6 }; // Temperature 可以根据模型调整 if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); console.log(`[输入翻译 v${SCRIPT_VERSION}] 中止了之前的 API 请求。`);} 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 rawTranslation = data.choices?.[0]?.message?.content?.trim(); if (rawTranslation) { console.log(`[输入翻译 v${SCRIPT_VERSION}][API 成功] 收到原始结果: "${rawTranslation}"`); let finalProcessedText; // << MODIFIED v3.2.0 >> 根据模式进行后处理 if (currentTranslationMode === MODE_ABBREVIATED) { console.log(`[输入翻译 v${SCRIPT_VERSION}][API 后处理] 应用缩写模式处理...`); finalProcessedText = applyLetterAbbreviations(fixNumberAbbreviations(rawTranslation)); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}][API 后处理] 标准模式,直接使用翻译结果。`); finalProcessedText = rawTranslation.trim(); // 提示词已要求不加句号,trim即可 } console.log(`[输入翻译 v${SCRIPT_VERSION}][API 处理后] 最终文本: "${finalProcessedText}"`); // << MODIFIED v3.2.0 >> 缓存处理后的结果,使用包含模式的键 if (!forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) { if (translationCache.size >= MAX_CACHE_SIZE) { const oldestKey = translationCache.keys().next().value; translationCache.delete(oldestKey); } translationCache.set(cacheKey, finalProcessedText); // 缓存处理后的结果 console.log(`[输入翻译 v${SCRIPT_VERSION}] 已缓存 [${currentModeText}] 模式处理结果: "${textToProcess.substring(0,30)}..." -> "${finalProcessedText}"`); } inputElement.textContent = finalProcessedText; setCursorToEnd(inputElement); inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); if (autoSendEnabled) { const sendDelay = 150; // API 延迟稍长 console.log(`[输入翻译 v${SCRIPT_VERSION}][API][自动发送] 自动发送已开启。将在 ${sendDelay}ms 后模拟点击发送。`); setTimeout(() => { if (!isTranslatingAndSending) { console.log(`[输入翻译 v${SCRIPT_VERSION}][API][发送超时] 发送已中止。`); return; } if (sendButton && sendButton.isConnected && !sendButton.disabled) { console.log(`[输入翻译 v${SCRIPT_VERSION}][API][自动发送] 重置状态并尝试发送...`); isTranslatingAndSending = false; justTranslated = false; // 发送后重置 sendButton.click(); hideStatusDisplay(); } else { console.error(`[输入翻译 v${SCRIPT_VERSION}][API][自动发送] 发送失败,按钮不可用或已消失。`); updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); isTranslatingAndSending = false; } }, sendDelay); } else { console.log(`[输入翻译 v${SCRIPT_VERSION}][API] 自动发送已关闭。`); updateStatusDisplay(`[${currentModeText}] 处理完成 ✓ (请手动发送)`, 'success', 0, true, true); // 保持状态栏直到用户操作 isTranslatingAndSending = false; justTranslated = true; // 标记为已翻译,等待手动发送 } } else { throw new Error(`API 返回空内容 (结束原因: ${data.choices?.[0]?.finish_reason || '未知'})`); } } else { let errorDetail = `HTTP ${response.status}: ${response.statusText}`; try { const errData = JSON.parse(response.responseText); errorDetail = errData.error?.message || errorDetail; } catch (e) { /* 忽略解析错误 */ } throw new Error(errorDetail); } } catch (e) { console.error(`[输入翻译 v${SCRIPT_VERSION}][API 错误] 处理 API 响应时出错:`, e); updateStatusDisplay(`处理失败: ${e.message.substring(0, 60)}`, 'error', 5000, true, true); isTranslatingAndSending = false; } }, onerror: function(response) { currentInputApiXhr = null; console.error(`[输入翻译 v${SCRIPT_VERSION}][网络错误] 请求失败:`, response); updateStatusDisplay(`处理失败: 网络错误 (${response.status || 'N/A'})`, 'error', 5000, true, true); isTranslatingAndSending = false; }, ontimeout: function() { currentInputApiXhr = null; console.error(`[输入翻译 v${SCRIPT_VERSION}][超时错误] API 请求超时。`); updateStatusDisplay("处理失败: 请求超时", 'error', 5000, true, true); isTranslatingAndSending = false; }, onabort: function() { currentInputApiXhr = null; console.log(`[输入翻译 v${SCRIPT_VERSION}] API 请求已中止。`); hideStatusDisplay(); isTranslatingAndSending = false; }, timeout: 45000 // << MODIFIED v3.2.0 >> 稍微增加超时时间以适应潜在较慢的模型 }); } // --- 事件监听器 (保持 v3.1.2 修复的手动发送逻辑) --- function handleInputKeyDown(event) { const inputElement = event.target; if (!inputElement || !inputElement.matches(INPUT_SELECTOR)) return; // 专为手动发送设计:如果刚刚翻译完成(自动发送关闭),第一次回车应该发送消息,而不是触发新的翻译。 if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey && justTranslated) { console.log(`[输入翻译 v${SCRIPT_VERSION}][回车] 检测到 justTranslated 标志,将直接发送已翻译内容。`); event.preventDefault(); event.stopPropagation(); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (sendButton && !sendButton.disabled) { sendButton.click(); } justTranslated = false; // 重置标志 hideStatusDisplay(); return; } // --- 主要回车键逻辑 --- if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) { // **核心改动:立即阻止默认的回车行为(发送消息或换行)。** // 这样,所有的发送逻辑都将由本脚本显式控制。 event.preventDefault(); event.stopPropagation(); // 如果正在翻译或处理中,则忽略本次回车,防止重复触发。 if (isTranslatingAndSending) { console.log(`[输入翻译 v${SCRIPT_VERSION}][回车] 正在处理中,已阻止并忽略本次回车。`); return; } const text = inputElement.textContent?.trim() || ""; const detectedLang = detectLanguage(text); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); // 检查发送按钮是否可用 if (!sendButton || sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, true); console.warn(`[输入翻译 v${SCRIPT_VERSION}][回车] 发送按钮不可用或未找到,操作中止。`); return; } // 判断是否需要翻译或处理 const needsProcessing = text && ( (detectedLang === 'Chinese' || detectedLang === 'Burmese') || (currentTranslationMode === MODE_ABBREVIATED && detectedLang !== 'Chinese' && detectedLang !== 'Burmese') ); if (needsProcessing) { // 如果文本需要翻译或处理 const currentModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; console.log(`[输入翻译 v${SCRIPT_VERSION}][回车] 检测到 ${detectedLang} 文本。将使用 [${currentModeText}] 模式处理...`); translateAndSend(text, inputElement, sendButton); // 调用翻译处理函数 } else { // 如果文本不需要翻译(例如,已经是英文且非缩写模式,或为空) console.log(`[输入翻译 v${SCRIPT_VERSION}][回车] 无需翻译的文本 ("${text.substring(0, 30)}...") 或空内容,将直接发送。`); hideStatusDisplay(); // 清除可能存在的旧状态 // 在发送前确保重置 justTranslated 标志 justTranslated = false; sendButton.click(); // 显式点击发送按钮 } } // --- 其他按键逻辑 (打断处理) --- else if (isTranslatingAndSending && !['Shift', 'Control', 'Alt', 'Meta', 'Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Escape', 'Tab'].includes(event.key)) { console.log(`[输入翻译 v${SCRIPT_VERSION}][输入打断] 检测到输入,中止当前处理... (按键: ${event.key})`); hideStatusDisplay(); if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); } else { isTranslatingAndSending = false; console.log(`[输入翻译 v${SCRIPT_VERSION}][输入打断] 已设置 isTranslatingAndSending 为 false。`); } } else if (!isTranslatingAndSending) { // 输入时隐藏非持久性状态 if (statusBarElement && statusBarElement.classList.contains('visible')) { const statusSpan = statusBarElement.querySelector('span.status'); const errorSpan = statusBarElement.querySelector('span.error'); const successSpan = statusBarElement.querySelector('span.success'); const infoSpan = statusBarElement.querySelector('span.info'); if (statusSpan && !errorSpan && !successSpan && !infoSpan) { hideStatusDisplay(); } } } } function handleSendButtonClick(event) { if (justTranslated) { console.log(`[输入翻译 v${SCRIPT_VERSION}][发送点击] 检测到 justTranslated 标志,允许默认发送行为。`); justTranslated = false; hideStatusDisplay(); return; // 允许默认的点击发送行为 } let sendButton = event.target.closest(SEND_BUTTON_SELECTOR); if (!sendButton) { const btn = event.target.closest('button'); if (btn && btn.textContent.trim().toUpperCase() === 'SEND') { sendButton = btn; } } if (!sendButton) return; // 查找输入元素:优先包含图片的预览容器中的 caption,fallback 到聊天输入框 let inputElement = null; let el = sendButton; while (el && el !== document.body) { const img = el.querySelector('img'); const caption = el.querySelector('div[contenteditable="true"]'); if (img && caption) { inputElement = caption; break; } el = el.parentElement; } if (!inputElement) { inputElement = document.querySelector(INPUT_SELECTOR); } if (!inputElement) return; // 无输入框, 跳过处理 const text = inputElement.textContent?.trim() || ""; const detectedLang = detectLanguage(text); // 英文缩写模式下直接替换文本,保留默认发送行为 if (currentTranslationMode === MODE_ABBREVIATED && detectedLang !== 'Chinese' && detectedLang !== 'Burmese' && text) { // << MODIFIED v3.5 >> 使用新函数处理缩写以保留表情符号 performAbbreviationPreservingEmojis(inputElement, text); updateStatusDisplay("替换缩写成功", 'success', 2000); showInlineStatus(inputElement, '替换缩写成功'); return; // 保留默认 click 发送 } // 中文或缅甸语时拦截并翻译后发送 if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) { const currentModeText = currentTranslationMode === MODE_ABBREVIATED ? '缩写' : '标准'; console.log(`[输入翻译 v${SCRIPT_VERSION}][发送点击] 检测到 ${detectedLang} 文本。将使用 [${currentModeText}] 模式处理...`); event.preventDefault(); event.stopPropagation(); if (sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, true); return; } translateAndSend(text, inputElement, sendButton); } else { // 其他情况允许默认发送 if (!isTranslatingAndSending) hideStatusDisplay(); } } // --- 初始化与附加监听器 (保持不变) --- function initialize() { console.log(`[输入翻译 v${SCRIPT_VERSION}] 初始化脚本...`); document.body.addEventListener('click', handleSendButtonClick, true); const observer = new MutationObserver(mutations => { let controlsNeedCheck = false; let sendButtonMaybeAppeared = 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; // 输入框出现也需要检查控件 } // 检查发送按钮 const sendButtonNode = node.matches(SEND_BUTTON_SELECTOR) ? node : node.querySelector(SEND_BUTTON_SELECTOR); if(sendButtonNode) sendButtonMaybeAppeared = true; }); } // 如果容器自身属性变化,也可能需要检查控件 if (mutation.target && mutation.target.matches && mutation.target.matches(INPUT_AREA_CONTAINER_SELECTOR)) { controlsNeedCheck = true; } }); if (controlsNeedCheck) { // 使用 setTimeout 稍微延迟执行,确保 DOM 结构稳定 setTimeout(ensureControlsExist, 50); } if (sendButtonMaybeAppeared || !sendButtonClickListenerAttached) { const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (sendButton && !sendButton.dataset.customSendClickListener) { attachSendButtonListener(sendButton); } } }); observer.observe(document.body, { childList: true, subtree: true }); // 初始检查,以防页面加载时元素已存在 setTimeout(() => { 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(`[输入翻译 v${SCRIPT_VERSION}] 初始检查完成,观察者已激活。`); }, 1800); // 延迟启动,等待 Telegram Web 加载 } function attachInputListeners(inputElement) { if (inputElement.dataset.customInputTranslateListener) return; console.log(`[输入翻译 v${SCRIPT_VERSION}] 正在附加 Keydown 监听器到输入框:`, inputElement); inputElement.addEventListener('keydown', handleInputKeyDown, true); // 使用捕获阶段确保优先处理 inputElement.dataset.customInputTranslateListener = 'true'; ensureControlsExist(); // 确保控件也存在 } function attachSendButtonListener(sendButton) { if (sendButton.dataset.customSendClickListener) return; console.log(`[输入翻译 v${SCRIPT_VERSION}] 正在附加 Click 监听器到发送按钮:`, sendButton); sendButton.addEventListener('click', handleSendButtonClick, true); // 使用捕获阶段确保优先处理 sendButton.dataset.customSendClickListener = 'true'; sendButtonClickListenerAttached = true; // 标记已附加监听器 // 添加一个观察器来检测按钮是否从 DOM 中移除(例如切换聊天时) const buttonObserver = new MutationObserver(() => { if (!sendButton.isConnected) { console.log(`[输入翻译 v${SCRIPT_VERSION}] 发送按钮已从 DOM 移除。重置监听器标志。`); 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(`[输入翻译 v${SCRIPT_VERSION}] 未找到发送按钮的父节点用于观察器。`); } } // --- 启动初始化 (保持不变) --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } function showInlineStatus(el, msg, duration=2000) { const parent = el.parentElement; if (!parent) return; parent.style.position = parent.style.position || 'relative'; const tip = document.createElement('div'); tip.textContent = msg; tip.style.cssText = 'position:absolute;top:-24px;right:0;padding:4px 8px;background:rgba(0,0,0,0.7);color:#fff;border-radius:4px;font-size:12px;z-index:500;'; parent.appendChild(tip); setTimeout(() => { if (tip && tip.parentElement) tip.parentElement.removeChild(tip); }, duration); } })();