Greasy Fork

Greasy Fork is available in English.

AI Image Description Generator

使用AI生成网页图片描述

当前为 2024-12-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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