您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
检测粘贴的图片,自动上传至SkyImg并根据域名配置返回不同格式的链接
当前为
// ==UserScript== // @name 粘贴图片自动上传图床 // @namespace https://skyimg.de/ // @version 1.2.3 // @license MIT // @author skyimg.de // @connect skyimg.de // @description 检测粘贴的图片,自动上传至SkyImg并根据域名配置返回不同格式的链接 // @match *://*/* // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // 全局配置参数 const config = { // 上传 API 接口地址 apiUrl: 'https://skyimg.de/api/upload', // 是否转换为 WebP 格式 webp: true, notificationDuration: { info: 3000, success: 3500, warning: 5000, error: 5000 }, // 链接格式配置 linkFormats: { url: 'URL格式', // 纯 URL 链接 md: 'Markdown格式', // Markdown 格式:  bbc: 'BBCode格式' // BBCode 格式: [img]URL[/img] } }; // 链接格式设置,默认使用 Markdown 格式 let domainFormatSettings = GM_getValue('domainFormatSettings', { 'default': 'md' }); // Token 设置 let uploadToken = GM_getValue('uploadToken', ''); // 排除网址设置,默认包含常见不适用的网站 let exclusionList = GM_getValue('excludeSites', ["*.google.com", "*.bing.com", "*.skyimg.de", "*.baidu.com", "*.youtube.com", "*.x.com", "*.instagram.com", "*.facebook.com", "*.linux.do", "*.douyin.com", "*.*.*.*"]); GM_registerMenuCommand('配置链接格式', showFormatSettingsDialog); GM_registerMenuCommand('配置上传 Token', showTokenSettingsDialog); GM_registerMenuCommand('配置排除网址', showExclusionSettingsDialog); // 若当前站点匹配排除规则,则主功能不启用(但菜单命令依然可用) if (isCurrentSiteExcluded()) { console.log("当前网站匹配排除规则,图片上传功能不启用。"); return; } // 监听粘贴事件 document.addEventListener('paste', function(e) { // 获取剪贴板数据 const clipboardData = e.clipboardData || window.clipboardData; if (!clipboardData) return; // 遍历剪贴板项,查找是否存在图像数据 const items = clipboardData.items; let imageFile = null; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { imageFile = items[i].getAsFile(); break; } } // 如果剪贴板中没有图像,则返回不处理 if (!imageFile) return; // 判断当前光标是否位于有效的输入区域中 const activeElement = document.activeElement; if (!isValidInputField(activeElement)) { showToast('当前不在发帖界面,无法上传图片', 'warning'); return; } // 阻止默认粘贴行为 e.preventDefault(); e.stopPropagation(); // 上传图片至 API uploadImage(imageFile, activeElement); }); /** * 检查当前网址是否符合排除规则 * @returns {boolean} */ function isCurrentSiteExcluded() { const hostname = window.location.hostname; for (let pattern of exclusionList) { const regexStr = '^' + pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\\\*/g, '.*') + '$'; const regex = new RegExp(regexStr); if (regex.test(hostname)) { return true; } } return false; } /** * 配置对话框:链接格式设置 */ function showFormatSettingsDialog() { const dialog = document.createElement('div'); dialog.style.position = 'fixed'; dialog.style.top = '50%'; dialog.style.left = '50%'; dialog.style.transform = 'translate(-50%, -50%)'; dialog.style.backgroundColor = '#fff'; dialog.style.padding = '20px'; dialog.style.borderRadius = '8px'; dialog.style.boxShadow = '0 4px 23px 0 rgba(0, 0, 0, 0.2)'; dialog.style.maxWidth = '500px'; dialog.style.width = '90%'; dialog.style.maxHeight = '80vh'; dialog.style.overflowY = 'auto'; dialog.style.zIndex = '10000'; dialog.style.fontFamily = 'Arial, sans-serif'; // 对话框标题 const title = document.createElement('h2'); title.textContent = '配置链接格式'; title.style.margin = '0 0 15px 0'; title.style.color = '#333'; dialog.appendChild(title); // 说明文本 const desc = document.createElement('p'); desc.textContent = '为不同域名配置粘贴图片后生成的链接格式。不在列表中的域名将使用默认格式。域名支持通配符匹配,例如:*.example.com'; desc.style.marginBottom = '15px'; desc.style.color = '#666'; dialog.appendChild(desc); // 默认格式设置 const defaultSection = document.createElement('div'); defaultSection.style.marginBottom = '20px'; defaultSection.style.padding = '10px'; defaultSection.style.backgroundColor = '#f5f5f5'; defaultSection.style.borderRadius = '4px'; const defaultLabel = document.createElement('label'); defaultLabel.textContent = '默认格式:'; defaultLabel.style.fontWeight = 'bold'; defaultLabel.style.display = 'block'; defaultLabel.style.marginBottom = '5px'; defaultSection.appendChild(defaultLabel); const defaultSelect = document.createElement('select'); defaultSelect.style.width = '100%'; defaultSelect.style.padding = '8px'; defaultSelect.style.borderRadius = '4px'; defaultSelect.style.border = '1px solid #ddd'; for (const [value, text] of Object.entries(config.linkFormats)) { const option = document.createElement('option'); option.value = value; option.textContent = text; if (domainFormatSettings['default'] === value) { option.selected = true; } defaultSelect.appendChild(option); } defaultSection.appendChild(defaultSelect); dialog.appendChild(defaultSection); // 现有域名配置列表 const domainList = document.createElement('div'); domainList.style.marginBottom = '20px'; Object.entries(domainFormatSettings).forEach(([domain, format]) => { if (domain !== 'default') { const domainRow = createDomainRow(domain, format); domainList.appendChild(domainRow); } }); dialog.appendChild(domainList); const addButton = document.createElement('button'); addButton.textContent = '添加新域名'; addButton.style.backgroundColor = '#4CAF50'; addButton.style.color = 'white'; addButton.style.border = 'none'; addButton.style.padding = '10px 15px'; addButton.style.borderRadius = '4px'; addButton.style.cursor = 'pointer'; addButton.style.marginRight = '10px'; addButton.addEventListener('click', () => { const newDomain = prompt('请输入域名(例如:example.com 或 *.example.com):'); if (newDomain && newDomain.trim() !== '' && newDomain !== 'default') { if (!domainFormatSettings[newDomain]) { domainFormatSettings[newDomain] = domainFormatSettings['default']; const domainRow = createDomainRow(newDomain, domainFormatSettings[newDomain]); domainList.appendChild(domainRow); } else { alert('该域名已存在!'); } } }); dialog.appendChild(addButton); // 保存按钮 const saveButton = document.createElement('button'); saveButton.textContent = '保存设置'; saveButton.style.backgroundColor = '#2196F3'; saveButton.style.color = 'white'; saveButton.style.border = 'none'; saveButton.style.padding = '10px 15px'; saveButton.style.borderRadius = '4px'; saveButton.style.cursor = 'pointer'; saveButton.addEventListener('click', () => { domainFormatSettings['default'] = defaultSelect.value; GM_setValue('domainFormatSettings', domainFormatSettings); document.body.removeChild(overlay); showToast('设置已保存', 'success'); }); dialog.appendChild(saveButton); function createDomainRow(domain, format) { const row = document.createElement('div'); row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.marginBottom = '10px'; row.style.padding = '10px'; row.style.backgroundColor = '#f9f9f9'; row.style.borderRadius = '4px'; const domainText = document.createElement('div'); domainText.textContent = domain; domainText.style.flexGrow = '1'; domainText.style.marginRight = '10px'; row.appendChild(domainText); const formatSelect = document.createElement('select'); formatSelect.style.padding = '5px'; formatSelect.style.marginRight = '10px'; formatSelect.style.borderRadius = '4px'; formatSelect.style.border = '1px solid #ddd'; for (const [value, text] of Object.entries(config.linkFormats)) { const option = document.createElement('option'); option.value = value; option.textContent = text; if (format === value) { option.selected = true; } formatSelect.appendChild(option); } formatSelect.addEventListener('change', () => { domainFormatSettings[domain] = formatSelect.value; }); row.appendChild(formatSelect); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.style.backgroundColor = '#F44336'; deleteBtn.style.color = 'white'; deleteBtn.style.border = 'none'; deleteBtn.style.padding = '5px 10px'; deleteBtn.style.borderRadius = '4px'; deleteBtn.style.cursor = 'pointer'; deleteBtn.addEventListener('click', () => { delete domainFormatSettings[domain]; row.remove(); }); row.appendChild(deleteBtn); return row; } const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; overlay.style.zIndex = '9999'; overlay.appendChild(dialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); document.body.appendChild(overlay); } /** * 配置对话框:上传 Token 设置 */ function showTokenSettingsDialog() { const dialog = document.createElement('div'); dialog.style.position = 'fixed'; dialog.style.top = '50%'; dialog.style.left = '50%'; dialog.style.transform = 'translate(-50%, -50%)'; dialog.style.backgroundColor = '#fff'; dialog.style.padding = '20px'; dialog.style.borderRadius = '8px'; dialog.style.boxShadow = '0 4px 23px 0 rgba(0, 0, 0, 0.2)'; dialog.style.maxWidth = '400px'; dialog.style.width = '80%'; dialog.style.zIndex = '10000'; dialog.style.fontFamily = 'Arial, sans-serif'; const title = document.createElement('h2'); title.textContent = '配置上传 Token'; title.style.margin = '0 0 15px 0'; title.style.color = '#333'; dialog.appendChild(title); const desc = document.createElement('p'); desc.textContent = '请输入用于云端同步的 Token(64 位字母数字,留空则不使用)。可前往 skyimg.de 网站获取'; desc.style.marginBottom = '15px'; desc.style.color = '#666'; dialog.appendChild(desc); const tokenInput = document.createElement('input'); tokenInput.type = 'text'; tokenInput.placeholder = 'Token'; tokenInput.value = uploadToken; tokenInput.style.width = 'calc(100% - 20px)'; tokenInput.style.padding = '8px'; tokenInput.style.marginBottom = '15px'; tokenInput.style.borderRadius = '4px'; tokenInput.style.border = '1px solid #ddd'; dialog.appendChild(tokenInput); const saveButton = document.createElement('button'); saveButton.textContent = '保存'; saveButton.style.backgroundColor = '#2196F3'; saveButton.style.color = 'white'; saveButton.style.border = 'none'; saveButton.style.padding = '10px 15px'; saveButton.style.borderRadius = '4px'; saveButton.style.cursor = 'pointer'; saveButton.addEventListener('click', function() { const tokenVal = tokenInput.value.trim(); if (tokenVal !== '' && !/^[A-Za-z0-9]{64}$/.test(tokenVal)) { alert('Token 格式不正确,必须是 64 位的字母数字'); return; } uploadToken = tokenVal; GM_setValue('uploadToken', uploadToken); document.body.removeChild(overlay); showToast('Token 已保存', 'success'); }); dialog.appendChild(saveButton); const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; overlay.style.zIndex = '9999'; overlay.appendChild(dialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); document.body.appendChild(overlay); } /** * 配置对话框:排除网址设置 */ function showExclusionSettingsDialog() { const dialog = document.createElement('div'); dialog.style.position = 'fixed'; dialog.style.top = '50%'; dialog.style.left = '50%'; dialog.style.width = '500px'; dialog.style.marginLeft = '-250px'; dialog.style.marginTop = '-200px'; dialog.style.backgroundColor = '#ffffff'; dialog.style.padding = '20px'; dialog.style.borderRadius = '8px'; dialog.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.2)'; dialog.style.zIndex = '10000'; dialog.style.fontFamily = 'Arial, sans-serif'; dialog.style.color = '#333333'; // 标题 const title = document.createElement('h2'); title.textContent = '配置排除网址'; title.style.margin = '0 0 15px 0'; title.style.fontSize = '18px'; title.style.fontWeight = 'bold'; title.style.color = '#333333'; dialog.appendChild(title); // 说明文本 const desc = document.createElement('p'); desc.textContent = '请在下方文本框中输入网址规则,每行一个规则(支持通配符,如:*.example.com)'; desc.style.marginBottom = '15px'; desc.style.fontSize = '14px'; desc.style.lineHeight = '1.4'; desc.style.color = '#666666'; dialog.appendChild(desc); const textareaContainer = document.createElement('div'); textareaContainer.style.position = 'relative'; textareaContainer.style.width = '100%'; textareaContainer.style.height = '200px'; textareaContainer.style.marginBottom = '15px'; textareaContainer.style.border = '1px solid #cccccc'; textareaContainer.style.borderRadius = '4px'; textareaContainer.style.overflow = 'hidden'; dialog.appendChild(textareaContainer); // 文本区域 const textarea = document.createElement('textarea'); textarea.style.position = 'absolute'; textarea.style.top = '0'; textarea.style.left = '0'; textarea.style.width = '100%'; textarea.style.height = '100%'; textarea.style.padding = '10px'; textarea.style.boxSizing = 'border-box'; textarea.style.border = 'none'; textarea.style.outline = 'none'; textarea.style.resize = 'none'; textarea.style.fontFamily = 'Consolas, Monaco, "Courier New", monospace'; textarea.style.fontSize = '14px'; textarea.style.lineHeight = '1.5'; textarea.style.color = '#000000'; textarea.style.WebkitFontSmoothing = 'auto'; textarea.style.MozOsxFontSmoothing = 'auto'; textarea.style.transition = 'none'; textarea.style.textAlign = 'left'; textarea.value = exclusionList.join('\n'); textareaContainer.appendChild(textarea); const btnContainer = document.createElement('div'); btnContainer.style.display = 'flex'; btnContainer.style.justifyContent = 'flex-end'; btnContainer.style.gap = '10px'; btnContainer.style.marginTop = '15px'; dialog.appendChild(btnContainer); const saveButton = document.createElement('button'); saveButton.textContent = '保存设置'; saveButton.style.padding = '8px 16px'; saveButton.style.backgroundColor = '#2196F3'; saveButton.style.color = '#ffffff'; saveButton.style.border = 'none'; saveButton.style.borderRadius = '4px'; saveButton.style.cursor = 'pointer'; saveButton.style.fontSize = '14px'; saveButton.style.fontWeight = 'normal'; saveButton.addEventListener('click', () => { let newRules = textarea.value.split('\n').map(rule => rule.trim()).filter(rule => rule !== ''); exclusionList = newRules; GM_setValue('excludeSites', exclusionList); document.body.removeChild(overlay); showToast('排除网址设置已保存', 'success'); }); btnContainer.appendChild(saveButton); const cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.style.padding = '8px 16px'; cancelButton.style.backgroundColor = '#F44336'; cancelButton.style.color = '#ffffff'; cancelButton.style.border = 'none'; cancelButton.style.borderRadius = '4px'; cancelButton.style.cursor = 'pointer'; cancelButton.style.fontSize = '14px'; cancelButton.style.fontWeight = 'normal'; cancelButton.addEventListener('click', () => { document.body.removeChild(overlay); }); btnContainer.appendChild(cancelButton); const overlay = document.createElement('div'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; overlay.style.zIndex = '9999'; overlay.appendChild(dialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); document.body.appendChild(overlay); // 加载后让textarea获得焦点 setTimeout(() => textarea.focus(), 100); } /** * 上传图片到 API 并处理响应 * @param {File} file - 上传的图片文件 * @param {Element} targetElement - 要插入链接的目标输入框 */ function uploadImage(file, targetElement) { showToast('正在上传图片...', 'info'); const formData = new FormData(); formData.append('file', file); // 根据配置决定是否调用 webp 转换 const url = config.webp ? `${config.apiUrl}?webp=true` : config.apiUrl; GM_xmlhttpRequest({ method: 'POST', url: url, data: formData, responseType: 'json', headers: (uploadToken && /^[A-Za-z0-9]{64}$/.test(uploadToken)) ? { 'x-sync-token': uploadToken } : {}, onload: function(response) { handleUploadResponse(response, targetElement); }, onerror: function(error) { console.error('上传失败:', error); showToast('图片上传失败,请重试', 'error'); } }); } /** * 处理上传响应,生成相应格式的图片链接并插入目标输入框 */ function handleUploadResponse(response, targetElement) { if (response.status !== 200 || !response.response || !Array.isArray(response.response) || response.response.length === 0) { showToast('图片上传失败:服务器响应异常', 'error'); return; } const imageData = response.response[0]; if (!imageData.url) { showToast('图片上传失败:响应数据不完整', 'error'); return; } // 根据当前域名及配置决定链接格式(支持通配符匹配) const currentDomain = window.location.hostname; let formatType = domainFormatSettings[currentDomain]; if (!formatType) { for (const key in domainFormatSettings) { if (key === 'default') continue; if (key.indexOf('*') !== -1) { const escaped = key.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); const pattern = '^' + escaped.replace(/\*/g, '.*') + '$'; const regex = new RegExp(pattern); if (regex.test(currentDomain)) { formatType = domainFormatSettings[key]; break; } } } } formatType = formatType || domainFormatSettings['default']; const imageUrl = imageData.url; let formattedLink; switch(formatType) { case 'url': formattedLink = imageUrl; break; case 'bbc': formattedLink = `[img]${imageUrl}[/img]`; break; case 'md': default: formattedLink = ``; break; } // 将生成的链接插入到目标输入框内 insertTextToElement(targetElement, formattedLink); showToast('图片上传成功!', 'success'); } /** * 向目标元素插入文本 */ function insertTextToElement(element, text) { if (!element) return; // 针对标准输入框处理 if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') { const startPos = element.selectionStart || 0; const endPos = element.selectionEnd || startPos; const beforeText = element.value.substring(0, startPos); const afterText = element.value.substring(endPos); element.value = beforeText + text + afterText; const newCursorPos = startPos + text.length; element.selectionStart = newCursorPos; element.selectionEnd = newCursorPos; element.focus(); return; } // 针对 contenteditable 元素处理 if (element.getAttribute('contenteditable') === 'true') { document.execCommand('insertText', false, text); return; } // 针对 CodeMirror 编辑器处理 const codeMirrorLine = element.closest('.CodeMirror-line') || element; if (codeMirrorLine.classList.contains('CodeMirror-line') || element.closest('.CodeMirror')) { const codeMirrorElement = element.closest('.CodeMirror'); if (codeMirrorElement && codeMirrorElement.CodeMirror) { const cm = codeMirrorElement.CodeMirror; const doc = cm.getDoc(); const cursor = doc.getCursor(); doc.replaceRange(text, cursor); } else { try { document.execCommand('insertText', false, text); } catch (e) { console.error('无法插入文本:', e); showToast('无法自动插入图片链接,请手动复制', 'warning'); copyToClipboard(text); } } return; } // 其他情况下,尝试 execCommand 插入文本,否则复制到剪贴板 try { document.execCommand('insertText', false, text); } catch (e) { showToast('无法自动插入图片链接,已复制到剪贴板', 'warning'); copyToClipboard(text); } } /** * 将文本复制到剪贴板 */ 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(); document.execCommand('copy'); document.body.removeChild(textarea); } /** * 使用 Toast 显示通知(若 GM_notification 可用,则优先使用) */ function showToast(message, type = 'info') { const duration = config.notificationDuration[type] || 3000; if (typeof GM_notification !== 'undefined') { GM_notification({ text: message, title: '图片上传', timeout: duration }); return; } let toast = document.getElementById('skyimg-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'skyimg-toast'; toast.style.position = 'fixed'; toast.style.bottom = '20px'; toast.style.right = '20px'; toast.style.padding = '10px 15px'; toast.style.borderRadius = '4px'; toast.style.fontSize = '14px'; toast.style.zIndex = '9999'; toast.style.transition = 'opacity 0.3s ease-in-out'; document.body.appendChild(toast); } switch(type) { case 'success': toast.style.backgroundColor = '#4CAF50'; toast.style.color = 'white'; break; case 'warning': toast.style.backgroundColor = '#FF9800'; toast.style.color = 'white'; break; case 'error': toast.style.backgroundColor = '#F44336'; toast.style.color = 'white'; break; default: toast.style.backgroundColor = '#2196F3'; toast.style.color = 'white'; break; } toast.textContent = message; toast.style.opacity = '1'; clearTimeout(toast.hideTimeout); toast.hideTimeout = setTimeout(() => { toast.style.opacity = '0'; }, duration); } })();