Image Uploader to Markdown to CloudFlare-ImgBed

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

当前为 2025-03-14 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==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: '![{filename}]({url})', // 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();
})();