// ==UserScript== // @name AI Image Description Generator // @namespace http://tampermonkey.net/ // @version 3.0 // @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 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 全局变量 let isSelectionMode = false; // 定义支持的视觉模型列表 const supportedVLModels = [ 'Qwen/Qwen2-VL-72B-Instruct', 'Pro/Qwen/Qwen2-VL-7B-Instruct', 'OpenGVLab/InternVL2-Llama3-76B', 'OpenGVLab/InternVL2-26B', 'Pro/OpenGVLab/InternVL2-8B' ]; // 定义GLM-4V系列模型 const glm4vModels = [ 'glm-4v', 'glm-4v-flash' ]; // 添加样式 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 12px; border-radius: 4px; font-size: 14px; line-height: 1.4; max-width: 300px; text-align: center; word-wrap: break-word; z-index: 10000; pointer-events: none; animation: fadeIn 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } @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: 1000px; max-height: 540px; overflow-y: auto; width: 90%; } .ai-result-modal h3 { margin: 0 0 10px 0; font-size: 14px; color: #333; } .ai-result-modal .description-code { background: #1e1e1e; // 深色背景 color: #ffffff; // 白色文字 padding: 12px; border-radius: 4px; margin: 5px 0; cursor: pointer; white-space: pre-wrap; word-wrap: break-word; font-family: monospace; border: 1px solid #333; // 深色边框 position: relative; max-height: 60px; overflow-y: auto; font-size: 12px; line-height: 1.4; } .ai-result-modal .description-code:hover { background: #2d2d2d; // 悬停时稍微变亮 } .ai-result-modal .copy-hint { font-size: 11px; color: #666; text-align: center; margin: 2px 0; } .ai-result-modal .close-button { position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 18px; cursor: pointer; color: #666; padding: 2px 6px; line-height: 1; } .ai-result-modal .close-button:hover { color: #333; } .ai-progress-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: 10001; min-width: 500px; height: auto; } .ai-progress-modal .progress-bar { width: 100%; height: 20px; background-color: #f3f3f3; } .ai-selection-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; cursor: crosshair; pointer-events: none; } .ai-selecting-image img { position: relative; z-index: 9999; cursor: pointer !important; transition: outline 0.2s ease; } .ai-selecting-image img:hover { outline: 2px solid white; outline-offset: 2px; } .ai-result-modal .balance-info { font-size: 9px; color: #666; text-align: right; margin-top: 3px; padding-top: 3px; border-top: 1px solid #eee; } `); // 密码显示切换功能 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 { // 如果是智谱AI的endpoint,跳过API检查 if(apiEndpoint.includes('bigmodel.cn')) { return true; } // 其他endpoint进行API检查 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; } } // 获取图片的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, timestamp: new Date().toISOString() }); try { const base64Image = await getImageBase64(imageUrl); console.log('[Debug] Image converted to base64, length:', base64Image.length); // 退出选择图片模式 exitImageSelectionMode(); const timeout = 30000; // 30秒超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const imageSize = base64Image.length * 0.75; // 转换为字节数 // 获取当前余额 const userInfo = await checkUserInfo(apiEndpoint, apiKey); const currentBalance = userInfo.totalBalance; // 计算每次调用的预估花费(根据图片大小和模型) const costPerCall = calculateCost(imageSize, selectedModel); // 计算可识别的剩余图片数量 const remainingImages = Math.floor(currentBalance / costPerCall); // 根据不同的API构建不同的请求体和endpoint let requestBody; let finalEndpoint; if(selectedModel.startsWith('glm-')) { // GLM系列模型的请求格式 requestBody = { model: selectedModel, messages: [{ role: "user", content: [{ type: "text", text: "请描述这张图片的主要内容。如果是人物图片,请至少用15个字描述人物。" }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${base64Image}` } }] }], stream: true }; finalEndpoint = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'; } else { // 原有模型的请求格式 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 }; finalEndpoint = `${apiEndpoint}/chat/completions`; } console.log('[Debug] API Request body:', JSON.stringify(requestBody, null, 2)); console.log('[Debug] Sending request to:', finalEndpoint); console.log('[Debug] Request headers:', { 'Authorization': 'Bearer ***' + apiKey.slice(-4), 'Content-Type': 'application/json' }); console.log('[Debug] Request body:', requestBody); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: finalEndpoint, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, data: JSON.stringify(requestBody), onload: function(response) { console.log('[Debug] Response received:', { status: response.status, statusText: response.statusText, headers: response.responseHeaders }); if (response.status === 200) { try { let description = ''; const lines = response.responseText.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.slice(6); if (jsonStr === '[DONE]') continue; try { const jsonData = JSON.parse(jsonStr); console.log('[Debug] Parsed chunk:', jsonData); const content = jsonData.choices[0]?.delta?.content; if (content) { description += content; console.log('[Debug] Current description:', description); } } catch (e) { console.error('[Debug] Error parsing chunk JSON:', e); } } } console.log('[Debug] Final description:', description); removeDescriptionTooltip(); const balanceInfo = `剩余额度为:${currentBalance.toFixed(4)},大约还可以识别 ${remainingImages} 张图片`; showDescriptionModal(description, balanceInfo); resolve(description); } catch (error) { console.error('[Debug] Error processing response:', error); reject(error); } } else { console.error('[Debug] Error response:', { status: response.status, statusText: response.statusText, response: response.responseText }); reject(new Error(`Request failed with status ${response.status}`)); } }, onerror: function(error) { console.error('[Debug] Request error:', error); reject(error); }, onprogress: function(progress) { // 用于处理流式响应的进度 console.log('[Debug] Progress:', progress); try { const lines = progress.responseText.split('\n').filter(line => line.trim() !== ''); let latestContent = ''; for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.slice(6); if (jsonStr === '[DONE]') continue; try { const jsonData = JSON.parse(jsonStr); const content = jsonData.choices[0]?.delta?.content; if (content) { latestContent += content; } } catch (e) { console.error('[Debug] Error parsing progress JSON:', e); } } } if (latestContent) { updateDescriptionTooltip('正在生成描述: ' + latestContent); } } catch (error) { console.error('[Debug] Error processing progress:', error); } } }); }); } catch (error) { if (error.name === 'AbortError') { showToast('请求超时,请重试'); } removeDescriptionTooltip(); console.error('[Debug] Error in getImageDescription:', { error, stack: error.stack, timestamp: new Date().toISOString() }); throw error; } } // 显示描述tooltip function showDescriptionTooltip(description) { const tooltip = document.createElement('div'); tooltip.className = 'ai-image-description'; tooltip.textContent = description; // 获取视口宽度 const viewportWidth = window.innerWidth || document.documentElement.clientWidth; // 计算tooltip位置(水平居中,距顶部20px) const tooltipX = Math.max(0, (viewportWidth - 300) / 2); // 300是tooltip的max-width tooltip.style.position = 'fixed'; tooltip.style.left = `${tooltipX}px`; tooltip.style.top = '20px'; document.body.appendChild(tooltip); // 添加动态点的动画 let dots = 1; const updateInterval = setInterval(() => { if (!document.body.contains(tooltip)) { clearInterval(updateInterval); return; } dots = dots % 6 + 1; tooltip.textContent = '正在生成描述' + '.'.repeat(dots); }, 500); // 每500ms更新一次 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'); isSelectionMode = true; // 创建遮罩层 const overlay = document.createElement('div'); overlay.className = 'ai-selection-overlay'; document.body.appendChild(overlay); // 添加选择状态的类名 document.body.classList.add('ai-selecting-image'); // 创建点击事件处理函数 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('正在生成描述...'); try { await getImageDescription(e.target.src, endpoint, apiKey, selectedModel); } catch (error) { console.error('[Debug] Description generation failed:', error); removeDescriptionTooltip(); showToast('生成描述失败: ' + error.message); } } }; // 添加点击事件监听器 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'); isSelectionMode = false; // 移除遮罩层 const overlay = document.querySelector('.ai-selection-overlay'); if (overlay) { overlay.remove(); } // 移除选择状态的类名 document.body.classList.remove('ai-selecting-image'); // 移除所有事件监听器 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 { // 对智谱AI的endpoint返回默认值 if(apiEndpoint.includes('bigmodel.cn')) { const defaultUserData = { name: 'GLM User', balance: 1000, // 默认余额 chargeBalance: 0, totalBalance: 1000 }; console.log('[Debug] Using default user data for GLM:', defaultUserData); return defaultUserData; } // 其他endpoint使用原有逻辑 return new Promise((resolve, reject) => { console.log('[Debug] Sending user info request to:', `${apiEndpoint}/v1/user/info`); GM_xmlhttpRequest({ method: 'GET', url: `${apiEndpoint}/v1/user/info`, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, onload: function(response) { console.log('[Debug] User Info Raw Response:', { status: response.status, statusText: response.statusText, responseText: response.responseText, headers: response.responseHeaders }); if (response.status === 200) { try { const result = JSON.parse(response.responseText); console.log('[Debug] User Info Parsed Response:', result); if (result.code === 20000 && result.status && result.data) { const { name, balance, chargeBalance, totalBalance } = result.data; resolve({ name, balance: parseFloat(balance), chargeBalance: parseFloat(chargeBalance), totalBalance: parseFloat(totalBalance) }); } else { throw new Error(result.message || 'Invalid response format'); } } catch (error) { console.error('[Debug] JSON Parse Error:', error); reject(error); } } else { console.error('[Debug] HTTP Error Response:', { status: response.status, statusText: response.statusText, response: response.responseText }); reject(new Error(`HTTP error! status: ${response.status}`)); } }, onerror: function(error) { console.error('[Debug] Request Error:', error); reject(error); } }); }); } catch (error) { console.error('[Debug] User Info Error:', error); throw error; } } // 获取可用模型列表 async function getAvailableModels(apiEndpoint, apiKey) { console.log('[Debug] Getting available models from:', apiEndpoint); try { // 如果是智谱AI的endpoint,直接返回GLM模型列表 if(apiEndpoint.includes('bigmodel.cn')) { const glmModels = [ { id: 'glm-4', name: 'GLM-4' }, { id: 'glm-4v', name: 'GLM-4V' }, { id: 'glm-4v-flash', name: 'GLM-4V-Flash' } ]; console.log('[Debug] Available GLM models:', glmModels); return glmModels; } // 其他endpoint使用原有逻辑 return new Promise((resolve, reject) => { console.log('[Debug] Sending models request to:', `${apiEndpoint}/v1/models`); GM_xmlhttpRequest({ method: 'GET', url: `${apiEndpoint}/v1/models`, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, onload: function(response) { console.log('[Debug] Models API Raw Response:', { status: response.status, statusText: response.statusText, responseText: response.responseText, headers: response.responseHeaders }); if (response.status === 200) { try { const result = JSON.parse(response.responseText); console.log('[Debug] Models API Parsed 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] Filtered and processed models:', models); resolve(models); } else { console.error('[Debug] Invalid models response format:', result); reject(new Error('Invalid models response format')); } } catch (error) { console.error('[Debug] JSON Parse Error:', error); reject(error); } } else { console.error('[Debug] HTTP Error Response:', { status: response.status, statusText: response.statusText, response: response.responseText }); reject(new Error(`HTTP error! status: ${response.status}`)); } }, onerror: function(error) { console.error('[Debug] Models API Request Error:', error); reject(error); } }); }); } catch (error) { console.error('[Debug] Models API Error:', error); throw error; } } // 更新模型下拉菜单 function updateModelSelect(selectElement, models) { if (models.length === 0) { selectElement.innerHTML = ''; selectElement.disabled = true; return; } selectElement.innerHTML = '' + models.map(model => `` ).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 = ` `; // 设置初始位置 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 hasMoved = false; let startX, startY; let initialLeft, initialTop; // 添加点击事件处理 btn.addEventListener('click', function(e) { if (e.button === 0 && !hasMoved) { // 左键点击且没有移动 enterImageSelectionMode(); e.stopPropagation(); // 阻止事件冒泡 } // 重置移动标志 hasMoved = false; }); // 右键点击显示配置 btn.addEventListener('contextmenu', function(e) { e.preventDefault(); exitImageSelectionMode(); createConfigUI(); }); // 拖拽相关事件 function dragStart(e) { if (e.target === btn || btn.contains(e.target)) { isDragging = true; hasMoved = false; 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; if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { hasMoved = true; } 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); // 将按钮添加到文档中 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 = `
${description}