Greasy Fork

AI Image Description Generator

使用AI生成网页图片描述

目前为 2024-12-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         AI Image Description Generator
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  使用AI生成网页图片描述
// @author       AlphaCat
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 全局变量
    let isSelectionMode = false;

    // 添加样式
    GM_addStyle(`
        .ai-config-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 10000;
            min-width: 500px;
            height: auto;
        }
        .ai-config-modal h3 {
            margin: 0 0 15px 0;
            font-size: 14px;
            font-weight: bold;
            color: #333;
        }
        .ai-config-modal label {
            display: inline-block;
            font-size: 12px;
            font-weight: bold;
            color: #333;
            margin: 0;
            line-height: normal;
            height: auto;
        }
        .ai-config-modal .input-wrapper {
            position: relative;
            display: flex;
            align-items: center;
        }
        .ai-config-modal input {
            display: block;
            width: 100%;
            padding: 2px 24px 2px 2px;
            margin: 2px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 13px;
            line-height: normal;
            height: auto;
            box-sizing: border-box;
        }
        .ai-config-modal .input-icon {
            position: absolute;
            right: 4px;
            width: 16px;
            height: 16px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #666;
            font-size: 12px;
            user-select: none;
        }
        .ai-config-modal .clear-icon {
            right: 24px;
        }
        .ai-config-modal .toggle-password {
            right: 4px;
        }
        .ai-config-modal .input-icon:hover {
            color: #333;
        }
        .ai-config-modal .input-group {
            margin-bottom: 12px;
            height: auto;
            display: flex;
            flex-direction: column;
        }
        .ai-config-modal .button-row {
            display: flex;
            gap: 10px;
            align-items: center;
            margin-top: 5px;
        }
        .ai-config-modal .check-button {
            padding: 4px 8px;
            border: none;
            border-radius: 4px;
            background: #007bff;
            color: white;
            cursor: pointer;
            font-size: 12px;
        }
        .ai-config-modal .check-button:hover {
            background: #0056b3;
        }
        .ai-config-modal .check-button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        .ai-config-modal select {
            width: 100%;
            padding: 4px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 13px;
            margin-top: 2px;
        }
        .ai-config-modal .status-text {
            font-size: 12px;
            margin-left: 10px;
        }
        .ai-config-modal .status-success {
            color: #28a745;
        }
        .ai-config-modal .status-error {
            color: #dc3545;
        }
        .ai-config-modal button {
            margin: 10px 5px;
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        .ai-config-modal button#ai-save-config {
            background: #4CAF50;
            color: white;
        }
        .ai-config-modal button#ai-cancel-config {
            background: #dc3545;
            color: white;
        }
        .ai-config-modal button:hover {
            opacity: 0.9;
        }
        .ai-floating-btn {
            position: fixed;
            width: 32px;
            height: 32px;
            background: #4CAF50;
            color: white;
            border-radius: 50%;
            cursor: move;
            z-index: 9999;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            display: flex;
            align-items: center;
            justify-content: center;
            user-select: none;
            transition: background-color 0.3s;
        }
        .ai-floating-btn:hover {
            background: #45a049;
        }
        .ai-floating-btn svg {
            width: 20px;
            height: 20px;
            fill: white;
        }
        .ai-menu {
            position: absolute;
            background: white;
            border-radius: 5px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 8px;
            z-index: 10000;
            display: flex;
            gap: 8px;
        }
        .ai-menu-item {
            width: 32px;
            height: 32px;
            padding: 6px;
            cursor: pointer;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background-color 0.3s;
        }
        .ai-menu-item:hover {
            background: #f5f5f5;
        }
        .ai-menu-item svg {
            width: 20px;
            height: 20px;
            fill: #666;
        }
        .ai-menu-item:hover svg {
            fill: #4CAF50;
        }
        .ai-image-options {
            display: flex;
            flex-direction: column;
            gap: 10px;
            margin: 15px 0;
        }
        .ai-image-options button {
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            background: #4CAF50;
            color: white;
            cursor: pointer;
            transition: background-color 0.3s;
            font-size: 14px;
        }
        .ai-image-options button:hover {
            background: #45a049;
        }
        #ai-cancel {
            background: #dc3545;
            color: white;
        }
        #ai-cancel:hover {
            opacity: 0.9;
        }
        .ai-toast {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            padding: 10px 20px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 4px;
            font-size: 14px;
            z-index: 10000;
            animation: fadeInOut 3s ease;
            pointer-events: none;
            white-space: pre-line;
            text-align: center;
            max-width: 80%;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        }
        @keyframes fadeInOut {
            0% { opacity: 0; transform: translate(-50%, 10px); }
            10% { opacity: 1; transform: translate(-50%, 0); }
            90% { opacity: 1; transform: translate(-50%, 0); }
            100% { opacity: 0; transform: translate(-50%, -10px); }
        }
        .ai-config-modal .button-group {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 20px;
        }
        .ai-config-modal .button-group button {
            padding: 6px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }
        .ai-config-modal .save-button {
            background: #007bff;
            color: white;
        }
        .ai-config-modal .save-button:hover {
            background: #0056b3;
        }
        .ai-config-modal .save-button:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        .ai-config-modal .cancel-button {
            background: #f8f9fa;
            color: #333;
        }
        .ai-config-modal .cancel-button:hover {
            background: #e2e6ea;
        }
        .ai-selecting-image {
            cursor: crosshair !important;
        }
        .ai-selecting-image * {
            cursor: crosshair !important;
        }
        .ai-image-description {
            position: fixed;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 8px;
            border-radius: 4px;
            font-size: 14px;
            line-height: 1.4;
            max-width: 300px;
            word-wrap: break-word;
            z-index: 10000;
            pointer-events: none;
            animation: fadeIn 0.3s ease;
        }
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        .ai-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
        .ai-result-modal {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            position: relative;
            min-width: 300px;
            max-width: 80%;
            max-height: 80vh;
            overflow-y: auto;
        }
        .ai-result-modal h3 {
            margin: 0 0 15px 0;
            font-size: 16px;
            color: #333;
        }
        .ai-result-modal .description-code {
            background: #f5f5f5;
            padding: 15px;
            border-radius: 4px;
            margin: 10px 0;
            cursor: pointer;
            white-space: pre-wrap;
            word-wrap: break-word;
            font-family: monospace;
            border: 1px solid #ddd;
            position: relative;
        }
        .ai-result-modal .description-code:hover {
            background: #ebebeb;
        }
        .ai-result-modal .copy-hint {
            font-size: 12px;
            color: #666;
            text-align: center;
            margin-top: 5px;
        }
        .ai-result-modal .close-button {
            position: absolute;
            top: 10px;
            right: 10px;
            background: none;
            border: none;
            font-size: 20px;
            cursor: pointer;
            color: #666;
            padding: 5px;
            line-height: 1;
        }
        .ai-result-modal .close-button:hover {
            color: #333;
        }
    `);

    // 密码显示切换功能
    function togglePassword(element) {
        const input = element.parentElement.querySelector('input');
        if (input.type === 'password') {
            input.type = 'text';
            element.textContent = '👁️‍🗨️';
        } else {
            input.type = 'password';
            element.textContent = '👁️';
        }
    }

    // 检查API配置并获取可用模型
    async function checkApiAndGetModels(apiEndpoint, apiKey) {
        try {
            const response = await fetch(`${apiEndpoint}/v1/models`, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${apiKey}`,
                    'Content-Type': 'application/json'
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();
            if (result.data && Array.isArray(result.data)) {
                // 过滤出多模态模型
                const multimodalModels = result.data
                    .filter(model => model.id.includes('vision') || model.id.includes('gpt-4-v'))
                    .map(model => ({
                        id: model.id,
                        name: model.id
                    }));
                return multimodalModels;
            } else {
                throw new Error('Invalid response format');
            }
        } catch (error) {
            console.error('Error fetching models:', error);
            throw error;
        }
    }

    // 检查API配置
    async function checkApiConfig() {
        const apiEndpoint = GM_getValue('apiEndpoint', '').trim();
        const apiKey = GM_getValue('apiKey', '').trim();
        const selectedModel = GM_getValue('selectedModel', '').trim();

        if (!apiEndpoint || !apiKey || !selectedModel) {
            alert('请先配置API Endpoint、API Key和模型');
            showConfigModal();
            return false;
        }

        try {
            const models = await checkApiAndGetModels(apiEndpoint, apiKey);
            if (models.length === 0) {
                alert('无法获取可用模型列表,请检查API配置是否正确');
                return false;
            }
            // 可以在这里添加模型选择的逻辑
            return true;
        } catch (error) {
            console.error('Error checking API config:', error);
            alert('API配置验证失败,请检查配置是否正确');
            return false;
        }
    }

    // 描述所有图片
    function describeAllImages() {
        const images = document.querySelectorAll('img');
        console.log('开始处理所有图片,共找到:', images.length, '张图片');
        processImages(Array.from(images));
    }

    // 描述可见图片
    function describeVisibleImages() {
        const images = Array.from(document.querySelectorAll('img')).filter(img => {
            const rect = img.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
            );
        });
        console.log('开始处理可见图片,共找到:', images.length, '张图片');
        processImages(images);
    }

    // 处理图片
    async function processImages(images) {
        const apiKey = GM_getValue('apiKey', '');
        const endpoint = GM_getValue('apiEndpoint', '');
        const selectedModel = GM_getValue('selectedModel', '');

        for (const img of images) {
            try {
                // 如果图片已经有alt文本,跳过
                if (img.alt && img.alt.length > 0) {
                    console.log('图片已有描述,跳过:', img.alt);
                    continue;
                }

                // 获取图片URL
                const imageUrl = img.src;
                console.log('处理图片:', imageUrl);

                // 调用API获取描述
                const description = await getImageDescription(imageUrl, endpoint, apiKey, selectedModel);
                
                // 更新图片alt文本
                if (description) {
                    img.alt = description;
                    img.title = description;
                    console.log('已添加描述:', description);
                }
            } catch (error) {
                console.error('处理图片时出错:', error);
            }
        }
    }

    // 获取图片的Base64内容
    async function getImageBase64(imageUrl) {
        console.log('[Debug] Starting image to Base64 conversion for:', imageUrl);
        
        // 尝试将HTTP URL转换为HTTPS
        if (imageUrl.startsWith('http:')) {
            imageUrl = imageUrl.replace('http:', 'https:');
            console.log('[Debug] Converted to HTTPS URL:', imageUrl);
        }

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: imageUrl,
                responseType: 'blob',
                onload: function(response) {
                    console.log('[Debug] Image fetch response:', response.status);
                    if (response.status === 200) {
                        const blob = response.response;
                        console.log('[Debug] Image blob size:', blob.size, 'bytes');
                        
                        const reader = new FileReader();
                        reader.onloadend = () => {
                            const base64 = reader.result.split(',')[1];
                            console.log('[Debug] Base64 conversion completed, length:', base64.length);
                            resolve(base64);
                        };
                        reader.onerror = (error) => {
                            console.error('[Debug] FileReader error:', error);
                            reject(error);
                        };
                        reader.readAsDataURL(blob);
                    } else {
                        reject(new Error(`Failed to fetch image: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    console.error('[Debug] GM_xmlhttpRequest error:', error);
                    reject(error);
                }
            });
        });
    }

    // 调用API获取图片描述
    async function getImageDescription(imageUrl, apiEndpoint, apiKey, selectedModel) {
        console.log('[Debug] Starting image description request:', {
            apiEndpoint,
            selectedModel,
            imageUrl
        });

        try {
            const base64Image = await getImageBase64(imageUrl);
            
            const requestBody = {
                model: selectedModel,
                messages: [{
                    role: "user",
                    content: [
                        {
                            type: "image_url",
                            image_url: {
                                url: `data:image/jpeg;base64,${base64Image}`
                            }
                        },
                        {
                            type: "text",
                            text: "Describe the main content of the image. If it is a person, provide a description of the person with at least 15 words. Answer in Chinese."
                        }
                    ]
                }],
                stream: true
            };

            console.log('[Debug] API Request body:', JSON.stringify(requestBody, null, 2));

            const response = await fetch(`${apiEndpoint}/chat/completions`, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${apiKey}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(requestBody)
            });

            console.log('[Debug] API Response status:', response.status, response.statusText);
            
            if (!response.ok) {
                const errorText = await response.text();
                console.error('[Debug] API Error response:', errorText);
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let description = '';
            let chunkCounter = 0;

            while (true) {
                const { value, done } = await reader.read();
                if (done) {
                    console.log('[Debug] Stream completed. Final description:', description);
                    showDescriptionModal(description);
                    break;
                }

                const chunk = decoder.decode(value);
                console.log(`[Debug] Received chunk #${++chunkCounter}:`, chunk);

                const lines = chunk.split('\n').filter(line => line.trim() !== '');

                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const jsonStr = line.slice(6);
                        if (jsonStr === '[DONE]') {
                            console.log('[Debug] Received [DONE] signal');
                            continue;
                        }
                        
                        try {
                            const jsonData = JSON.parse(jsonStr);
                            console.log('[Debug] Parsed JSON data:', jsonData);
                            
                            const content = jsonData.choices[0]?.delta?.content;
                            if (content) {
                                description += content;
                                console.log('[Debug] Updated description:', description);
                            }
                        } catch (e) {
                            console.error('[Debug] Error parsing JSON:', e, 'Raw string:', jsonStr);
                        }
                    }
                }
            }

            return description;
        } catch (error) {
            console.error('[Debug] Error in getImageDescription:', error);
            throw error;
        }
    }

    // 显示描述tooltip
    function showDescriptionTooltip(description, x, y) {
        const tooltip = document.createElement('div');
        tooltip.className = 'ai-image-description';
        tooltip.textContent = description;
        tooltip.style.left = `${x}px`;
        tooltip.style.top = `${y}px`;
        document.body.appendChild(tooltip);
        return tooltip;
    }

    // 更新描述tooltip内容
    function updateDescriptionTooltip(description) {
        const tooltip = document.querySelector('.ai-image-description');
        if (tooltip) {
            tooltip.textContent = description;
        }
    }

    // 移除描述tooltip
    function removeDescriptionTooltip() {
        const tooltip = document.querySelector('.ai-image-description');
        if (tooltip) {
            tooltip.remove();
        }
    }

    // 进入图片选择模式
    function enterImageSelectionMode() {
        console.log('[Debug] Entering image selection mode');
        document.body.style.cursor = 'crosshair';
        isSelectionMode = true;

        // 创建点击事件处理函数
        const clickHandler = async function(e) {
            if (!isSelectionMode) return; // 确保在选择模式下才处理点击

            if (e.target.tagName === 'IMG') {
                console.log('[Debug] Image clicked:', e.target.src);
                e.preventDefault();
                e.stopPropagation();
                
                // 获取配置
                const endpoint = GM_getValue('apiEndpoint', '');
                const apiKey = GM_getValue('apiKey', '');
                const selectedModel = GM_getValue('selectedModel', '');

                console.log('[Debug] Current configuration:', {
                    endpoint,
                    selectedModel,
                    hasApiKey: !!apiKey
                });

                if (!endpoint || !apiKey || !selectedModel) {
                    showToast('请先完成API配置');
                    exitImageSelectionMode();
                    return;
                }

                // 显示加载中的tooltip
                showDescriptionTooltip('正在生成描述...', e.pageX + 10, e.pageY + 10);

                try {
                    await getImageDescription(e.target.src, endpoint, apiKey, selectedModel);
                } catch (error) {
                    console.error('[Debug] Description generation failed:', error);
                    removeDescriptionTooltip();
                    showToast('生成描述失败: ' + error.message);
                }

                // 处理完一张图片后自动退出选择模式
                exitImageSelectionMode();
            }
        };

        // 添加点击事件监听器
        document.addEventListener('click', clickHandler, true);
        
        // ESC键退出选择模式
        const escHandler = (e) => {
            if (e.key === 'Escape') {
                exitImageSelectionMode();
            }
        };
        document.addEventListener('keydown', escHandler);

        // 保存事件处理函数以便后续移除
        window._imageSelectionHandlers = {
            click: clickHandler,
            keydown: escHandler
        };
    }

    // 退出图片选择模式
    function exitImageSelectionMode() {
        console.log('[Debug] Exiting image selection mode');
        document.body.style.cursor = 'default';
        isSelectionMode = false;

        // 移除所有事件监听器
        if (window._imageSelectionHandlers) {
            document.removeEventListener('click', window._imageSelectionHandlers.click, true);
            document.removeEventListener('keydown', window._imageSelectionHandlers.keydown);
            window._imageSelectionHandlers = null;
        }
    }

    // 显示toast提示
    function showToast(message, duration = 3000) {
        const toast = document.createElement('div');
        toast.className = 'ai-toast';
        toast.textContent = message;
        document.body.appendChild(toast);
        
        setTimeout(() => {
            toast.remove();
        }, duration);
    }

    // 检查用户信息
    async function checkUserInfo(apiEndpoint, apiKey) {
        try {
            const response = await fetch(`${apiEndpoint}/v1/user/info`, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${apiKey}`
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();
            console.log('[Debug] User Info API Response:', result);
            
            if (result.code === 20000 && result.status && result.data) {
                const { name, balance, chargeBalance, totalBalance } = result.data;
                return {
                    name,
                    balance: parseFloat(balance),
                    chargeBalance: parseFloat(chargeBalance),
                    totalBalance: parseFloat(totalBalance)
                };
            } else {
                throw new Error(result.message || 'Invalid response format');
            }
        } catch (error) {
            console.error('[Debug] User Info API Error:', error);
            throw error;
        }
    }

    // 获取可用模型列表
    async function getAvailableModels(apiEndpoint, apiKey) {
        // 定义支持的视觉模型列表
        const supportedVLModels = [
            'Qwen/Qwen2-VL-72B-Instruct',
            'Pro/Qwen/Qwen2-VL-7B-Instruct',
            'OpenGVLab/InternVL2-Llama3-76B',
            'OpenGVLab/InternVL2-26B',
            'Pro/OpenGVLab/InternVL2-8B'
        ];

        try {
            const response = await fetch(`${apiEndpoint}/v1/models`, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${apiKey}`
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();
            console.log('[Debug] Models API Response:', result);
            
            if (result.object === 'list' && Array.isArray(result.data)) {
                // 筛选出支持的视觉模型
                const models = result.data
                    .filter(model => supportedVLModels.includes(model.id))
                    .map(model => ({
                        id: model.id,
                        // 美化显示名称
                        name: model.id.split('/').pop()
                            .replace('Qwen2-VL-', 'Qwen2-')
                            .replace('InternVL2-Llama3-', 'InternVL2-')
                            .replace('-Instruct', '')
                    }));

                console.log('[Debug] Available VL Models:', models);
                
                if (models.length === 0) {
                    console.warn('[Debug] No supported VL models found in the response');
                }
                
                return models;
            } else {
                throw new Error('Invalid models response format');
            }
        } catch (error) {
            console.error('[Debug] Models API Error:', error);
            throw error;
        }
    }

    // 更新模型下拉菜单
    function updateModelSelect(selectElement, models) {
        if (models.length === 0) {
            selectElement.innerHTML = '<option value="">未找到可用的视觉模型</option>';
            selectElement.disabled = true;
            return;
        }

        selectElement.innerHTML = '<option value="">请选择视觉模型</option>' +
            models.map(model => 
                `<option value="${model.id}" title="${model.id}">${model.name}</option>`
            ).join('');
        selectElement.disabled = false;
    }

    // 保存模型列表到GM存储
    function saveModelList(models) {
        GM_setValue('availableModels', models);
    }

    // 从GM存储获取模型列表
    function getStoredModelList() {
        return GM_getValue('availableModels', []);
    }

    // 创建悬浮按钮
    function createFloatingButton() {
        const btn = document.createElement('div');
        btn.className = 'ai-floating-btn';
        btn.innerHTML = `
            <svg viewBox="0 0 24 24">
                <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
            </svg>
        `;

        // 添加点击事件处理
        btn.addEventListener('click', function(e) {
            if (e.button === 0) { // 左键点击
                enterImageSelectionMode();
                e.stopPropagation(); // 阻止事件冒泡
            }
        });

        // 右键点击显示配置
        btn.addEventListener('contextmenu', function(e) {
            e.preventDefault();
            exitImageSelectionMode();
            createConfigUI();
        });

        // ESC键退出选择模式
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                exitImageSelectionMode();
                removeDescriptionTooltip();
            }
        });

        // 设置初始位置为左上角或保存的位置
        const savedPos = JSON.parse(GM_getValue('btnPosition', '{"x": 20, "y": 20}'));
        btn.style.left = (savedPos.x || 20) + 'px';
        btn.style.top = (savedPos.y || 20) + 'px';
        btn.style.right = 'auto';
        btn.style.bottom = 'auto';

        let isDragging = false;
        let startX, startY;
        let initialLeft, initialTop;

        function dragStart(e) {
            if (e.target === btn || btn.contains(e.target)) {
                isDragging = true;
                const rect = btn.getBoundingClientRect();
                startX = e.clientX;
                startY = e.clientY;
                initialLeft = rect.left;
                initialTop = rect.top;
                e.preventDefault();
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();
                const deltaX = e.clientX - startX;
                const deltaY = e.clientY - startY;
                
                const newLeft = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, initialLeft + deltaX));
                const newTop = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, initialTop + deltaY));
                
                btn.style.left = newLeft + 'px';
                btn.style.top = newTop + 'px';
            }
        }

        function dragEnd(e) {
            if (isDragging) {
                isDragging = false;
                const rect = btn.getBoundingClientRect();
                GM_setValue('btnPosition', JSON.stringify({
                    x: rect.left,
                    y: rect.top
                }));
            }
        }

        btn.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        // 添加悬浮菜单
        let menu = null;
        let menuTimeout = null;
        
        function showMainMenu() {
            if (menu) return;

            menu = document.createElement('div');
            menu.className = 'ai-menu';
            menu.innerHTML = `
                <div class="ai-menu-item" id="ai-describe-images" title="选择要识别的图像">
                    <svg viewBox="0 0 24 24">
                        <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
                    </svg>
                </div>
                <div class="ai-menu-item" id="ai-settings" title="设置AI功能">
                    <svg viewBox="0 0 24 24">
                        <path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6zm0-14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
                    </svg>
                </div>
            `;
            // 计算菜单位置
            const btnRect = btn.getBoundingClientRect();
            menu.style.left = (btnRect.right + 5) + 'px';
            menu.style.top = btnRect.top + 'px';

            document.body.appendChild(menu);

            // 为菜单添加鼠标进入和离开事件
            menu.addEventListener('mouseenter', () => {
                if (menuTimeout) {
                    clearTimeout(menuTimeout);
                    menuTimeout = null;
                }
            });

            menu.addEventListener('mouseleave', () => {
                hideMainMenu();
            });

            // 添加菜单项点击事件
            menu.querySelector('#ai-describe-images').onclick = () => {
                menu.remove();
                menu = null;
                showImageSelectionModal();
            };
            
            menu.querySelector('#ai-settings').onclick = () => {
                menu.remove();
                menu = null;
                createConfigUI();
            };
        }

        function hideMainMenu() {
            if (menuTimeout) {
                clearTimeout(menuTimeout);
            }
            menuTimeout = setTimeout(() => {
                if (menu) {
                    menu.remove();
                    menu = null;
                }
                menuTimeout = null;
            }, 300); // 300ms延迟,避免菜单闪烁
        }

        // 添加鼠标进入和离开事件
        btn.addEventListener('mouseenter', () => {
            if (menuTimeout) {
                clearTimeout(menuTimeout);
                menuTimeout = null;
            }
            showMainMenu();
        });

        btn.addEventListener('mouseleave', () => {
            hideMainMenu();
        });

        document.body.appendChild(btn);
        return btn;
    }

    // 创建配置界面
    function createConfigUI() {
        const overlay = document.createElement('div');
        overlay.className = 'ai-modal-overlay';
        
        const modal = document.createElement('div');
        modal.className = 'ai-config-modal';
        modal.innerHTML = `
            <h3>AI图像描述配置</h3>
            <div class="input-group">
                <label>API Endpoint:</label>
                <div class="input-wrapper">
                    <input type="text" id="ai-endpoint" placeholder="https://api.openai.com" value="${GM_getValue('apiEndpoint', '')}">
                    <span class="input-icon clear-icon" title="清空" onclick="this.previousElementSibling.value=''">✕</span>
                </div>
            </div>
            <div class="input-group">
                <label>API Key:</label>
                <div class="input-wrapper">
                    <input type="password" id="ai-apikey" value="${GM_getValue('apiKey', '')}">
                    <span class="input-icon clear-icon" title="清空" onclick="this.previousElementSibling.value=''">✕</span>
                    <span class="input-icon toggle-password" title="显示/隐藏密码">👁️</span>
                </div>
                <div class="button-row">
                    <button class="check-button" id="check-api">检测可用性</button>
                </div>
            </div>
            <div class="input-group">
                <label>可用模型:</label>
                <select id="ai-model">
                    <option value="">加载中...</option>
                </select>
            </div>
            <div class="button-group">
                <button class="cancel-button" id="ai-cancel-config">取消</button>
                <button class="save-button" id="ai-save-config">保存</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // 初始化模型下拉菜单
        const modelSelect = modal.querySelector('#ai-model');
        const storedModels = getStoredModelList();
        const selectedModel = GM_getValue('selectedModel', '');
        
        if (storedModels.length > 0) {
            updateModelSelect(modelSelect, storedModels);
            if (selectedModel) {
                modelSelect.value = selectedModel;
            }
        } else {
            modelSelect.innerHTML = '<option value="">请先检测API可用性</option>';
            modelSelect.disabled = true;
        }

        // 添加密码显示切换事件监听
        const toggleBtn = modal.querySelector('.toggle-password');
        toggleBtn.addEventListener('click', function() {
            togglePassword(this);
        });

        // 自动保存配置
        const inputs = modal.querySelectorAll('input');
        inputs.forEach(input => {
            input.addEventListener('blur', function() {
                const endpoint = modal.querySelector('#ai-endpoint').value.trim();
                const apiKey = modal.querySelector('#ai-apikey').value.trim();
                
                if (endpoint && apiKey) {
                    GM_setValue('apiEndpoint', endpoint);
                    GM_setValue('apiKey', apiKey);
                    showToast('配置已保存');
                }
            });
        });

        // 检测API可用性
        const checkButton = modal.querySelector('#check-api');
        checkButton.addEventListener('click', async function() {
            const endpoint = modal.querySelector('#ai-endpoint').value.trim();
            const apiKey = modal.querySelector('#ai-apikey').value.trim();

            if (!endpoint || !apiKey) {
                showToast('请先填写API Endpoint和API Key');
                return;
            }

            checkButton.disabled = true;
            modelSelect.disabled = true;
            modelSelect.innerHTML = '<option value="">检测中...</option>';

            try {
                // 并行请求用户信息和模型列表
                const [userInfo, models] = await Promise.all([
                    checkUserInfo(endpoint, apiKey),
                    getAvailableModels(endpoint, apiKey)
                ]);

                // 保存模型列表
                saveModelList(models);

                // 更新模型下拉菜单
                updateModelSelect(modelSelect, models);

                // 显示用户信息
                showToast(`检测通过,欢迎 ${userInfo.name}!\n账户余额:${userInfo.balance.toFixed(2)}\n充值余额:${userInfo.chargeBalance.toFixed(2)}\n总余额:${userInfo.totalBalance.toFixed(2)}`);

                // 如果之前保存过模型选择,恢复选择
                const savedModel = GM_getValue('selectedModel', '');
                if (savedModel && models.some(m => m.id === savedModel)) {
                    modelSelect.value = savedModel;
                }
            } catch (error) {
                showToast('API检测失败:' + error.message);
                modelSelect.innerHTML = '<option value="">获取模型列表失败</option>';
                modelSelect.disabled = true;
            } finally {
                checkButton.disabled = false;
            }
        });

        // 模型选择变更时保存
        modelSelect.addEventListener('change', function() {
            if (this.value) {
                GM_setValue('selectedModel', this.value);
                showToast('已保存模型选择');
            }
        });

        // 保存配置
        const saveButton = modal.querySelector('#ai-save-config');
        saveButton.addEventListener('click', function() {
            const endpoint = modal.querySelector('#ai-endpoint').value.trim();
            const apiKey = modal.querySelector('#ai-apikey').value.trim();
            const selectedModel = modelSelect.value;

            if (!endpoint || !apiKey) {
                showToast('请填写API Endpoint和API Key');
                return;
            }

            if (!selectedModel) {
                showToast('请选择一个视觉模型');
                return;
            }

            GM_setValue('apiEndpoint', endpoint);
            GM_setValue('apiKey', apiKey);
            GM_setValue('selectedModel', selectedModel);
            showToast('配置已保存');
            overlay.remove();
        });

        // 更新保存按钮状态
        function updateSaveButtonState() {
            const endpoint = modal.querySelector('#ai-endpoint').value.trim();
            const apiKey = modal.querySelector('#ai-apikey').value.trim();
            const selectedModel = modelSelect.value;
            
            saveButton.disabled = !endpoint || !apiKey || !selectedModel;
        }

        // 监听输入变化
        modal.querySelector('#ai-endpoint').addEventListener('input', updateSaveButtonState);
        modal.querySelector('#ai-apikey').addEventListener('input', updateSaveButtonState);
        modelSelect.addEventListener('change', updateSaveButtonState);

        // 初始化保存按钮状态
        updateSaveButtonState();

        // 取消配置
        modal.querySelector('#ai-cancel-config').onclick = () => {
            overlay.remove();
        };

        // 点击遮罩层关闭
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                overlay.remove();
            }
        });
    }

    // 显示图像选择界面
    function showImageSelectionModal() {
        const overlay = document.createElement('div');
        overlay.className = 'ai-modal-overlay';
        
        const modal = document.createElement('div');
        modal.className = 'ai-config-modal';
        modal.innerHTML = `
            <h3>选择要识别的图像</h3>
            <div class="ai-image-options">
                <button id="ai-all-images">识别所有图片</button>
                <button id="ai-visible-images">仅识别可见图片</button>
            </div>
            <button id="ai-cancel">取消</button>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // 添加事件监听
        modal.querySelector('#ai-all-images').onclick = () => {
            if (checkApiConfig()) {
                describeAllImages();
                overlay.remove();
            }
        };

        modal.querySelector('#ai-visible-images').onclick = () => {
            if (checkApiConfig()) {
                describeVisibleImages();
                overlay.remove();
            }
        };

        modal.querySelector('#ai-cancel').onclick = () => {
            overlay.remove();
        };

        // 点击遮罩层关闭
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                overlay.remove();
            }
        });
    }

    function showDescriptionModal(description) {
        // 创建遮罩层
        const overlay = document.createElement('div');
        overlay.className = 'ai-modal-overlay';
        
        const modal = document.createElement('div');
        modal.className = 'ai-result-modal';
        modal.innerHTML = `
            <h3>图片描述结果</h3>
            <pre class="description-code"><code>${description}</code></pre>
            <div class="copy-hint">点击上方代码块复制内容</div>
            <button class="close-button">&times;</button>
        `;
    
        // 添加复制功能
        const codeBlock = modal.querySelector('.description-code');
        codeBlock.addEventListener('click', async () => {
            try {
                await navigator.clipboard.writeText(description);
                showToast('已复制描述');
            } catch (err) {
                console.error('[Debug] Copy failed:', err);
                showToast('复制失败,请手动复制');
            }
        });
    
        // 添加关闭按钮功能
        const closeButton = modal.querySelector('.close-button');
        closeButton.addEventListener('click', () => {
            overlay.remove();
        });
    
        // 点击遮罩层关闭
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                overlay.remove();
            }
        });
    
        // ESC键关闭
        const escHandler = (e) => {
            if (e.key === 'Escape') {
                overlay.remove();
                document.removeEventListener('keydown', escHandler);
            }
        };
        document.addEventListener('keydown', escHandler);
    
        overlay.appendChild(modal);
        document.body.appendChild(overlay);
    }

    // 初始化
    function initialize() {
        // 确保DOM加载完成后再创建按钮
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                createFloatingButton();
            });
        } else {
            createFloatingButton();
        }
    }

    // 启动脚本
    initialize();
})();