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 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

// ==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
// @icon         https://raw.githubusercontent.com/MarSeventh/CloudFlare-ImgBed/refs/heads/main/logo.png
// ==/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();
})();