日语学习助手 - 语法分析与Anki制卡

选中日语文本进行语法和单词分析,支持一键保存到Anki,提供更好的UI交互和错误处理

// ==UserScript==
// @name         日语学习助手 - 语法分析与Anki制卡
// @namespace    http://tampermonkey.net/
// @version      2025-01-05
// @description  选中日语文本进行语法和单词分析,支持一键保存到Anki,提供更好的UI交互和错误处理
// @author       乃木流架
// @match        *://*/*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAqElEQVR4nO2V3QmAMBCD6yZ1Op+EbtOZ3MPeGhHBvgin3vWw9SeQF6nkSwrq3K+L8lPEaqtzYvnPA/RTxGoA3ZHzOfMroJQgsVkwtmZSgPxeMQAJg82WgLK52RJUGKxeAkzzNM8iq5cgpkEYB5HVSxDTXArALcF+Yv3m0uZnS+zz3OMASPn8PQCh9hUEa4DmvwN0N0D1f0EzAFnVgpsByKoW3AzAr9dqAc7mUbe0kIcIAAAAAElFTkSuQmCC
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.xmlHttpRequest
// @connect      api.openai.com
// @connect      localhost
// @connect      127.0.0.1
// @connect      *
// @license MIT
// ==/UserScript==
(async function() {
    'use strict';

    // 工具函数
    const logger = {
        debug: (...args) => console.log('[日语助手-DEBUG]', new Date().toISOString(), ...args),
        info: (...args) => console.info('[日语助手-INFO]', new Date().toISOString(), ...args),
        error: (...args) => console.error('[日语助手-ERROR]', new Date().toISOString(), ...args),
        state: (state, data) => console.log('[日语助手-STATE]', new Date().toISOString(), state, data),
        data: (label, data) => console.log('[日语助手-DATA]', new Date().toISOString(), label, JSON.stringify(data, null, 2))
    };

    // 防抖函数
    const debounce = (fn, delay) => {
        let timer = null;
        return (...args) => {
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => fn(...args), delay);
        };
    };

    // 缓存相关
    const CACHE_KEY = 'jp_helper_cache';
    const MAX_CACHE_SIZE = 50;

    async function getCachedResult(text) {
        try {
            const cache = await GM_getValue(CACHE_KEY, {});
            const cached = cache[text];
            if (cached && Date.now() - cached.timestamp < 24 * 60 * 60 * 1000) { // 24小时有效期
                return cached.result;
            }
            return null;
        } catch (error) {
            logger.error('获取缓存失败:', error);
            return null;
        }
    }

    async function cacheResult(text, result) {
        try {
            const cache = await GM_getValue(CACHE_KEY, {});
            const entries = Object.entries(cache);
            
            if (entries.length >= MAX_CACHE_SIZE) {
                // 删除最旧的缓存
                const oldestKey = entries.sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
                delete cache[oldestKey];
            }
            
            cache[text] = {
                result,
                timestamp: Date.now()
            };
            
            await GM_setValue(CACHE_KEY, cache);
        } catch (error) {
            logger.error('保存缓存失败:', error);
        }
    }

    // 在线状态检查
    async function checkOnlineStatus() {
        if (!navigator.onLine) {
            throw new Error('当前处于离线状态,无法访问API');
        }
    }

    // API密钥加密存储
    async function encryptApiKey(apiKey) {
        return btoa(apiKey);
    }

    async function decryptApiKey(encryptedKey) {
        return atob(encryptedKey);
    }

    // 配置和常量
    const DEFAULT_CONFIG = {
        apiKey: '',
        models: [
            'gpt-4',
            'gpt-4o',
            'gpt-4o-mini',
            'gpt-3.5-turbo-0125',
            'gpt-3.5-turbo-1106'
        ],
        model: 'gpt-4o',
        maxTextLength: 2000,
        requestTimeout: 60000,
        systemPrompt: `你是一名知识渊博的日语老师,帮我分析日语段落,要求整洁清晰的排版,以句子为单位分析生词(用平假名标读音)以及语法结构,也可适当拓展知识,结尾总结并给出译文:`,
        ankiConnect: {
            endpoint: 'http://localhost:8765',
            deckName: '日语牌组',
            modelName: '日语模版',
            timeout: 5000,
            duplicateScope: 'deck'
        }
    };

    // 从存储加载配置
    const CONFIG = await loadConfig();
    console.log(CONFIG);

    // 配置管理函数
    async function loadConfig() {
        try {
            const savedConfig = await GM_getValue('config', {});
            logger.debug('加载配置');
            logger.data('默认配置', DEFAULT_CONFIG);
            logger.data('保存的配置', savedConfig);

            // 解密API密钥
            if (savedConfig.apiKey) {
                try {
                    savedConfig.apiKey = await decryptApiKey(savedConfig.apiKey);
                } catch (error) {
                    logger.error('API密钥解密失败:', error);
                    // 如果解密失败,使用默认配置的API密钥
                    savedConfig.apiKey = DEFAULT_CONFIG.apiKey;
                }
            }

            // 深度合并配置
            const mergedConfig = {
                ...DEFAULT_CONFIG,
                ...savedConfig,
                models: DEFAULT_CONFIG.models,  // 始终使用默认的models
                ankiConnect: {
                    ...DEFAULT_CONFIG.ankiConnect,
                    ...savedConfig.ankiConnect,
                    duplicateScope: DEFAULT_CONFIG.ankiConnect.duplicateScope  // 确保新增字段存在
                }
            };

            logger.data('合并后的配置', mergedConfig);
            return mergedConfig;
        } catch (error) {
            logger.error('加载配置失败:', error);
            return DEFAULT_CONFIG;
        }
    }

    async function saveConfig(newConfig) {
        try {
            logger.debug('保存配置');
            logger.data('当前配置', CONFIG);
            logger.data('新配置', newConfig);

            // 加密API密钥
            if (newConfig.apiKey) {
                try {
                    const encryptedKey = await encryptApiKey(newConfig.apiKey);
                    newConfig = {
                        ...newConfig,
                        apiKey: encryptedKey
                    };
                } catch (error) {
                    logger.error('API密钥加密失败:', error);
                    throw new Error('API密钥加密失败');
                }
            }

            await GM_setValue('config', newConfig);
            // 更新当前配置时使用解密后的API密钥
            Object.assign(CONFIG, {
                ...newConfig,
                apiKey: await decryptApiKey(newConfig.apiKey)
            });
            
            logger.data('更新后的配置', CONFIG);
            logger.debug('配置已保存');
        } catch (error) {
            logger.error('保存配置失败:', error);
            logger.data('错误配置', newConfig);
            throw error;
        }
    }

    // 加载必要的依赖
    async function loadDependencies() {
        logger.info('开始加载依赖项');
        try {
            await loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js');
            logger.debug('markdown-it.js 加载完成');
            logger.info('所有依赖项加载成功');
        } catch (error) {
            logger.error('依赖项加载失败', error);
            throw error;
        }
    }
    function loadScript(url) {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = url;
            script.onload = resolve;
            script.onerror = () => reject(new Error(`Failed to load ${url}`));
            document.head.appendChild(script);
        });
    }
    // 添加样式
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .loading-spinner {
                display: inline-block;
                width: 40px;
                height: 40px;
                border: 4px solid #f3f3f3;
                border-top: 4px solid #4CAF50;
                border-radius: 50%;
                animation: spin 1s linear infinite;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            .jp-helper-menu {
                position: absolute;
                background: transparent;
                padding: 2px;
                border-radius: 6px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.1);
                z-index: 10000;
                font-family: system-ui, -apple-system, sans-serif;
            }
            .jp-helper-button {
                padding: 6px 10px;
                background: white;
                color: #4CAF50;
                border: 1px solid #4CAF50;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
                font-weight: 500;
                transition: all 0.2s;
                display: flex;
                align-items: center;
                gap: 6px;
                min-width: 100px;
                white-space: nowrap;
            }
            .jp-helper-button:hover {
                background: #4CAF50;
                color: white;
                transform: translateY(-1px);
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }
            .jp-helper-button img {
                width: 18px;
                height: 18px;
                object-fit: contain;
            }
            .jp-helper-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #ffffff;
                border-radius: 12px;
                box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
                max-width: 800px;
                width: 90%;
                max-height: 80vh;
                z-index: 10001;
                display: flex;
                flex-direction: column;
                overflow: hidden;
                border: 1px solid rgba(229, 231, 235, 1);
            }
            .jp-helper-panel .drag-handle {
                padding: 12px 16px;
                background: #f0f7ff;
                border-bottom: 1px solid rgba(209, 213, 219, 0.5);
                cursor: move;
                user-select: none;
                border-radius: 12px 12px 0 0;
                display: flex;
                justify-content: space-between;
                align-items: center;
                position: sticky;
                top: 0;
                z-index: 2;
                height: 56px;
                box-sizing: border-box;
            }
            .jp-helper-panel .drag-handle span {
                font-size: 15px;
                font-weight: 600;
                color: #111827;
                letter-spacing: -0.01em;
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .jp-helper-panel .content-container {
                padding: 0 30px;
                overflow-y: auto;
                flex: 1;
                min-height: 0;
                max-height: calc(80vh - 56px);
                color: #111827;
                line-height: 1.6;
            }
            .jp-helper-panel .content-container .analysis-result {
                padding-bottom: 30px;
            }
            .jp-helper-toggle,
            .jp-helper-copy,
            .jp-helper-refresh,
            .jp-helper-close,
            .jp-helper-anki {
                padding: 6px 12px;
                font-size: 13px;
                font-weight: 500;
                border-radius: 6px;
                cursor: pointer;
                transition: all 0.2s ease;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                min-width: 64px;
                height: 32px;
                border: none;
                color: white;
            }
            .jp-helper-toggle {
                background: #6b7280;
            }
            .jp-helper-copy {
                background: #3b82f6;
            }
            .jp-helper-refresh {
                background: #10b981;
            }
            .jp-helper-close {
                background: #ef4444;
            }
            .jp-helper-anki {
                background: #6366f1;
            }
            .jp-helper-toggle:hover,
            .jp-helper-copy:hover,
            .jp-helper-refresh:hover,
            .jp-helper-close:hover,
            .jp-helper-anki:hover {
                filter: brightness(0.95);
            }
            .jp-helper-toggle:active,
            .jp-helper-copy:active,
            .jp-helper-refresh:active,
            .jp-helper-close:active,
            .jp-helper-anki:active {
                filter: brightness(0.9);
            }
            .update-btn,
            .cancel-btn {
                padding: 8px 16px;
                font-size: 14px;
                font-weight: 500;
                border-radius: 6px;
                cursor: pointer;
                transition: all 0.2s ease;
                border: none;
                color: white;
            }
            .update-btn {
                background: #10b981;
            }
            .cancel-btn {
                background: #ef4444;
            }
            .update-btn:hover,
            .cancel-btn:hover {
                filter: brightness(0.95);
            }
            .update-btn:active,
            .cancel-btn:active {
                filter: brightness(0.9);
            }
            .jp-helper-anki:disabled {
                background: #9ca3af;
                cursor: not-allowed;
            }
            .jp-helper-anki:disabled:hover {
                filter: none;
            }
            .jp-helper-panel .content-container::-webkit-scrollbar,
            .compare-dialog-content::-webkit-scrollbar {
                width: 8px;
                height: 8px;
            }
            .jp-helper-panel .content-container::-webkit-scrollbar-track,
            .compare-dialog-content::-webkit-scrollbar-track {
                background: #f1f1f1;
                border-radius: 4px;
            }
            .jp-helper-panel .content-container::-webkit-scrollbar-thumb,
            .compare-dialog-content::-webkit-scrollbar-thumb {
                background: #c1c1c1;
                border-radius: 4px;
            }
            .jp-helper-panel .content-container::-webkit-scrollbar-thumb:hover,
            .compare-dialog-content::-webkit-scrollbar-thumb:hover {
                background: #a8a8a8;
            }
            .jp-helper-settings-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #ffffff;
                border-radius: 16px;
                box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
                max-width: 600px;
                width: 90%;
                max-height: 85vh;
                z-index: 10003;
                display: none;
                flex-direction: column;
                border: 1px solid #e5e7eb;
            }
            .jp-helper-settings-panel .settings-header {
                padding: 20px 24px;
                border-bottom: 1px solid #e5e7eb;
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: #f8fafc;
                border-radius: 16px 16px 0 0;
            }
            .jp-helper-settings-panel .settings-content {
                padding: 24px;
                overflow-y: auto;
                max-height: calc(85vh - 140px);
            }
            .jp-helper-settings-panel .settings-footer {
                padding: 16px 24px;
                border-top: 1px solid #e5e7eb;
                display: flex;
                justify-content: flex-end;
                gap: 12px;
                background: #f8fafc;
                border-radius: 0 0 16px 16px;
            }
            .jp-helper-settings-panel .settings-group {
                margin-bottom: 32px;
            }
            .jp-helper-settings-panel .settings-group:last-child {
                margin-bottom: 0;
            }
            .jp-helper-settings-panel .settings-group-title {
                font-weight: 600;
                color: #111827;
                font-size: 16px;
                margin-bottom: 20px;
                padding-bottom: 12px;
                border-bottom: 1px solid #e5e7eb;
            }
            .jp-helper-settings-panel .setting-item {
                margin-bottom: 20px;
            }
            .jp-helper-settings-panel .setting-item:last-child {
                margin-bottom: 0;
            }
            .jp-helper-settings-panel .help-text {
                margin-top: 6px;
                font-size: 12px;
                color: #6b7280;
                line-height: 1.5;
            }
            .jp-helper-settings-panel input,
            .jp-helper-settings-panel select,
            .jp-helper-settings-panel textarea {
                width: 100%;
                padding: 10px 14px;
                border: 1px solid #e5e7eb;
                border-radius: 8px;
                font-size: 14px;
                background: #f9fafb;
                transition: all 0.2s ease;
                box-sizing: border-box;
                max-width: 100%;
                color: #1f2937;
            }
            .jp-helper-settings-panel input[type="password"] {
                font-family: monospace;
                letter-spacing: 1px;
            }
            .jp-helper-settings-panel input:focus,
            .jp-helper-settings-panel select:focus,
            .jp-helper-settings-panel textarea:focus {
                outline: none;
                border-color: #3b82f6;
                box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
                background: #ffffff;
            }
            .jp-helper-settings-panel label {
                font-weight: 600;
                color: #374151;
                display: block;
                margin-bottom: 8px;
                font-size: 14px;
            }
            .jp-helper-settings-panel .section-title {
                font-weight: 600;
                color: #111827;
                margin: 32px 0 24px;
                padding-top: 24px;
                border-top: 1px solid #e5e7eb;
                font-size: 16px;
            }
            .jp-helper-settings-panel textarea {
                min-height: 120px;
                resize: vertical;
                font-family: inherit;
                line-height: 1.6;
                will-change: height;
                transition: none;
            }
            .jp-helper-settings-panel .help-text {
                margin-top: 6px;
                font-size: 12px;
                color: #6b7280;
                line-height: 1.5;
            }
            .jp-helper-settings-panel .settings-close,
            .jp-helper-settings-panel .settings-cancel,
            .jp-helper-settings-panel .settings-save {
                padding: 8px 16px;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 500;
                transition: all 0.2s ease;
                border: none;
                cursor: pointer;
                display: inline-flex;
                align-items: center;
                gap: 6px;
                min-width: 80px;
                justify-content: center;
            }
            .jp-helper-settings-panel .settings-close {
                background: #ef4444;
                color: white;
            }
            .jp-helper-settings-panel .settings-cancel {
                background: #6b7280;
                color: white;
            }
            .jp-helper-settings-panel .settings-save {
                background: #3b82f6;
                color: white;
            }
            .jp-helper-settings-panel .settings-close:hover,
            .jp-helper-settings-panel .settings-cancel:hover,
            .jp-helper-settings-panel .settings-save:hover {
                filter: brightness(1.1);
                transform: translateY(-1px);
            }
            .jp-helper-settings-panel .settings-close:active,
            .jp-helper-settings-panel .settings-cancel:active,
            .jp-helper-settings-panel .settings-save:active {
                transform: translateY(0);
            }
            .jp-helper-settings-panel .api-key-container {
                position: relative;
            }
            .jp-helper-settings-panel .toggle-password {
                position: absolute;
                right: 8px;
                top: 50%;
                transform: translateY(-50%);
                background: none;
                border: none;
                padding: 4px;
                cursor: pointer;
                color: #6b7280;
            }
            .jp-helper-settings-panel .toggle-password:hover {
                color: #374151;
            }
            .jp-helper-settings-panel .settings-content::-webkit-scrollbar {
                width: 8px;
                height: 8px;
            }
            .jp-helper-settings-panel .settings-content::-webkit-scrollbar-track {
                background: #f1f1f1;
                border-radius: 4px;
            }
            .jp-helper-settings-panel .settings-content::-webkit-scrollbar-thumb {
                background: #c1c1c1;
                border-radius: 4px;
            }
            .jp-helper-settings-panel .settings-content::-webkit-scrollbar-thumb:hover {
                background: #a8a8a8;
            }
            .jp-helper-settings-panel input[type="password"],
            .jp-helper-settings-panel input[type="text"] {
                padding-right: 36px;
            }
            .jp-helper-settings-panel .api-key-container {
                position: relative;
                display: flex;
                align-items: center;
            }
            .jp-helper-settings-panel .toggle-password {
                position: absolute;
                right: 8px;
                top: 50%;
                transform: translateY(-50%);
                background: none;
                border: none;
                padding: 4px;
                cursor: pointer;
                color: #6b7280;
                z-index: 1;
            }
        `;
        document.head.appendChild(style);
    }
    // 添加重试工具函数
    async function retry(fn, retries = 3, delay = 1000) {
        let lastError;
        for (let i = 0; i < retries; i++) {
            try {
                return await fn();
            } catch (error) {
                lastError = error;
                logger.error(`重试第 ${i + 1} 次失败:`, error);
                if (i < retries - 1) {
                    await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
                }
            }
        }
        throw lastError;
    }

    // API 请求函数
    async function analyzeText(text, skipCache = false) {
        try {
            // 基本验证
            if (!text || typeof text !== 'string') {
                throw new Error('无效的输入文本');
            }
            
            logger.debug('开始分析文本', { length: text.length, skipCache });
            logger.data('输入文本', text);
            
            // 检查在线状态
            await checkOnlineStatus();
            
            // 检查配置
            if (!CONFIG.apiKey) {
                throw new Error('请先设置 OpenAI API Key');
            }
            if (!CONFIG.apiEndpoint) {
                throw new Error('API端点未设置');
            }
            if (text.length > CONFIG.maxTextLength) {
                throw new Error(`文本过长,请限制在${CONFIG.maxTextLength} 字符以内`);
            }

            // 检查缓存(如果不跳过缓存)
            if (!skipCache) {
                const cachedResult = await getCachedResult(text);
                if (cachedResult) {
                    logger.debug('使用缓存结果');
                    return cachedResult;
                }
            }

            // API请求
            const result = await retry(async () => {
                return new Promise((resolve, reject) => {
                    const timeoutId = setTimeout(() => {
                        logger.error('API请求超时');
                        reject(new Error('请求超时'));
                    }, CONFIG.requestTimeout);

                    const requestData = {
                        model: CONFIG.model,
                        messages: [
                            { role: "system", content: CONFIG.systemPrompt },
                            { role: "user", content: text }
                        ],
                        temperature: 0.7
                    };
                    
                    logger.debug('发送API请求');
                    logger.data('请求数据', requestData);

                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: CONFIG.apiEndpoint,
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Bearer ${CONFIG.apiKey}`
                        },
                        data: JSON.stringify(requestData),
                        onload: function(response) {
                            clearTimeout(timeoutId);
                            logger.debug('收到API响应:', { 
                                status: response.status,
                                statusText: response.statusText,
                                headers: response.headers
                            });
                            logger.data('响应原始数据', response.responseText);

                            try {
                                if (response.status !== 200) {
                                    const errorData = JSON.parse(response.responseText);
                                    logger.error('API错误响应:', errorData);
                                    throw new Error(`API错误: ${errorData.error?.message || response.statusText}`);
                                }

                                const data = JSON.parse(response.responseText);
                                logger.debug('API响应解析成功');
                                logger.data('解析后的响应数据', data);
                                
                                const result = data.choices[0].message.content;
                                // 缓存结果
                                cacheResult(text, result);
                                resolve(result);
                            } catch (error) {
                                logger.error('API响应处理失败:', error);
                                reject(error);
                            }
                        },
                        onerror: function(error) {
                            clearTimeout(timeoutId);
                            reject(new Error('网络请求失败'));
                        }
                    });
                });
            });

            return result;
        } catch (error) {
            logger.error('文本分析失败:', {
                error: error.message,
                text_length: text?.length,
                api_endpoint: CONFIG.apiEndpoint,
                skipCache
            });
            throw error;
        }
    }
    // UI 组件
    function createUI() {
        const menu = document.createElement('div');
        menu.className = 'jp-helper-menu';
        menu.style.display = 'none';
        
        const button = document.createElement('button');
        button.className = 'jp-helper-button';
        button.innerHTML = `
            <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAqElEQVR4nO2V3QmAMBCD6yZ1Op+EbtOZ3MPeGhHBvgin3vWw9SeQF6nkSwrq3K+L8lPEaqtzYvnPA/RTxGoA3ZHzOfMroJQgsVkwtmZSgPxeMQAJg82WgLK52RJUGKxeAkzzNM8iq5cgpkEYB5HVSxDTXArALcF+Yv3m0uZnS+zz3OMASPn8PQCh9hUEa4DmvwN0N0D1f0EzAFnVgpsByKoW3AzAr9dqAc7mUbe0kIcIAAAAAElFTkSuQmCC">
            <span>日语分析</span>
        `;
        button.style.display = 'flex';
        button.style.alignItems = 'center';
        button.style.padding = '8px 12px';

        menu.appendChild(button);
        const panel = document.createElement('div');
        panel.className = 'jp-helper-panel';
        panel.style.display = 'none';
        
        requestAnimationFrame(() => {
            panel.style.transform = 'translate(-50%, -50%)';
        });
        document.body.appendChild(menu);
        document.body.appendChild(panel);
        return { menu, button, panel };
    }
    // UI 状态更新
    function updateUI(panel, state, content = '', selectedText = '') {
        logger.debug('更新UI状态', { state });
        logger.data('UI更新内容---content', content);
        // 保存当前transform
        const currentTransform = panel.style.transform;
        
        // 清除面板内容
        panel.innerHTML = '';
        
        const header = `
            <div class="drag-handle">
                <span style="font-weight: bold; display: flex; align-items: center; gap: 8px;">
                    <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAqElEQVR4nO2V3QmAMBCD6yZ1Op+EbtOZ3MPeGhHBvgin3vWw9SeQF6nkSwrq3K+L8lPEaqtzYvnPA/RTxGoA3ZHzOfMroJQgsVkwtmZSgPxeMQAJg82WgLK52RJUGKxeAkzzNM8iq5cgpkEYB5HVSxDTXArALcF+Yv3m0uZnS+zz3OMASPn8PQCh9hUEa4DmvwN0N0D1f0EzAFnVgpsByKoW3AzAr9dqAc7mUbe0kIcIAAAAAElFTkSuQmCC" 
                        style="width: 20px; height: 20px;">
                    ${state === 'loading' ? '分析中...' : '分析结果'}
                </span>
                <div class="button-group" style="display: flex; gap: 8px;">
                    <button class="jp-helper-settings" style="
                        padding: 6px 12px;
                        background: #6b7280;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">设置</button>
                    <button class="jp-helper-toggle" style="
                        padding: 6px 12px;
                        background: #6b7280;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">显示</button>
                    <button class="jp-helper-copy" style="
                        padding: 6px 12px;
                        background: #3b82f6;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">复制</button>
                    <button class="jp-helper-anki" style="
                        padding: 6px 12px;
                        background: #6366f1;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">保存到Anki</button>
                    <button class="jp-helper-refresh" style="
                        padding: 6px 12px;
                        background: #10b981;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">刷新</button>
                    <button class="jp-helper-close" style="
                        padding: 6px 12px;
                        background: #ef4444;
                        color: white;
                        border: none;
                        border-radius: 6px;
                        cursor: pointer;
                        font-size: 14px;
                        font-weight: 500;
                        transition: all 0.2s ease;
                        min-width: 64px;
                    ">关闭</button>
                </div>
            </div>
        `;
        let body = '';
        switch (state) {
            case 'loading':
                body = `
                    <div class="content-container">
                        <div style="text-align: center; padding: 20px;">
                            <div class="loading-spinner"></div>
                            <p class="loading-text" style="margin-top: 16px; color: #666;">分析中...</p>
                        </div>
                    </div>
                `;
                // 在分析中禁用Anki和复制按钮
                setTimeout(() => {
                    const ankiBtn = panel.querySelector('.jp-helper-anki');
                    const copyBtn = panel.querySelector('.jp-helper-copy');
                    [ankiBtn, copyBtn].forEach(btn => {
                        if (btn) {
                            btn.disabled = true;
                            btn.style.background = '#9ca3af';
                            btn.style.cursor = 'not-allowed';
                        }
                    });
                }, 0);
                break;
            case 'error':
                body = `
                    <div class="content-container">
                        <div style="padding: 20px;">
                            <h3 style="color: #dc3545; margin: 0 0 16px;">发生错误</h3>
                            <p style="color: #666;">${content}</p>
                        </div>
                    </div>
                `;
                break;
            case 'success':
                const result = markdownit().render(content);
                content = result.replace(/\n/g, "");
                logger.data('解析后的内容', content);
                const sT = `<p><strong>原文</strong>:${selectedText}</p>`;
                body = `
                    <div class="content-container">
                        <div class="analysis-result">
                            <div style="position: sticky; top: 0; background: white; z-index: 1; padding-top: 20px;">
                                ${sT}
                                <hr style="border: 0; height: 1px; background: #e5e7eb; margin: 16px 0;">
                            </div>
                            ${content}
                        </div>
                    </div>
                `;

                // 确保在成功状态下启用按钮
                setTimeout(() => {
                    const ankiBtn = panel.querySelector('.jp-helper-anki');
                    const copyBtn = panel.querySelector('.jp-helper-copy');
                    if (ankiBtn) {
                        ankiBtn.disabled = false;
                        ankiBtn.style.background = '#6366f1';
                        ankiBtn.style.cursor = 'pointer';
                    }
                    if (copyBtn) {
                        copyBtn.disabled = false;
                        copyBtn.style.background = '#3b82f6';
                        copyBtn.style.cursor = 'pointer';
                    }
                }, 0);
                break;
        }
        panel.innerHTML = header + body;
        // 恢复transform
        panel.style.transform = currentTransform;
        // 保存当前文本用于刷新
        panel.dataset.selectedText = selectedText;
        // 恢复内容显示状态
        const contentContainer = panel.querySelector('.content-container');
        if (contentContainer) {
            contentContainer.style.display = panel.dataset.contentHidden === 'true' ? 'none' : 'block';
        }
        // 更新切换按钮文本
        const toggleBtn = panel.querySelector('.jp-helper-toggle');
        if (toggleBtn) {
            toggleBtn.textContent = panel.dataset.contentHidden === 'true' ? '显示' : '隐藏';
        }
        // 绑定按钮事件
        panel.querySelector('.jp-helper-close').onclick = () => panel.style.display = 'none';
        panel.querySelector('.jp-helper-refresh').onclick = async () => {
            const savedText = panel.dataset.selectedText;
            if (savedText) {
                const currentTransform = panel.style.transform;
                updateUI(panel, 'loading', '', savedText);
                panel.style.transform = currentTransform;
                try {
                    // 传入 skipCache = true 跳过缓存
                    const result = await analyzeText(savedText, true);
                    updateUI(panel, 'success', result, savedText);
                    panel.style.transform = currentTransform;
                } catch (error) {
                    updateUI(panel, 'error', error.message, savedText);
                    panel.style.transform = currentTransform;
                }
            }
        };
        panel.querySelector('.jp-helper-toggle').onclick = () => {
            const contentContainer = panel.querySelector('.content-container');
            const isHidden = contentContainer.style.display === 'none';
            
            // 保存当前transform值
            const transform = window.getComputedStyle(panel).transform;
            const matrix = new DOMMatrix(transform);
            const currentX = matrix.m41;
            const currentY = matrix.m42;
            
            contentContainer.style.display = isHidden ? 'block' : 'none';
            panel.dataset.contentHidden = !isHidden;
            panel.querySelector('.jp-helper-toggle').textContent = isHidden ? '隐藏' : '显示';
            
            // 保持当前位置
            if (transform !== 'none') {
                panel.style.transform = `translate(${currentX}px, ${currentY}px)`;
            }
        };
        panel.querySelector('.jp-helper-copy').onclick = () => {
            const contentContainer = panel.querySelector('.content-container');
            if (contentContainer) {
                const textToCopy = contentContainer.innerText;
                handleCopyOperation(panel.querySelector('.jp-helper-copy'), textToCopy);
            }
        };
        // 添加Anki按钮事件
        const ankiBtn = panel.querySelector('.jp-helper-anki');
        if (ankiBtn) {
            ankiBtn.onclick = async () => {
                    const originalText = selectedText;
                    const analysisResult = content;
                await handleAnkiOperation(ankiBtn, originalText, analysisResult);
            };
        }
        // 添加设置按钮事件
        const settingsBtn = panel.querySelector('.jp-helper-settings');
        if (settingsBtn) {
            const settingsPanel = document.querySelector('.jp-helper-settings-panel');
            settingsBtn.onclick = () => {
                if (settingsPanel) {
                    settingsPanel.style.display = 'flex';
                }
            };
        }
    }
    // 拖拽功能
    function enableDrag(element) {
        let isDragging = false;
        let currentX = -window.innerWidth / 2;  // 初始位置为中心
        let currentY = -window.innerHeight / 2;
        let initialX = 0, initialY = 0;
        function dragStart(e) {
            if (!e.target.classList.contains('drag-handle')) return;
            
            isDragging = true;
            
            // 获取当前transform的值
            const transform = window.getComputedStyle(element).transform;
            const matrix = new DOMMatrix(transform);
            currentX = matrix.m41;
            currentY = matrix.m42;
            
            initialX = e.clientX - currentX;
            initialY = e.clientY - currentY;
            
            // 添加拖拽时的鼠标样式
            document.body.style.cursor = 'move';
            e.target.style.cursor = 'move';
        }
        function dragEnd() {
            if (!isDragging) return;
            
            isDragging = false;
            
            // 恢复鼠标样式
            document.body.style.cursor = '';
            const dragHandle = element.querySelector('.drag-handle');
            if (dragHandle) {
                dragHandle.style.cursor = 'move';
            }
        }
        function drag(e) {
            if (!isDragging) return;
            
            e.preventDefault();
            
            currentX = e.clientX - initialX;
            currentY = e.clientY - initialY;
            element.style.transform = `translate(${currentX}px, ${currentY}px)`;
        }
        element.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);
        document.addEventListener('mouseleave', dragEnd);
        return {
            reset: () => {
                currentX = -window.innerWidth / 2;  // 重置到中心位置
                currentY = -window.innerHeight / 2;
                element.style.transform = 'translate(-50%, -50%)';
            }
        };
    }
    // 添加资源预加载
    function preloadResources() {
        // 预加载 markdown-it
        const link = document.createElement('link');
        link.rel = 'preload';
        link.as = 'script';
        link.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js';
        document.head.appendChild(link);
    }

    // 添加内存管理
    function cleanupCache() {
        try {
            const cache = GM_getValue(CACHE_KEY, {});
            const now = Date.now();
            const entries = Object.entries(cache);
            
            // 清理过期缓存(超过24小时)
            const cleanedCache = entries.reduce((acc, [key, value]) => {
                if (now - value.timestamp < 24 * 60 * 60 * 1000) {
                    acc[key] = value;
                }
                return acc;
            }, {});
            
            // 如果清理后的缓存大小发生变化,则保存
            if (Object.keys(cleanedCache).length !== entries.length) {
                GM_setValue(CACHE_KEY, cleanedCache);
                logger.debug('清理过期缓存完成');
            }
        } catch (error) {
            logger.error('清理缓存失败:', error);
        }
    }

    // 修改初始化函数
    async function initialize() {
        try {
            // 预加载资源
            preloadResources();
            
            // 清理过期缓存
            cleanupCache();
            
            // 1. 加载依赖
            await loadDependencies();
            addStyles();
            
            // 2. 创建 UI
            const { menu, button, panel } = createUI();
            const dragHandler = enableDrag(panel);
            // 创建设置面板
            const settingsPanel = createSettingsPanel();

            // 防抖处理的文本分析
            const debouncedAnalyze = debounce(async (text, panel, dragHandler) => {
                try {
                    updateUI(panel, 'loading', '', text);
                    const result = await analyzeText(text);
                    updateUI(panel, 'success', result, text);
                    logger.state('analysis_complete', { success: true });
                } catch (error) {
                    logger.error('分析失败:', error);
                    updateUI(panel, 'error', error.message, text);
                    logger.state('analysis_complete', { success: false, error: error.message });
                }
            }, 500);

            // 分析选中文本的函数
            const analyzeSelectedText = async () => {
                const selectedText = window.getSelection().toString().trim();
                if (!selectedText) {
                    logger.debug('没有选中文本,退出处理');
                    return;
                }

                logger.debug('选中文本:', { length: selectedText.length });
                logger.data('选中的具体内容', selectedText);

                menu.style.display = 'none';
                panel.style.display = 'block';
                dragHandler.reset();

                logger.state('panel_opened', { 
                    text_length: selectedText.length,
                    window_width: window.innerWidth,
                    window_height: window.innerHeight,
                    panel_position: {
                        left: panel.offsetLeft,
                        top: panel.offsetTop
                    }
                });

                await debouncedAnalyze(selectedText, panel, dragHandler);
            };

            // 3. 事件处理
            button.onclick = analyzeSelectedText;

            // 添加快捷键支持
            document.addEventListener('keydown', async (e) => {
                // Ctrl/Cmd + Shift + A 快速分析
                if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'a') {
                    e.preventDefault();
                    await analyzeSelectedText();
                }
                // Ctrl/Cmd + Shift + X 关闭面板
                else if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'x') {
                    e.preventDefault();
                    panel.style.display = 'none';
                }
                // Ctrl/Cmd + Shift + S 打开设置
                else if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 's') {
                    e.preventDefault();
                    if (settingsPanel) {
                        settingsPanel.style.display = 'flex';
                    }
                }
            });

            // 4. 选中文本处理
            document.addEventListener('mouseup', function(e) {
                setTimeout(() => {
                    const selection = window.getSelection();
                    const selectedText = selection.toString().trim();
                    if (!selectedText || menu.contains(e.target) || panel.contains(e.target)) {
                        return;
                    }
                    const range = selection.getRangeAt(0);
                    const rect = range.getBoundingClientRect();
                    
                    // 先获取菜单的尺寸
                    menu.style.visibility = 'hidden';
                    menu.style.display = 'block';
                    const menuRect = menu.getBoundingClientRect();
                    menu.style.display = 'none';
                    menu.style.visibility = 'visible';
                    
                    // 计算菜单位置:默认显示在选中文本的正上方
                    const menuX = rect.left + window.pageXOffset;
                    const menuY = rect.top + window.pageYOffset - menuRect.height - 10;
                    
                    const viewportWidth = window.innerWidth;
                    const viewportHeight = window.innerHeight;
                    
                    // 如果菜单会超出右边界,则向左调整
                    let finalX = menuX;
                    if (menuX + menuRect.width > viewportWidth) {
                        finalX = viewportWidth - menuRect.width - 5;
                    }
                    
                    // 如果菜单会超出顶部边界,则显示在选中文本的下方
                    let finalY = menuY;
                    if (menuY < window.pageYOffset) {
                        finalY = rect.bottom + window.pageYOffset + 10;
                    }
                    
                    Object.assign(menu.style, {
                        left: `${finalX}px`,
                        top: `${finalY}px`,
                        display: 'block'
                    });
                    
                    logger.debug('显示菜单', {
                        selectedTextRect: {
                            left: rect.left,
                            right: rect.right,
                            top: rect.top,
                            bottom: rect.bottom
                        },
                        menuPosition: {
                            x: finalX,
                            y: finalY
                        },
                        viewport: {
                            width: viewportWidth,
                            height: viewportHeight
                        }
                    });
                }, 10);
            });

            // 5. 其他事件
            document.addEventListener('mousedown', function(e) {
                if (!menu.contains(e.target) && !panel.contains(e.target)) {
                    menu.style.display = 'none';
                }
            });

            document.addEventListener('keydown', function(e) {
                if (e.key === 'Escape') {
                    panel.style.display = 'none';
                    settingsPanel.style.display = 'none';
                }
            });

            logger.info('日语学习助手初始化完成');
        } catch (error) {
            logger.error('初始化失败', error);
        }
    }
    // Anki Connect 功能
    async function invokeAnkiConnect(action, params = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: CONFIG.ankiConnect.endpoint,
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify({
                    action,
                    version: 6,
                    params: params
                }),
                onload: function(response) {
                    try {
                        const result = JSON.parse(response.responseText);
                        if (result.error) {
                            reject(new Error(result.error));
                        } else {
                            resolve(result);
                        }
                    } catch (error) {
                        reject(error);
                    }
                },
                onerror: function(error) {
                    reject(new Error('Anki Connect请求失败'));
                },
                ontimeout: function() {
                    reject(new Error('Anki Connect请求超时'));
                },
                timeout: CONFIG.ankiConnect.timeout
            });
        });
    }
    async function addNoteToAnki(japaneseText, analysisResult) {
        try {
            logger.debug('开始添加/更新Anki笔记');
            logger.data('输入参数', { japaneseText, analysisResult });
            logger.data('当前Anki配置', CONFIG.ankiConnect);
            // 检查deck是否存在,不存在则创建
            logger.debug('检查并创建牌组:', CONFIG.ankiConnect.deckName);
            const createDeckResult = await invokeAnkiConnect('createDeck', {
                deck: CONFIG.ankiConnect.deckName
            });
            logger.data('创建牌组结果', createDeckResult);
            // 构建笔记字段
            const fields = {
                '单词': japaneseText,
                '释义': analysisResult,
                '笔记': document.title || '',
                '来源': window.location.href
            };
            logger.data('构建的笔记字段', fields);
            // 查找现有笔记
            const query = `deck:"${CONFIG.ankiConnect.deckName}" "单词:${japaneseText}"`;
            logger.debug('查找现有笔记,查询语句:', query);
            const existingNotes = await invokeAnkiConnect('findNotes', { query });
            logger.data('查找结果', existingNotes);
            if (existingNotes?.result?.length > 0) {
                const noteId = existingNotes.result[0];
                logger.debug('找到现有笔记,ID:', noteId);
                const noteInfo = await invokeAnkiConnect('notesInfo', { notes: [noteId] });
                logger.data('现有笔记信息', noteInfo);
                if (!noteInfo?.result?.[0]?.fields) {
                    throw new Error('获取现有笔记信息失败');
                }
                const existingNote = noteInfo.result[0];
                const existingContent = existingNote.fields.释义.value;
                const existingTags = existingNote.tags || [];
                logger.data('现有笔记内容', { existingContent, existingTags });
                const newTags = [...new Set([...existingTags, '日语助手', '自动更新'])];
                logger.data('合并后的标签', newTags);
                return {
                    type: 'duplicate',
                    noteId,
                    existingContent,
                    newContent: analysisResult,
                    tags: newTags
                };
            }
            // 创建新笔记
            logger.debug('创建新笔记');
            const note = {
                deckName: CONFIG.ankiConnect.deckName,
                modelName: CONFIG.ankiConnect.modelName,
                fields: fields,
                tags: ['日语助手', '自动添加'],
                options: {
                    duplicateScope: CONFIG.ankiConnect.duplicateScope
                }
            };
            logger.data('新笔记数据', note);
            const result = await invokeAnkiConnect('addNote', { note });
            logger.data('创建笔记结果', result);
            logger.debug('创建新笔记成功');
            return { type: 'create', noteId: result };
        } catch (error) {
            logger.error('添加Anki笔记失败:', error);
            logger.data('错误详情', {
                message: error.message,
                stack: error.stack,
                japaneseText,
                analysisResult,
                ankiConfig: CONFIG.ankiConnect
            });
            throw error;
        }
    }
    // UI 状态更新中的Anki按钮处理
    const handleAnkiOperation = async (ankiBtn, originalText, analysisResult) => {
        try {
            logger.debug('开始Anki操作');
            logger.data('操作参数', { 
                originalText, 
                analysisResult,
                buttonState: {
                    text: ankiBtn.textContent,
                    disabled: ankiBtn.disabled,
                    style: ankiBtn.style.cssText
                }
            });
            ankiBtn.textContent = '处理中...';
            ankiBtn.disabled = true;
            ankiBtn.style.background = '#9ca3af';
            ankiBtn.style.cursor = 'not-allowed';
            logger.debug('按钮状态已更新为处理中');
            const result = await addNoteToAnki(originalText, analysisResult);
            logger.data('Anki操作结果', result);
            if (result.type === 'duplicate') {
                logger.debug('检测到重复笔记,显示比较对话框');
                // 创建比较对话框
                const compareDialog = document.createElement('div');
                compareDialog.style.cssText = `
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #ffffff;
                    padding: 32px;
                    border-radius: 16px;
                    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
                    z-index: 10002;
                    max-width: 1200px;
                    width: 95%;
                    max-height: 85vh;
                    display: flex;
                    flex-direction: column;
                    border: 1px solid #e5e7eb;
                    gap: 24px;
                `;
                compareDialog.innerHTML = `
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <h3 style="
                            color: #111827;
                            font-size: 20px;
                            font-weight: 600;
                            margin: 0;
                            letter-spacing: -0.025em;
                        ">已存在的笔记</h3>
                        <div style="display: flex; gap: 12px;">
                            <button class="update-btn" style="
                                padding: 10px 20px;
                                background: #ffffff;
                                color: #059669;
                                border: 2px solid #059669;
                                border-radius: 8px;
                                cursor: pointer;
                                font-size: 14px;
                                font-weight: 600;
                                box-shadow: 2px 2px 0 #059669;
                                transition: all 0.2s ease;
                                display: inline-flex;
                                align-items: center;
                                gap: 6px;
                            ">
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                    <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
                                    <polyline points="17 21 17 13 7 13 7 21"></polyline>
                                    <polyline points="7 3 7 8 15 8"></polyline>
                                </svg>
                                更新笔记
                            </button>
                            <button class="cancel-btn" style="
                                padding: 10px 20px;
                                background: #ffffff;
                                color: #dc2626;
                                border: 2px solid #dc2626;
                                border-radius: 8px;
                                cursor: pointer;
                                font-size: 14px;
                                font-weight: 600;
                                box-shadow: 2px 2px 0 #dc2626;
                                transition: all 0.2s ease;
                                display: inline-flex;
                                align-items: center;
                                gap: 6px;
                            ">
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                    <line x1="18" y1="6" x2="6" y2="18"></line>
                                    <line x1="6" y1="6" x2="18" y2="18"></line>
                                </svg>
                                取消
                            </button>
                        </div>
                    </div>
                    <div class="compare-dialog-content" style="
                        overflow-y: auto;
                        max-height: calc(85vh - 120px);
                        display: grid;
                        grid-template-columns: 1fr 1fr;
                        gap: 32px;
                        padding-right: 12px;
                    ">
                        <div>
                            <h4 style="
                                color: #4b5563;
                                margin: 0 0 16px;
                                font-size: 15px;
                                font-weight: 600;
                                position: sticky;
                                top: 0;
                                background: white;
                                padding: 8px 0;
                                z-index: 1;
                                display: flex;
                                align-items: center;
                                gap: 8px;
                            ">
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                    <polyline points="14 2 14 8 20 8"></polyline>
                                    <line x1="16" y1="13" x2="8" y2="13"></line>
                                    <line x1="16" y1="17" x2="8" y2="17"></line>
                                    <polyline points="10 9 9 9 8 9"></polyline>
                                </svg>
                                现有释义
                            </h4>
                            <div style="
                                background: #f8fafc;
                                padding: 20px;
                                border-radius: 12px;
                                border: 1px solid #e2e8f0;
                                font-size: 14px;
                                line-height: 1.6;
                                color: #334155;
                            ">
                                ${result.existingContent}
                            </div>
                        </div>
                        <div>
                            <h4 style="
                                color: #4b5563;
                                margin: 0 0 16px;
                                font-size: 15px;
                                font-weight: 600;
                                position: sticky;
                                top: 0;
                                background: white;
                                padding: 8px 0;
                                z-index: 1;
                                display: flex;
                                align-items: center;
                                gap: 8px;
                            ">
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                    <polyline points="14 2 14 8 20 8"></polyline>
                                    <line x1="12" y1="18" x2="12" y2="12"></line>
                                    <line x1="9" y1="15" x2="15" y2="15"></line>
                                </svg>
                                新释义
                            </h4>
                            <div style="
                                background: #f0fdf4;
                                padding: 20px;
                                border-radius: 12px;
                                border: 1px solid #dcfce7;
                                font-size: 14px;
                                line-height: 1.6;
                                color: #334155;
                            ">
                                ${result.newContent}
                            </div>
                        </div>
                    </div>
                `;
                document.body.appendChild(compareDialog);
                logger.debug('比较对话框已创建并显示');
                // 添加点击事件
                const updateBtn = compareDialog.querySelector('.update-btn');
                const cancelBtn = compareDialog.querySelector('.cancel-btn');
                updateBtn.onclick = async () => {
                    try {
                        logger.debug('开始更新笔记');
                        logger.data('更新内容', {
                            noteId: result.noteId,
                            newContent: result.newContent,
                            title: document.title,
                            url: window.location.href
                        });
                        await invokeAnkiConnect('updateNote', {
                            note: {
                                id: result.noteId,
                                fields: {
                                    '释义': result.newContent,
                                    '笔记': document.title || '',
                                    '来源': window.location.href
                                },
                                tags: result.tags
                            }
                        });
                        logger.debug('笔记更新成功');
                        compareDialog.remove();
                        ankiBtn.textContent = '已更新';
                        ankiBtn.style.background = '#059669';
                        ankiBtn.style.color = 'white';
                        ankiBtn.disabled = true;
                        logger.debug('更新成功,UI已更新');
                    } catch (error) {
                        logger.error('更新笔记失败:', error);
                        logger.data('错误详情', {
                            message: error.message,
                            stack: error.stack,
                            noteId: result.noteId
                        });
                        ankiBtn.textContent = '更新失败';
                        ankiBtn.style.background = '#dc2626';
                        ankiBtn.style.color = 'white';
                        ankiBtn.disabled = true;
                    }
                    setTimeout(() => {
                        ankiBtn.textContent = '保存到Anki';
                        ankiBtn.style.background = '#6366f1';
                        ankiBtn.style.color = 'white';
                        ankiBtn.style.cursor = 'pointer';
                        ankiBtn.disabled = false;
                        logger.debug('按钮状态已重置');
                    }, 1500);
                };
                cancelBtn.onclick = () => {
                    logger.debug('用户取消更新操作');
                    compareDialog.remove();
                    ankiBtn.textContent = '保存到Anki';
                    ankiBtn.style.background = '#6366f1';
                    ankiBtn.style.color = 'white';
                    ankiBtn.style.cursor = 'pointer';
                    ankiBtn.disabled = false;
                    logger.debug('对话框已关闭,按钮状态已重置');
                };
                return;
            }
            if (result.type === 'update') {
                logger.debug('笔记更新完成');
                ankiBtn.textContent = '已更新';
            } else {
                logger.debug('新笔记创建完成');
                ankiBtn.textContent = '已保存';
            }
            ankiBtn.style.background = '#059669';
            ankiBtn.style.color = 'white';
            ankiBtn.disabled = true;
        } catch (error) {
            logger.error('Anki操作失败:', error);
            logger.data('错误详情', {
                message: error.message,
                stack: error.stack,
                originalText,
                analysisResult
            });
            ankiBtn.textContent = '保存失败';
            ankiBtn.style.background = '#dc2626';
            ankiBtn.style.color = 'white';
            ankiBtn.disabled = true;
        }
        setTimeout(() => {
            ankiBtn.textContent = '保存到Anki';
            ankiBtn.style.background = '#6366f1';
            ankiBtn.style.color = 'white';
            ankiBtn.style.cursor = 'pointer';
            ankiBtn.disabled = false;
            logger.debug('操作完成,按钮状态已重置');
        }, 1500);
    };
    // 复制按钮事件处理
    const handleCopyOperation = async (copyBtn, textToCopy) => {
        try {
            logger.debug('开始复制操作');
            logger.data('复制内容长度', textToCopy.length);
            await navigator.clipboard.writeText(textToCopy);
            logger.debug('内容已复制到剪贴板');
            copyBtn.textContent = '已复制';
            copyBtn.style.background = '#4ade80';
            copyBtn.style.color = '#064e3b';
            logger.debug('复制成功,按钮状态已更新');
        } catch (err) {
            logger.error('复制失败:', err);
            logger.data('错误详情', {
                message: err.message,
                stack: err.stack
            });
            copyBtn.textContent = '复制失败';
            copyBtn.style.background = '#fca5a5';
            copyBtn.style.color = '#7f1d1d';
        }
        setTimeout(() => {
            copyBtn.textContent = '复制';
            copyBtn.style.background = '#3b82f6';
            copyBtn.style.color = 'white';
            logger.debug('按钮状态已重置');
        }, 1500);
    };
    // 创建设置面板
    function createSettingsPanel() {
        const settingsPanel = document.createElement('div');
        settingsPanel.className = 'jp-helper-settings-panel';
        settingsPanel.innerHTML = `
            <div class="settings-header">
                <span style="font-weight: 600; font-size: 20px; color: #111827;">设置</span>
                <button class="settings-close">
                    关闭
                </button>
            </div>
            <div class="settings-content">
                <div class="settings-group">
                    <div class="settings-group-title">OpenAI 设置</div>
                    <div class="setting-item">
                        <label>API Key</label>
                        <div class="api-key-container">
                            <input type="password" id="apiKey" value="${CONFIG.apiKey}">
                            <button class="toggle-password" type="button" aria-label="显示密码">
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                                    <circle cx="12" cy="12" r="3"></circle>
                                </svg>
                            </button>
                        </div>
                    </div>
                    <div class="setting-item">
                        <label>API 端点</label>
                        <input type="text" id="apiEndpoint" value="${CONFIG.apiEndpoint}">
                    </div>
                    <div class="setting-item">
                        <label>模型</label>
                        <select id="model">
                            ${CONFIG.models.map(model => `
                                <option value="${model}" ${CONFIG.model === model ? 'selected' : ''}>
                                    ${model}
                                </option>
                            `).join('')}
                        </select>
                    </div>
                </div>
                <div class="settings-group">
                    <div class="settings-group-title">分析设置</div>
                    <div class="setting-item">
                        <label>最大文本长度</label>
                        <input type="number" id="maxTextLength" value="${CONFIG.maxTextLength}">
                        <div class="help-text">单次分析的最大字符数限制</div>
                    </div>
                    <div class="setting-item">
                        <label>请求超时时间</label>
                        <input type="number" id="requestTimeout" value="${CONFIG.requestTimeout}">
                        <div class="help-text">API请求超时时间(毫秒)</div>
                    </div>
                    <div class="setting-item">
                        <label>系统提示词</label>
                        <textarea id="systemPrompt">${CONFIG.systemPrompt}</textarea>
                        <div class="help-text">用于指导AI分析的系统提示词</div>
                    </div>
                </div>
                <div class="settings-group">
                    <div class="settings-group-title">Anki 设置</div>
                    <div class="setting-item">
                        <label>AnkiConnect 端点</label>
                        <input type="text" id="ankiEndpoint" value="${CONFIG.ankiConnect.endpoint}">
                        <div class="help-text">需要安装 AnkiConnect 插件(2055492159)</div>
                    </div>
                    <div class="setting-item">
                        <label>牌组名称</label>
                        <input type="text" id="deckName" value="${CONFIG.ankiConnect.deckName}">
                        <div class="help-text">如果牌组不存在会自动创建</div>
                    </div>
                    <div class="setting-item">
                        <label>模板名称</label>
                        <input type="text" id="modelName" value="${CONFIG.ankiConnect.modelName}">
                        <div class="help-text">
                            必需字段(其他字段可选):<br>
                            - 单词:存储选中的日语原文<br>
                            - 释义:存储分析结果<br>
                            - 笔记:存储页面标题<br>
                            - 来源:存储页面URL
                        </div>
                    </div>
                </div>
            </div>
            <div class="settings-footer">
                <button class="settings-cancel">取消</button>
                <button class="settings-save">保存</button>
            </div>
        `;
        document.body.appendChild(settingsPanel);
        // 绑定事件
        const closeBtn = settingsPanel.querySelector('.settings-close');
        const cancelBtn = settingsPanel.querySelector('.settings-cancel');
        const saveBtn = settingsPanel.querySelector('.settings-save');
        const togglePasswordBtn = settingsPanel.querySelector('.toggle-password');
        const apiKeyInput = settingsPanel.querySelector('#apiKey');
        closeBtn.onclick = cancelBtn.onclick = () => {
            settingsPanel.style.display = 'none';
        };
        togglePasswordBtn.onclick = () => {
            const type = apiKeyInput.type === 'password' ? 'text' : 'password';
            apiKeyInput.type = type;
            togglePasswordBtn.innerHTML = type === 'password' ? 
                '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>' :
                '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>';
        };
        saveBtn.onclick = async () => {
            try {
                const newConfig = {
                    apiKey: settingsPanel.querySelector('#apiKey').value,
                    apiEndpoint: settingsPanel.querySelector('#apiEndpoint').value,
                    models: CONFIG.models,
                    model: settingsPanel.querySelector('#model').value,
                    maxTextLength: parseInt(settingsPanel.querySelector('#maxTextLength').value),
                    requestTimeout: parseInt(settingsPanel.querySelector('#requestTimeout').value),
                    systemPrompt: settingsPanel.querySelector('#systemPrompt').value,
                    ankiConnect: {
                        endpoint: settingsPanel.querySelector('#ankiEndpoint').value,
                        deckName: settingsPanel.querySelector('#deckName').value,
                        modelName: settingsPanel.querySelector('#modelName').value,
                        timeout: CONFIG.ankiConnect.timeout,
                        duplicateScope: CONFIG.ankiConnect.duplicateScope
                    }
                };

                // 基本验证
                if (!newConfig.apiKey) throw new Error('API Key 不能为空');
                if (!newConfig.apiEndpoint) throw new Error('API 端点不能为空');
                if (!newConfig.ankiConnect.endpoint) throw new Error('AnkiConnect 端点不能为空');
                if (!newConfig.ankiConnect.deckName) throw new Error('牌组名称不能为空');
                if (!newConfig.ankiConnect.modelName) throw new Error('模板名称不能为空');

                saveBtn.textContent = '保存中...';
                saveBtn.disabled = true;
                await saveConfig(newConfig);
                settingsPanel.style.display = 'none';
            } catch (error) {
                logger.error('保存配置失败:', error);
                const errorText = error.message || '保存配置失败';
                saveBtn.textContent = errorText;
                saveBtn.style.background = '#ef4444';
                setTimeout(() => {
                    saveBtn.textContent = '保存';
                    saveBtn.style.background = '#3b82f6';
                    saveBtn.disabled = false;
                }, 2000);
            }
        };
        return settingsPanel;
    }
    // 添加定期清理任务
    setInterval(cleanupCache, 60 * 60 * 1000);
    
    // 启动
    try {
        initialize();
    } catch (error) {
        logger.error('初始化失败:', error);
        const errorMessage = error.message.includes('API') ? 
            '请先在设置中配置正确的API信息' : 
            '初始化失败,请刷新页面重试';

        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 16px 24px;
            background: #fee2e2;
            border: 1px solid #ef4444;
            border-radius: 8px;
            color: #dc2626;
            font-size: 14px;
            z-index: 10000;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
            max-width: 400px;
            word-break: break-word;
            animation: slideIn 0.3s ease;
        `;
        notification.innerHTML = `
            <div style="display: flex; align-items: center; gap: 12px;">
                <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
                    <path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9 9v5m0 0h2m-2 0H9m2-9h.01" 
                        stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                </svg>
                <span>${errorMessage}</span>
            </div>
        `;
        document.body.appendChild(notification);
        
        setTimeout(() => {
            notification.style.animation = 'slideOut 0.3s ease';
            setTimeout(() => notification.remove(), 300);
        }, 5000);
    }

    // 添加所有样式
    const style = document.createElement('style');
    style.textContent = `
        .loading-spinner {
            display: inline-block;
            width: 40px;
            height: 40px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #4CAF50;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        @keyframes slideIn {
            from { transform: translateX(100%); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
        @keyframes slideOut {
            from { transform: translateX(0); opacity: 1; }
            to { transform: translateX(100%); opacity: 0; }
        }
    `;
    document.head.appendChild(style);
})();