// ==UserScript== // @name GROQ/KIMI/ZHIPU 分析 (精简版 - 自动切换) // @namespace http://tampermonkey.net/ // @version 3.1 // @description 集成AI文本解释工具(GROQ/KIMI/ZHIPU),支持文本选择唤出AI按钮、滚动显示/隐藏按钮、智能缓存、离线模式和无障碍访问。新增AI分析失败时自动切换服务功能。 // @author FocusReader & 整合版 & GROQ/KIMI/ZHIPU (精简 by AI) // @match http://*/* // @match https://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setClipboard // @connect api.groq.com // @connect api.moonshot.cn // @connect open.bigmodel.cn // @connect ms-ra-forwarder-for-ifreetime-beta-two.vercel.app // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/554836/GROQKIMIZHIPU%20%E5%88%86%E6%9E%90%20%28%E7%B2%BE%E7%AE%80%E7%89%88%20-%20%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2%29.user.js // @updateURL https://update.greasyfork.icu/scripts/554836/GROQKIMIZHIPU%20%E5%88%86%E6%9E%90%20%28%E7%B2%BE%E7%AE%80%E7%89%88%20-%20%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2%29.meta.js // ==/UserScript== (function() { 'use strict'; // --- 默认 AI 配置常量 --- const AI_SERVICES = { GROQ: { name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions', model: '', // 留空 apiKey: '', // 留空 }, KIMI: { name: 'Kimi', url: 'https://api.moonshot.cn/v1/chat/completions', model: '', // 留空 apiKey: '', // 留空 }, ZHIPU: { name: 'ChatGLM', url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', model: '', // 留空 apiKey: '', // 留空 } }; // **新增:AI 服务尝试顺序** (KIMI作为最后的选择,除非是用户默认选择) const AI_SERVICE_ORDER = ['GROQ', 'ZHIPU', 'KIMI']; // --- 用户可配置的 AI 设置 --- let userSettings = { activeService: GM_getValue('activeService', 'GROQ'), // 默认激活 Groq groqApiKey: GM_getValue('groqApiKey', AI_SERVICES.GROQ.apiKey), groqModel: GM_getValue('groqModel', AI_SERVICES.GROQ.model), kimiApiKey: GM_getValue('kimiApiKey', AI_SERVICES.KIMI.apiKey), kimiModel: GM_getValue('kimiModel', AI_SERVICES.KIMI.model), zhipuApiKey: GM_getValue('zhipuApiKey', AI_SERVICES.ZHIPU.apiKey), zhipuModel: GM_getValue('zhipuModel', AI_SERVICES.ZHIPU.model), }; /** * 根据服务键获取完整的配置信息 * @param {string} serviceKey - GROQ, KIMI, or ZHIPU * @returns {object} 配置对象 */ function getServiceConfig(serviceKey) { const defaults = AI_SERVICES[serviceKey]; let config = { key: '', model: '', name: defaults.name, url: defaults.url, serviceKey: serviceKey }; if (serviceKey === 'GROQ') { config.key = userSettings.groqApiKey; config.model = userSettings.groqModel; } else if (serviceKey === 'KIMI') { config.key = userSettings.kimiApiKey; config.model = userSettings.kimiModel; } else if (serviceKey === 'ZHIPU') { config.key = userSettings.zhipuApiKey; config.model = userSettings.zhipuModel; } return config; } // 获取当前活动服务的配置(主要用于 UI 初始化和默认尝试) function getActiveServiceConfig() { return getServiceConfig(userSettings.activeService); } // **新增:获取下一个 AI 服务的 Key** /** * 获取当前服务失败后的下一个服务键 * @param {string} currentServiceKey - 当前失败的服务键 * @returns {string|null} 下一个服务键,如果没有更多服务则返回 null */ function getNextServiceKey(currentServiceKey) { // 确保用户默认的服务是第一个尝试的 let order = [userSettings.activeService]; AI_SERVICE_ORDER.forEach(key => { if (key !== userSettings.activeService) { order.push(key); } }); const currentIndex = order.indexOf(currentServiceKey); if (currentIndex !== -1 && currentIndex < order.length - 1) { // 返回列表中的下一个服务 return order[currentIndex + 1]; } return null; // 没有下一个服务 } // --- AI 解释配置常量 (保持不变) --- const TTS_URL = 'https://ms-ra-forwarder-for-ifreetime-2.vercel.app/api/aiyue?text='; const VOICE = 'en-US-EricNeural'; const CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 7 天缓存有效期 const MAX_CACHE_SIZE = 100; // 最大缓存条目数 const MAX_TEXT_LENGTH = 1000; // AI 处理的最大文本长度 const DEBOUNCE_DELAY = 100; // 选中变更的防抖延迟 const instruction = `你是一个智能助手,请用中文分析下面的内容。请根据内容类型(单词或句子)按以下要求进行分析: 如果是**句子或段落**,请: 1. 给出难度等级(A1-C2)并解释 2. 核心语法结构分析 3. 准确翻译 4. 重点短语及例句和例句翻译 如果是**单词**,请: 1. 音标及发音提示 2. 详细释义及词性 3. 常用搭配和例句 4. 记忆技巧(如有) 用 **加粗** 标出重点内容,保持回答简洁实用。`; // --- 全局状态管理 --- let appState = { aiLastSelection: '', // 存储触发 AI 弹窗的文本 isAiModalOpen: false, isSettingsModalOpen: false, // 新增:设置模态框状态 isAiLoading: false, networkStatus: navigator.onLine, lastScrollY: window.scrollY }; // --- 缓存管理器 (保持不变) --- class CacheManager { static getCache() { try { const cache = GM_getValue('aiExplainCache', '{}'); return JSON.parse(cache); } catch { return {}; } } static setCache(cache) { try { GM_setValue('aiExplainCache', JSON.stringify(cache)); } catch (e) { console.warn('Cache save failed:', e); } } static get(key) { const cache = this.getCache(); const item = cache[key]; if (!item) return null; if (Date.now() - item.timestamp > CACHE_EXPIRE_TIME) { this.remove(key); return null; } return item.data; } static set(key, data) { const cache = this.getCache(); this.cleanup(cache); cache[key] = { data: data, timestamp: Date.now() }; this.setCache(cache); } static remove(key) { const cache = this.getCache(); delete cache[key]; this.setCache(cache); } static cleanup(cache = null) { if (!cache) cache = this.getCache(); const now = Date.now(); const keys = Object.keys(cache); keys.forEach(key => { if (now - cache[key].timestamp > CACHE_EXPIRE_TIME) { delete cache[key]; } }); const remainingKeys = Object.keys(cache); if (remainingKeys.length > MAX_CACHE_SIZE) { remainingKeys .sort((a, b) => cache[a].timestamp - cache[b].timestamp) .slice(0, remainingKeys.length - MAX_CACHE_SIZE) .forEach(key => delete cache[key]); } this.setCache(cache); } static getMostRecent() { const cache = this.getCache(); let mostRecentKey = null; let mostRecentTimestamp = 0; for (const key in cache) { if (cache.hasOwnProperty(key)) { const item = cache[key]; if (item.timestamp > mostRecentTimestamp && (Date.now() - item.timestamp <= CACHE_EXPIRE_TIME)) { mostRecentTimestamp = item.timestamp; mostRecentKey = key; } } } return mostRecentKey ? { text: mostRecentKey, data: cache[mostRecentKey].data } : null; } } // --- 工具函数 (保持不变) --- const utils = { debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } }, sanitizeText(text) { return text.trim().substring(0, MAX_TEXT_LENGTH); }, isValidText(text) { return text && text.trim().length > 0 && text.trim().length <= MAX_TEXT_LENGTH; }, vibrate(pattern = [50]) { if (navigator.vibrate) { navigator.vibrate(pattern); } }, showToast(message, duration = 2000) { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 10px 20px; border-radius: 20px; font-size: 14px; z-index: 10003; animation: fadeInOut ${duration}ms ease-in-out; `; if (!document.getElementById('toast-style')) { const style = document.createElement('style'); style.id = 'toast-style'; style.textContent = ` @keyframes fadeInOut { 0%, 100% { opacity: 0; transform: translateX(-50%) translateY(-20px); } 10%, 90% { opacity: 1; transform: translateX(-50%) translateY(0); } } `; document.head.appendChild(style); } document.body.appendChild(toast); setTimeout(() => toast.remove(), duration); } }; // --- 样式 (新增配置模态框样式) --- GM_addStyle(` /* Floating AI Button */ #floatingAiButton { position: fixed; right: 15px; top: 50%; transform: translateY(-70%); background: rgba(0, 122, 255, 0.85); color: white; border: none; border-radius: 50%; width: 48px; height: 48px; font-size: 20px; display: none; /* 默认隐藏 */ align-items: center; justify-content: center; cursor: pointer; z-index: 9999; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); transition: opacity 0.3s ease, transform 0.3s ease; } #floatingAiButton:hover { background: rgba(0, 122, 255, 1); } #floatingAiButton:active { transform: translateY(-70%) scale(0.95); } /* Modal Base Styles */ .ai-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; /* Hidden by default */ align-items: flex-start; justify-content: center; z-index: 10002; padding: env(safe-area-inset-top, 20px) 10px 10px 10px; box-sizing: border-box; overflow-y: auto; -webkit-overflow-scrolling: touch; } .ai-modal-content { background: #2c2c2c; color: #fff; border-radius: 16px; width: 100%; max-width: 500px; position: relative; box-shadow: 0 10px 30px rgba(0,0,0,0.5); margin: 20px 0; overflow: hidden; display: flex; flex-direction: column; } .ai-modal-header { background: #333; padding: 15px 20px; border-bottom: 1px solid #444; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; } .ai-modal-body { padding: 20px; overflow-y: auto; -webkit-overflow-scrolling: touch; flex-grow: 1; } .ai-modal-footer { background: #333; padding: 15px 20px; border-top: 1px solid #444; display: flex; gap: 10px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; /* 右对齐按钮 */ align-items: center; } .modal-btn { padding: 10px 15px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; -webkit-tap-highlight-color: transparent; } .modal-btn:active { transform: scale(0.95); } /* Specific AI Modal Styles */ #aiClose { background: #666; color: white; } #aiCopy { background: #4CAF50; color: white; } #aiPlay { background: #2196F3; color: white; } #aiSource { font-size: 12px; color: #ccc; margin-bottom: 10px; flex-basis: 100%; text-align: center; } #aiText { font-weight: bold; margin-bottom: 15px; padding: 10px; background: #3a3a3a; border-radius: 8px; border-left: 4px solid #4CAF50; word-break: break-word; font-size: 16px; } #aiResult { white-space: pre-wrap; line-height: 1.6; word-break: break-word; font-size: 15px; } #aiResult strong { color: #ffd700; font-weight: 600; } /* Loading Indicator */ #loadingIndicator { display: flex; align-items: center; gap: 10px; color: #999; } .loading-spinner { width: 20px; height: 20px; border: 2px solid #333; border-top: 2px solid #4CAF50; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* Settings Modal Styles */ .settings-group { margin-bottom: 20px; border: 1px solid #444; padding: 15px; border-radius: 8px; } .settings-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #fff; font-size: 14px; } .settings-group input[type="text"] { width: 100%; padding: 10px; margin-top: 4px; margin-bottom: 10px; border: 1px solid #555; border-radius: 5px; background: #3a3a3a; color: #fff; box-sizing: border-box; } .settings-group input:focus { border-color: #2196F3; outline: none; } .radio-container { display: flex; gap: 20px; margin-bottom: 15px; flex-wrap: wrap; } .radio-container input[type="radio"] { margin-right: 5px; accent-color: #4CAF50; } .radio-container label { font-weight: normal; } #saveSettingsBtn { background: #4CAF50; color: white; } #closeSettingsBtn { background: #666; color: white; } /* General */ #networkStatus { position: fixed; top: env(safe-area-inset-top, 10px); left: 50%; transform: translateX(-50%); background: #f44336; color: white; padding: 8px 16px; border-radius: 20px; font-size: 12px; z-index: 10001; display: none; } /* Mobile specific optimizations */ @media (max-width: 480px) { .ai-modal-content { margin: 10px 0; border-radius: 12px; } .ai-modal-header, .ai-modal-footer { padding: 12px 15px; } .ai-modal-body { padding: 15px; } .modal-btn { padding: 12px 8px; font-size: 13px; min-width: unset; } #aiText { font-size: 15px; padding: 8px; } #aiResult { font-size: 14px; } #floatingAiButton { right: 10px; width: 44px; height: 44px; font-size: 18px; } .settings-group input[type="text"] { font-size: 14px; } } `); // --- UI 元素 (创建一次) --- let uiElements = {}; function createCommonUI() { // AI Modal const aiModal = document.createElement('div'); aiModal.id = 'aiModal'; aiModal.className = 'ai-modal-overlay'; aiModal.setAttribute('role', 'dialog'); aiModal.setAttribute('aria-modal', 'true'); aiModal.setAttribute('aria-labelledby', 'aiText'); const currentConfig = getActiveServiceConfig(); const currentModelDisplay = currentConfig.model || '未配置'; aiModal.innerHTML = `

