// ==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 = `
`; 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 = ` `; 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