您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Upload pasted images to CloudFlare-ImgBed and insert as markdown format. Support clipboard images and custom configuration. , CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
当前为
// ==UserScript== // @name Image Uploader to Markdown to CloudFlare-ImgBed // @namespace http://tampermonkey.net/ // @version 0.3 // @description Upload pasted images to CloudFlare-ImgBed and insert as markdown format. Support clipboard images and custom configuration. , CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed // @author calg // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function() { 'use strict'; // 默认配置信息 const DEFAULT_CONFIG = { AUTH_CODE: 'AUTH_CODE', // 替换为你的认证码 SERVER_URL: 'https://SERVER_URL', // 替换为实际的服务器地址 UPLOAD_PARAMS: { serverCompress: true, uploadChannel: 'telegram', // 可选 telegram 和 cfr2 autoRetry: true, uploadNameType: 'index', // 可选值为[default, index, origin, short] returnFormat: 'full', uploadFolder: 'apiupload' // 指定上传目录,用相对路径表示,例如上传到img/test目录需填img/test }, NOTIFICATION_DURATION: 3000, // 通知显示时间(毫秒) MARKDOWN_TEMPLATE: '', // Markdown 模板 AUTO_COPY_URL: false, // 是否自动复制URL到剪贴板 ALLOWED_HOSTS: ['*'], // 允许在哪些网站上运行,* 表示所有网站 MAX_FILE_SIZE: 5 * 1024 * 1024 // 最大文件大小(5MB) }; // 获取用户配置并确保所有必需的字段都存在 const userConfig = GM_getValue('userConfig', {}); let CONFIG = {}; // 深度合并配置 function mergeConfig(target, source) { for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { target[key] = target[key] || {}; mergeConfig(target[key], source[key]); } else { target[key] = source[key]; } } return target; } // 确保所有默认配置项都存在 CONFIG = mergeConfig({...DEFAULT_CONFIG}, userConfig); // 验证配置的完整性 function validateConfig() { if (!Array.isArray(CONFIG.ALLOWED_HOSTS)) { CONFIG.ALLOWED_HOSTS = DEFAULT_CONFIG.ALLOWED_HOSTS; } if (typeof CONFIG.NOTIFICATION_DURATION !== 'number') { CONFIG.NOTIFICATION_DURATION = DEFAULT_CONFIG.NOTIFICATION_DURATION; } if (typeof CONFIG.MAX_FILE_SIZE !== 'number') { CONFIG.MAX_FILE_SIZE = DEFAULT_CONFIG.MAX_FILE_SIZE; } if (typeof CONFIG.MARKDOWN_TEMPLATE !== 'string') { CONFIG.MARKDOWN_TEMPLATE = DEFAULT_CONFIG.MARKDOWN_TEMPLATE; } if (typeof CONFIG.AUTO_COPY_URL !== 'boolean') { CONFIG.AUTO_COPY_URL = DEFAULT_CONFIG.AUTO_COPY_URL; } } validateConfig(); // 添加通知样式 GM_addStyle(` .img-upload-notification { position: fixed; top: 20px; right: 20px; padding: 15px 20px; border-radius: 5px; z-index: 9999; max-width: 300px; font-size: 14px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; opacity: 0; transform: translateX(20px); } .img-upload-notification.show { opacity: 1; transform: translateX(0); } .img-upload-success { background-color: #4caf50; color: white; } .img-upload-error { background-color: #f44336; color: white; } .img-upload-info { background-color: #2196F3; color: white; } .img-upload-close { float: right; margin-left: 10px; cursor: pointer; opacity: 0.8; } .img-upload-close:hover { opacity: 1; } .img-upload-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10000; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; } .img-upload-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 9999; } .img-upload-modal h2 { margin: 0 0 20px; color: #333; font-size: 18px; } .img-upload-form-group { margin-bottom: 20px; } .img-upload-form-group label { display: block; margin-bottom: 8px; color: #333; font-weight: 500; } .img-upload-help-text { margin-top: 4px; color: #666; font-size: 12px; } .img-upload-form-group input[type="text"], .img-upload-form-group input[type="number"], .img-upload-form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .img-upload-form-group textarea { min-height: 100px; font-family: monospace; } .img-upload-form-group input[type="checkbox"] { margin-right: 8px; } .img-upload-buttons { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .img-upload-button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.2s; } .img-upload-button-primary { background: #2196F3; color: white; } .img-upload-button-secondary { background: #e0e0e0; color: #333; } .img-upload-button:hover { opacity: 0.9; } .img-upload-error { color: #ffffff; font-size: 12px; margin-top: 4px; } .img-upload-info-icon { display: inline-block; width: 16px; height: 16px; background: #2196F3; color: white; border-radius: 50%; text-align: center; line-height: 16px; font-size: 12px; margin-left: 4px; cursor: help; } .img-upload-form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; background-color: white; } .img-upload-input-group { display: flex; align-items: center; } .img-upload-input-group input { flex: 1; border-top-right-radius: 0; border-bottom-right-radius: 0; } .img-upload-input-group-text { padding: 8px 12px; background: #f5f5f5; border: 1px solid #ddd; border-left: none; border-radius: 0 4px 4px 0; color: #666; } .img-upload-checkbox-label { display: flex !important; align-items: center; font-weight: normal !important; } .img-upload-checkbox-label input { margin-right: 8px; } `); // 显示通知的函数 function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `img-upload-notification img-upload-${type}`; const closeBtn = document.createElement('span'); closeBtn.className = 'img-upload-close'; closeBtn.textContent = '✕'; closeBtn.onclick = () => removeNotification(notification); const messageSpan = document.createElement('span'); messageSpan.textContent = message; notification.appendChild(closeBtn); notification.appendChild(messageSpan); document.body.appendChild(notification); // 添加显示动画 setTimeout(() => notification.classList.add('show'), 10); // 自动消失 const timeout = setTimeout(() => removeNotification(notification), CONFIG.NOTIFICATION_DURATION); // 鼠标悬停时暂停消失 notification.addEventListener('mouseenter', () => clearTimeout(timeout)); notification.addEventListener('mouseleave', () => setTimeout(() => removeNotification(notification), 1000)); } // 移除通知 function removeNotification(notification) { notification.classList.remove('show'); setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); } // 复制文本到剪贴板 function copyToClipboard(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); showNotification('链接已复制到剪贴板!', 'success'); } catch (err) { showNotification('复制失败:' + err.message, 'error'); } document.body.removeChild(textarea); } // 检查文件大小 function checkFileSize(file) { if (file.size > CONFIG.MAX_FILE_SIZE) { showNotification(`文件大小超过限制(${Math.round(CONFIG.MAX_FILE_SIZE/1024/1024)}MB)`, 'error'); return false; } return true; } // 检查当前网站是否允许上传 function isAllowedHost() { const currentHost = window.location.hostname; return CONFIG.ALLOWED_HOSTS.includes('*') || CONFIG.ALLOWED_HOSTS.includes(currentHost); } // 监听所有文本输入区域的粘贴事件 function addPasteListener() { document.addEventListener('paste', async function(event) { if (!isAllowedHost()) return; const activeElement = document.activeElement; if (!activeElement || !['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) { return; } const items = event.clipboardData.items; let hasImage = false; for (let item of items) { if (item.type.startsWith('image/')) { hasImage = true; event.preventDefault(); const blob = item.getAsFile(); if (!checkFileSize(blob)) { return; } showNotification('正在上传图片,请稍候...', 'info'); await uploadImage(blob, activeElement); break; } } if (!hasImage) { return; } }); } // 上传图片 async function uploadImage(blob, targetElement) { const formData = new FormData(); const filename = `pasted-image-${Date.now()}.png`; formData.append('file', blob, filename); const queryParams = new URLSearchParams({ authCode: CONFIG.AUTH_CODE, ...CONFIG.UPLOAD_PARAMS }).toString(); try { GM_xmlhttpRequest({ method: 'POST', url: `${CONFIG.SERVER_URL}/upload?${queryParams}`, data: formData, onload: function(response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText); if (result && result.length > 0) { const imageUrl = result[0].src; insertMarkdownImage(imageUrl, targetElement, filename); showNotification('图片上传成功!', 'success'); if (CONFIG.AUTO_COPY_URL) { copyToClipboard(imageUrl); } } else { showNotification('上传成功但未获取到图片链接,请检查服务器响应', 'error'); } } catch (e) { showNotification('解析服务器响应失败:' + e.message, 'error'); } } else { let errorMsg = '上传失败'; try { const errorResponse = JSON.parse(response.responseText); errorMsg += ':' + (errorResponse.message || response.statusText); } catch (e) { errorMsg += `(状态码:${response.status})`; } showNotification(errorMsg, 'error'); } }, onerror: function(error) { showNotification('网络错误:无法连接到图床服务器', 'error'); } }); } catch (error) { showNotification('上传过程发生错误:' + error.message, 'error'); } } // 在输入框中插入 Markdown 格式的图片链接 function insertMarkdownImage(imageUrl, element, filename) { const markdownImage = CONFIG.MARKDOWN_TEMPLATE .replace('{url}', imageUrl) .replace('{filename}', filename.replace(/\.[^/.]+$/, '')); // 移除文件扩展名 const start = element.selectionStart; const end = element.selectionEnd; const text = element.value; element.value = text.substring(0, start) + markdownImage + text.substring(end); element.selectionStart = element.selectionEnd = start + markdownImage.length; element.focus(); } // 创建配置界面 function createConfigModal() { const overlay = document.createElement('div'); overlay.className = 'img-upload-modal-overlay'; const modal = document.createElement('div'); modal.className = 'img-upload-modal'; const content = ` <h2>图床上传配置</h2> <form id="img-upload-config-form"> <div class="img-upload-form-group"> <label>认证码</label> <input type="text" name="AUTH_CODE" value="${CONFIG.AUTH_CODE}" required> <div class="img-upload-help-text">用于验证上传请求的密钥</div> </div> <div class="img-upload-form-group"> <label>服务器地址</label> <input type="text" name="SERVER_URL" value="${CONFIG.SERVER_URL}" required> <div class="img-upload-help-text">图床服务器的URL地址</div> </div> <div class="img-upload-form-group"> <label>上传通道</label> <select name="uploadChannel"> <option value="cfr2" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'cfr2' ? 'selected' : ''}>CloudFlare R2</option> <option value="telegram" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'telegram' ? 'selected' : ''}>Telegram</option> </select> <div class="img-upload-help-text">选择图片上传的存储通道</div> </div> <div class="img-upload-form-group"> <label>文件命名方式</label> <select name="uploadNameType"> <option value="default" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'default' ? 'selected' : ''}>默认(前缀_原名)</option> <option value="index" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'index' ? 'selected' : ''}>仅前缀</option> <option value="origin" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'origin' ? 'selected' : ''}>仅原名</option> <option value="short" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'short' ? 'selected' : ''}>短链接</option> </select> <div class="img-upload-help-text">选择上传后的文件命名方式</div> </div> <div class="img-upload-form-group"> <label>上传目录</label> <input type="text" name="uploadFolder" value="${CONFIG.UPLOAD_PARAMS.uploadFolder}"> <div class="img-upload-help-text">指定上传目录,使用相对路径,例如:img/test</div> </div> <div class="img-upload-form-group"> <label>通知显示时间</label> <input type="number" name="NOTIFICATION_DURATION" value="${CONFIG.NOTIFICATION_DURATION}" min="1000" step="500"> <div class="img-upload-help-text">通知消息显示的时间(毫秒)</div> </div> <div class="img-upload-form-group"> <label>Markdown模板</label> <input type="text" name="MARKDOWN_TEMPLATE" value="${CONFIG.MARKDOWN_TEMPLATE}"> <div class="img-upload-help-text">支持 {filename} 和 {url} 两个变量</div> </div> <div class="img-upload-form-group"> <label>允许的网站</label> <input type="text" name="ALLOWED_HOSTS" value="${CONFIG.ALLOWED_HOSTS.join(',')}"> <div class="img-upload-help-text">输入域名,用逗号分隔。使用 * 表示允许所有网站</div> </div> <div class="img-upload-form-group"> <label>最大文件大小</label> <div class="img-upload-input-group"> <input type="number" name="MAX_FILE_SIZE" value="${CONFIG.MAX_FILE_SIZE / 1024 / 1024}" min="1" step="1"> <span class="img-upload-input-group-text">MB</span> </div> </div> <div class="img-upload-form-group"> <label class="img-upload-checkbox-label"> <input type="checkbox" name="AUTO_COPY_URL" ${CONFIG.AUTO_COPY_URL ? 'checked' : ''}> 自动复制URL到剪贴板 </label> </div> <div class="img-upload-buttons"> <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-cancel">取消</button> <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-reset">重置默认值</button> <button type="submit" class="img-upload-button img-upload-button-primary">保存</button> </div> </form> `; modal.innerHTML = content; document.body.appendChild(overlay); document.body.appendChild(modal); // 事件处理 const form = modal.querySelector('#img-upload-config-form'); const cancelBtn = modal.querySelector('#img-upload-cancel'); const resetBtn = modal.querySelector('#img-upload-reset'); function closeModal() { document.body.removeChild(overlay); document.body.removeChild(modal); } overlay.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); resetBtn.addEventListener('click', () => { if (confirm('确定要重置所有配置到默认值吗?')) { CONFIG = {...DEFAULT_CONFIG}; GM_setValue('userConfig', {}); showNotification('配置已重置为默认值!', 'success'); closeModal(); } }); form.addEventListener('submit', (e) => { e.preventDefault(); try { const formData = new FormData(form); const newConfig = { AUTH_CODE: formData.get('AUTH_CODE'), SERVER_URL: formData.get('SERVER_URL'), UPLOAD_PARAMS: { ...DEFAULT_CONFIG.UPLOAD_PARAMS, uploadChannel: formData.get('uploadChannel'), uploadNameType: formData.get('uploadNameType'), uploadFolder: formData.get('uploadFolder') }, NOTIFICATION_DURATION: parseInt(formData.get('NOTIFICATION_DURATION')), MARKDOWN_TEMPLATE: formData.get('MARKDOWN_TEMPLATE'), ALLOWED_HOSTS: formData.get('ALLOWED_HOSTS').split(',').map(h => h.trim()), MAX_FILE_SIZE: parseFloat(formData.get('MAX_FILE_SIZE')) * 1024 * 1024, AUTO_COPY_URL: formData.get('AUTO_COPY_URL') === 'on' }; CONFIG = mergeConfig({...DEFAULT_CONFIG}, newConfig); GM_setValue('userConfig', CONFIG); showNotification('配置已更新!', 'success'); closeModal(); } catch (error) { showNotification('配置格式错误:' + error.message, 'error'); } }); // 防止点击模态框时关闭 modal.addEventListener('click', (e) => e.stopPropagation()); } // 修改注册配置菜单函数 function registerMenuCommands() { GM_registerMenuCommand('配置图床参数', createConfigModal); } // 初始化 function init() { if (!isAllowedHost()) return; addPasteListener(); registerMenuCommands(); } init(); })();