📖 AI解释

`; document.body.appendChild(aiModal); // Settings Modal (新增) const settingsModal = document.createElement('div'); settingsModal.id = 'settingsModal'; settingsModal.className = 'ai-modal-overlay'; settingsModal.setAttribute('role', 'dialog'); settingsModal.setAttribute('aria-modal', 'true'); settingsModal.setAttribute('aria-labelledby', 'settingsHeader'); settingsModal.style.zIndex = '10004'; // 确保在 AI Modal 上方 settingsModal.innerHTML = `

⚙️ AI 设置 (GROQ/KIMI/ZHIPU)

API URL: ${AI_SERVICES.GROQ.url}
API URL: ${AI_SERVICES.KIMI.url}
API URL: ${AI_SERVICES.ZHIPU.url}
`; document.body.appendChild(settingsModal); // Network status indicator const networkStatus = document.createElement('div'); networkStatus.id = 'networkStatus'; networkStatus.textContent = '📡 网络离线'; document.body.appendChild(networkStatus); // Floating AI button const floatingAiButton = document.createElement('button'); floatingAiButton.id = 'floatingAiButton'; floatingAiButton.title = 'AI解释'; floatingAiButton.innerHTML = '💡'; document.body.appendChild(floatingAiButton); uiElements = { aiModal, settingsModal, networkStatus, floatingAiButton }; } // --- 设置模态框控制器 (保持不变) --- const settingsModalController = { open() { // 确保 AI 模态框已关闭 if (appState.isAiModalOpen) aiModalController.close(); appState.isSettingsModalOpen = true; uiElements.settingsModal.style.display = 'flex'; document.body.style.overflow = 'hidden'; uiElements.floatingAiButton.style.display = 'none'; this.updateView(); document.getElementById('saveSettingsBtn').focus(); }, close() { appState.isSettingsModalOpen = false; uiElements.settingsModal.style.display = 'none'; document.body.style.overflow = ''; updateFloatingAiButtonVisibility(); // 恢复浮动按钮逻辑 }, updateView() { const active = userSettings.activeService; document.getElementById('radioGroq').checked = active === 'GROQ'; document.getElementById('radioKimi').checked = active === 'KIMI'; document.getElementById('radioZhipu').checked = active === 'ZHIPU'; document.getElementById('groqSettings').style.display = active === 'GROQ' ? 'block' : 'none'; document.getElementById('kimiSettings').style.display = active === 'KIMI' ? 'block' : 'none'; document.getElementById('zhipuSettings').style.display = active === 'ZHIPU' ? 'block' : 'none'; }, save() { // 读取 UI 上的值 const newActiveService = document.querySelector('input[name="activeService"]:checked').value; const newGroqKey = document.getElementById('groqApiKey').value.trim(); const newGroqModel = document.getElementById('groqModel').value.trim(); const newKimiKey = document.getElementById('kimiApiKey').value.trim(); const newKimiModel = document.getElementById('kimiModel').value.trim(); const newZhipuKey = document.getElementById('zhipuApiKey').value.trim(); const newZhipuModel = document.getElementById('zhipuModel').value.trim(); // 简单的必填项检查 let checkFailed = false; if (newActiveService === 'GROQ' && (!newGroqKey || !newGroqModel)) { utils.showToast('⚠️ Groq API Key 或 Model 不能为空。'); checkFailed = true; } if (newActiveService === 'KIMI' && (!newKimiKey || !newKimiModel)) { utils.showToast('⚠️ KIMI API Key 或 Model 不能为空。'); checkFailed = true; } if (newActiveService === 'ZHIPU' && (!newZhipuKey || !newZhipuModel)) { utils.showToast('⚠️ 智谱 API Key 或 Model 不能为空。'); checkFailed = true; } if (checkFailed) return; // 更新内部状态 userSettings.activeService = newActiveService; userSettings.groqApiKey = newGroqKey; userSettings.groqModel = newGroqModel; userSettings.kimiApiKey = newKimiKey; userSettings.kimiModel = newKimiModel; userSettings.zhipuApiKey = newZhipuKey; userSettings.zhipuModel = newZhipuModel; // 存储到 Tampermonkey GM_setValue('activeService', newActiveService); GM_setValue('groqApiKey', newGroqKey); GM_setValue('groqModel', newGroqModel); GM_setValue('kimiApiKey', newKimiKey); GM_setValue('kimiModel', newKimiModel); GM_setValue('zhipuApiKey', newZhipuKey); GM_setValue('zhipuModel', newZhipuModel); // 更新 AI Modal 的来源显示 const currentConfig = getActiveServiceConfig(); document.getElementById('aiSource').textContent = `来源:${currentConfig.name} (${currentConfig.model || '未配置'})`; this.close(); utils.showToast('✅ AI设置已保存并应用。'); // 首次反馈 } }; // --- 网络状态监控 (保持不变) --- function setupNetworkMonitoring() { const updateNetworkStatus = () => { appState.networkStatus = navigator.onLine; uiElements.networkStatus.style.display = appState.networkStatus ? 'none' : 'block'; if (!appState.networkStatus) { utils.showToast('📡 网络连接中断,将使用缓存数据'); } }; window.addEventListener('online', updateNetworkStatus); window.addEventListener('offline', updateNetworkStatus); updateNetworkStatus(); } // --- AI 模态框控制 (仅修改关闭时的逻辑) --- const aiModalController = { open(text) { appState.isAiModalOpen = true; uiElements.aiModal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // 防止主页面滚动 uiElements.floatingAiButton.style.display = 'none'; document.getElementById('aiText').textContent = text; const closeBtn = document.getElementById('aiClose'); if (closeBtn) closeBtn.focus(); utils.vibrate([50, 50, 100]); }, close() { appState.isAiModalOpen = false; uiElements.aiModal.style.display = 'none'; document.body.style.overflow = ''; // 恢复主页面滚动 // 关闭模态框后重新评估浮动按钮的可见性 updateFloatingAiButtonVisibility(); }, /** * 更新模态框内容和来源显示 * @param {string} text - 输入文本 * @param {string} result - AI结果或错误信息 * @param {object} config - 成功提供服务的配置对象 (可选, 成功时使用) */ updateContent(text, result, config = null) { const aiText = document.getElementById('aiText'); const aiResult = document.getElementById('aiResult'); const aiSource = document.getElementById('aiSource'); if (aiText) aiText.textContent = text; if (aiResult) { if (typeof result === 'string') { // 替换 markdown **加粗** 为 aiResult.innerHTML = result.replace(/\*\*(.*?)\*\*/g, '$1'); } else { aiResult.textContent = result; } } if (aiSource && config) { aiSource.textContent = `来源:${config.name} (${config.model || '未配置'})`; } else if (aiSource && result.includes('(缓存)')) { // 如果是缓存,保留缓存标记 } else if (aiSource && result.includes('❌')) { // 如果是错误信息,不更新来源,或者显示当前尝试的服务 const currentConfig = getActiveServiceConfig(); aiSource.textContent = `来源:${currentConfig.name} (尝试失败/错误)`; } }, setLoading(isLoading) { appState.isAiLoading = isLoading; const loadingIndicator = document.getElementById('loadingIndicator'); if (loadingIndicator) { loadingIndicator.style.display = isLoading ? 'flex' : 'none'; } } }; // --- AI 请求处理器 (修改为使用当前活动配置并支持切换) --- /** * 尝试使用一个 AI 服务获取解释 * @param {string} text - 待解释的文本 * @param {object} config - 当前尝试的 AI 服务配置 * @returns {Promise} 成功的解释结果 */ async function attemptFetch(text, config) { const { url, key, model, name, serviceKey } = config; // 检查配置 if (!key || !model || key.length < 5) { throw new Error(`[${name}] 配置无效 (API Key 或 Model 缺失/过短)`); } aiModalController.updateContent(text, `🤖 尝试连接 ${name} (${model}) 进行分析...`); const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`[${name}] 请求超时 (120秒)`)); }, 120000); GM_xmlhttpRequest({ method: "POST", url: url, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` }, data: JSON.stringify({ model: model, messages: [ { role: "system", content: instruction }, { role: "user", content: `请分析:\n"${text}"` } ] }), onload: (res) => { clearTimeout(timeout); resolve(res); }, onerror: (err) => { clearTimeout(timeout); reject(new Error(`[${name}] 网络错误或连接失败`)); } }); }); let reply; try { const jsonResponse = JSON.parse(response.responseText); // 检查 API 错误 if (jsonResponse.error) { const errorMsg = jsonResponse.error.message || '未知错误'; throw new Error(`[${name}] API 错误: ${errorMsg}`); } reply = jsonResponse.choices?.[0]?.message?.content || null; } catch (e) { console.error(`[${name}] Error parsing AI response:`, e, response.responseText); throw new Error(`[${name}] 无效的响应格式或 API 错误`); } if (!reply || !reply.trim()) { throw new Error(`[${name}] AI 返回空内容`); } // 成功返回配置和结果 return { reply, config }; } /** * 循环尝试 AI 服务直到成功 * @param {string} text - 待解释的文本 */ async function fetchAIExplanation(text) { aiModalController.setLoading(true); let serviceKeyToTry = userSettings.activeService; // 从用户默认选择的服务开始 let attemptsMade = 0; let finalError = ''; while (serviceKeyToTry) { attemptsMade++; const config = getServiceConfig(serviceKeyToTry); try { const { reply, config: successConfig } = await attemptFetch(text, config); // 成功 CacheManager.set(text, reply); aiModalController.updateContent(text, reply, successConfig); utils.showToast(`✅ ${successConfig.name} 解释完成`); aiModalController.setLoading(false); return; // 退出循环 } catch (error) { console.warn(`AI service failed on attempt ${attemptsMade} (${config.name}):`, error.message); finalError = error.message; // 尝试下一个服务 serviceKeyToTry = getNextServiceKey(serviceKeyToTry); if (!serviceKeyToTry) { // 没有更多服务了 break; } utils.showToast(`⚠️ ${config.name} 失败,自动切换至 ${getServiceConfig(serviceKeyToTry).name}`); // 更新模态框状态,显示正在切换 aiModalController.updateContent(text, `${error.message}\n\n正在自动切换到下一个 AI 服务...`); } } // 所有尝试都失败了 const allFailedMessage = `❌ 所有 AI 服务尝试失败。最后错误:${finalError}。请检查API密钥、模型配置和网络连接。`; aiModalController.updateContent(text, allFailedMessage); utils.showToast('❌ 所有 AI 服务请求失败'); aiModalController.setLoading(false); } // 统一的 AI 解释触发逻辑 (更新来源显示) async function triggerAIExplanation() { const selection = window.getSelection(); let text = utils.sanitizeText(selection.toString()); const currentConfig = getActiveServiceConfig(); const currentModelDisplay = currentConfig.model || '未配置'; if (!utils.isValidText(text)) { const recentCache = CacheManager.getMostRecent(); if (recentCache) { appState.aiLastSelection = recentCache.text; aiModalController.open(recentCache.text); aiModalController.updateContent(recentCache.text, recentCache.data); document.getElementById('aiSource').textContent = `来源:最近一次 (缓存)`; utils.showToast('📋 显示最近一次解释'); } else { utils.showToast('请先选择需要AI解释的文本,或无最近解释内容'); } return; } appState.aiLastSelection = text; aiModalController.open(text); aiModalController.updateContent(text, '正在加载...'); document.getElementById('aiSource').textContent = `来源:${currentConfig.name} (${currentModelDisplay})`; const cached = CacheManager.get(text); if (cached) { aiModalController.updateContent(text, cached); document.getElementById('aiSource').textContent = `来源:${currentConfig.name} (缓存)`; utils.showToast('📋 使用缓存数据'); } else if (appState.networkStatus) { aiModalController.updateContent(text, `🤖 正在开始 ${currentConfig.name} 分析...`); await fetchAIExplanation(text); } else { aiModalController.updateContent(text, '📡 网络离线,无法获取新的解释。'); } } // --- 全局事件监听器 --- function setupGlobalEventListeners() { // AI modal event listeners document.getElementById('aiClose').addEventListener('click', aiModalController.close); document.getElementById('aiCopy').addEventListener('click', () => { const result = document.getElementById('aiResult'); const textToCopy = result ? result.innerText : ''; if (textToCopy) { GM_setClipboard(textToCopy); const copyBtnSpan = document.getElementById('aiCopyText'); if (copyBtnSpan) { copyBtnSpan.textContent = '✅ 已复制'; setTimeout(() => { if (copyBtnSpan) copyBtnSpan.textContent = '📋 复制'; }, 1500); } utils.vibrate([50]); utils.showToast('📋 内容已复制到剪贴板'); } }); document.getElementById('aiPlay').addEventListener('click', () => { const textToSpeak = document.getElementById('aiText').textContent.trim(); if (textToSpeak) { try { const audio = new Audio(`${TTS_URL}${encodeURIComponent(textToSpeak)}&voiceName=${VOICE}`); audio.play().catch(e => { console.error('TTS Audio Playback Failed:', e); utils.showToast('🔊 朗读功能暂时不可用 (播放失败)'); }); utils.vibrate([30]); } catch (e) { console.error('TTS Audio Object Creation Failed:', e); utils.showToast('🔊 朗读功能暂时不可用 (创建音频失败)'); } } else { utils.showToast('🔊 没有可朗读的文本'); console.warn("TTS: No text found in #aiText for speaking."); } }); uiElements.aiModal.addEventListener('click', (e) => { if (e.target === uiElements.aiModal) { aiModalController.close(); } }); // Settings Modal Listeners document.getElementById('saveSettingsBtn').addEventListener('click', settingsModalController.save.bind(settingsModalController)); document.getElementById('closeSettingsBtn').addEventListener('click', settingsModalController.close.bind(settingsModalController)); uiElements.settingsModal.addEventListener('click', (e) => { if (e.target === uiElements.settingsModal) { settingsModalController.close(); } }); // 监听单选框变化,切换设置面板 document.querySelectorAll('input[name="activeService"]').forEach(radio => { radio.addEventListener('change', (e) => { userSettings.activeService = e.target.value; settingsModalController.updateView(); }); }); // Keyboard navigation document.addEventListener('keydown', (e) => { if (appState.isAiModalOpen && e.key === 'Escape') { aiModalController.close(); } else if (appState.isSettingsModalOpen && e.key === 'Escape') { settingsModalController.close(); } }); // Floating AI Button Events uiElements.floatingAiButton.addEventListener('click', triggerAIExplanation); // --- 浮动 AI 按钮可见性逻辑 --- let lastWindowScrollYForFloatingButton = window.scrollY; let scrollTimeoutFloatingButton; const showFloatingAiButton = () => { if (!appState.isAiModalOpen && !appState.isSettingsModalOpen) { uiElements.floatingAiButton.style.display = 'flex'; uiElements.floatingAiButton.style.opacity = '1'; uiElements.floatingAiButton.style.transform = 'translateY(-70%) scale(1)'; } }; const hideFloatingAiButton = () => { uiElements.floatingAiButton.style.opacity = '0'; uiElements.floatingAiButton.style.transform = 'translateY(-70%) scale(0.8)'; clearTimeout(scrollTimeoutFloatingButton); scrollTimeoutFloatingButton = setTimeout(() => { uiElements.floatingAiButton.style.display = 'none'; }, 300); }; const updateFloatingAiButtonVisibility = utils.debounce(() => { if (appState.isAiModalOpen || appState.isSettingsModalOpen) { hideFloatingAiButton(); return; } const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText.length > 0) { showFloatingAiButton(); } else { if (window.scrollY > 100) hideFloatingAiButton(); } }, DEBOUNCE_DELAY); window.addEventListener('scroll', utils.throttle(() => { if (appState.isAiModalOpen || appState.isSettingsModalOpen) { return; } const currentWindowScrollY = window.scrollY; const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText.length > 0) { showFloatingAiButton(); } else { if (currentWindowScrollY > lastWindowScrollYForFloatingButton && currentWindowScrollY > 100) { hideFloatingAiButton(); } else if (currentWindowScrollY < lastWindowScrollYForFloatingButton || currentWindowScrollY <= 100) { showFloatingAiButton(); } } lastWindowScrollYForFloatingButton = currentWindowScrollY; }, 100)); document.addEventListener('selectionchange', updateFloatingAiButtonVisibility); updateFloatingAiButtonVisibility(); } // Function to register settings menu commands function registerSettingsMenu() { GM_registerMenuCommand('⚙️ AI设置 (GROQ/KIMI/ZHIPU)', () => { settingsModalController.open(); }); } // --- 初始化 --- function init() { createCommonUI(); // 创建 AI 和 Settings 模态框 setupNetworkMonitoring(); // 开始监控网络状态 setupGlobalEventListeners(); // 绑定所有事件监听器 CacheManager.cleanup(); // 清理过期缓存 // 注册 Tampermonkey 菜单命令 registerSettingsMenu(); console.log(`GROQ/KIMI/ZHIPU AI解释脚本(精简版 - 自动切换)已加载 - 默认服务: ${userSettings.activeService}`); } // 确保 DOM 准备就绪 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();