Greasy Fork

Greasy Fork is available in English.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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