// ==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);
}
})();