Greasy Fork

Greasy Fork is available in English.

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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