GROQ/KIMI/ZHIPU 分析 (精简版 - 自动切换)

集成AI文本解释工具(GROQ/KIMI/ZHIPU),支持文本选择唤出AI按钮、滚动显示/隐藏按钮、智能缓存、离线模式和无障碍访问。新增AI分析失败时自动切换服务功能。

// ==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
// ==/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 = `
            <div id="aiModalContent" class="ai-modal-content">
                <div id="aiModalHeader" class="ai-modal-header">
                    <h3 style="margin: 0; font-size: 16px;">📖 AI解释</h3>
                    <div id="loadingIndicator" style="display: none;">
                        <div class="loading-spinner"></div>
                        <span>分析中...</span>
                    </div>
                </div>
                <div id="aiModalBody" class="ai-modal-body">
                    <div id="aiText" aria-live="polite"></div>
                    <div id="aiResult" aria-live="polite"></div>
                </div>
                <div id="aiModalFooter" class="ai-modal-footer">
                    <div id="aiSource">来源:${currentConfig.name} (${currentModelDisplay})</div>
                    <button id="aiPlay" class="modal-btn" aria-label="朗读文本">🔊 朗读</button>
                    <button id="aiCopy" class="modal-btn" aria-label="复制结果">
                        <span id="aiCopyText">📋 复制</span>
                    </button>
                    <button id="aiClose" class="modal-btn" aria-label="关闭对话框">❌ 关闭</button>
                </div>
            </div>
        `;
        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 = `
            <div id="settingsModalContent" class="ai-modal-content" style="max-height: calc(100vh - 40px);">
                <div id="settingsHeader" class="ai-modal-header">
                    <h3 style="margin: 0; font-size: 18px;">⚙️ AI 设置 (GROQ/KIMI/ZHIPU)</h3>
                </div>
                <div id="settingsBody" class="ai-modal-body">
                    <div class="settings-group">
                        <label>选择当前活动的 AI 服务 (分析失败时,将按 Groq -> 智谱 -> Kimi 顺序尝试):</label>
                        <div class="radio-container">
                            <input type="radio" id="radioGroq" name="activeService" value="GROQ" ${userSettings.activeService === 'GROQ' ? 'checked' : ''}>
                            <label for="radioGroq">${AI_SERVICES.GROQ.name}</label>
                            <input type="radio" id="radioKimi" name="activeService" value="KIMI" ${userSettings.activeService === 'KIMI' ? 'checked' : ''}>
                            <label for="radioKimi">${AI_SERVICES.KIMI.name}</label>
                            <input type="radio" id="radioZhipu" name="activeService" value="ZHIPU" ${userSettings.activeService === 'ZHIPU' ? 'checked' : ''}>
                            <label for="radioZhipu">${AI_SERVICES.ZHIPU.name}</label>
                        </div>
                    </div>

                    <div id="groqSettings" class="settings-group" style="display: ${userSettings.activeService === 'GROQ' ? 'block' : 'none'};">
                        <label for="groqApiKey">Groq API Key (必填):</label>
                        <input type="text" id="groqApiKey" value="${userSettings.groqApiKey}" placeholder="sk-..." autocomplete="off">
                        <label for="groqModel">Groq Model (必填):</label>
                        <input type="text" id="groqModel" value="${userSettings.groqModel}" placeholder="例如: llama3-8b-8192" autocomplete="off">
                        <small style="color:#aaa;">API URL: ${AI_SERVICES.GROQ.url}</small>
                    </div>

                    <div id="kimiSettings" class="settings-group" style="display: ${userSettings.activeService === 'KIMI' ? 'block' : 'none'};">
                        <label for="kimiApiKey">KIMI API Key (必填):</label>
                        <input type="text" id="kimiApiKey" value="${userSettings.kimiApiKey}" placeholder="sk-..." autocomplete="off">
                        <label for="kimiModel">KIMI Model (必填):</label>
                        <input type="text" id="kimiModel" value="${userSettings.kimiModel}" placeholder="例如: moonshot-v1-8k" autocomplete="off">
                        <small style="color:#aaa;">API URL: ${AI_SERVICES.KIMI.url}</small>
                    </div>

                    <div id="zhipuSettings" class="settings-group" style="display: ${userSettings.activeService === 'ZHIPU' ? 'block' : 'none'};">
                        <label for="zhipuApiKey">智谱 API Key (必填):</label>
                        <input type="text" id="zhipuApiKey" value="${userSettings.zhipuApiKey}" placeholder="..." autocomplete="off">
                        <label for="zhipuModel">智谱 Model (必填):</label>
                        <input type="text" id="zhipuModel" value="${userSettings.zhipuModel}" placeholder="例如: glm-4" autocomplete="off">
                        <small style="color:#aaa;">API URL: ${AI_SERVICES.ZHIPU.url}</small>
                    </div>
                </div>
                <div id="settingsFooter" class="ai-modal-footer">
                    <button id="saveSettingsBtn" class="modal-btn">💾 保存并应用</button>
                    <button id="closeSettingsBtn" class="modal-btn">❌ 关闭</button>
                </div>
            </div>
        `;
        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 **加粗** 为 <strong>
                    aiResult.innerHTML = result.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
                } 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<string>} 成功的解释结果
     */
    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();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址