ImgBed Markdown Uploader

适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         ImgBed Markdown Uploader
// @name:zh-CN   ImgBed Markdown 上传
// @namespace    http://tampermonkey.net/
// @version      0.5.0-beta
// @description  适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
// @description:zh-CN 适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
// @description:zh-cn 适配于 CloudFlare-ImgBed 的粘贴上传并生成markdown的脚本, CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
// @author       calg
// @match        https://*.linux.do/*
// @match        https://*.nodeseek.com/*
// @exclude      *://*/*.jpg*
// @exclude      *://*/*.jpeg*
// @exclude      *://*/*.png*
// @exclude      *://*/*.gif*
// @exclude      *://*/*.webp*
// @exclude      *://*/*.pdf*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_info
// @grant        GM_unregisterMenuCommand
// @grant        GM_log
// @license      MIT
// @icon         https://raw.githubusercontent.com/MarSeventh/CloudFlare-ImgBed/refs/heads/main/logo.png
// ==/UserScript==

(function () {
    'use strict';

    // 防止重复注入
    if (window.imageUploaderInitialized) {
        return;
    }
    window.imageUploaderInitialized = true;

    // 创建一个唯一的命名空间
    const SCRIPT_NAMESPACE = 'image_uploader_' + GM_info.script.version.replace(/\./g, '_');

    // 存储菜单命令ID
    let menuCommandId = null;

    const MENU_LABEL_CONFIG = '配置图床参数';

    // 简单的字符混淆处理 (保护 API Key)
    function obfuscate(str) {
        if (!str || str === 'AUTH_CODE') return str;
        try {
            const shifted = str.split('').map(c => String.fromCharCode(c.charCodeAt(0) + 5)).join('');
            return btoa(shifted);
        } catch (e) { return str; }
    }

    function deobfuscate(str) {
        if (!str || str === 'AUTH_CODE') return str;
        try {
            const decoded = atob(str);
            return decoded.split('').map(c => String.fromCharCode(c.charCodeAt(0) - 5)).join('');
        } catch (e) { return str; }
    }

    // 检查是否已经存在事件监听器
    function hasEventListener(element, eventName) {
        const key = `${SCRIPT_NAMESPACE}_${eventName}`;
        return element[key] === true;
    }

    // 标记事件监听器已添加
    function markEventListener(element, eventName) {
        const key = `${SCRIPT_NAMESPACE}_${eventName}`;
        element[key] = true;
    }

    function setupSidebarScrolling(sidebar) {
        if (!sidebar) return;
        const initKey = `${SCRIPT_NAMESPACE}_sidebar_scroll_setup`;
        if (sidebar[initKey]) return;
        sidebar[initKey] = true;

        const header = sidebar.querySelector('.img-upload-sidebar-header');
        const tabs = sidebar.querySelector('.img-upload-sidebar-tabs');
        const scroller = sidebar.querySelector('.img-upload-sidebar-content');
        if (!scroller) return;

        const updateScrollerHeight = () => {
            const headerHeight = header ? header.offsetHeight : 0;
            const tabsHeight = tabs ? tabs.offsetHeight : 0;
            const available = window.innerHeight - headerHeight - tabsHeight;
            scroller.style.height = `${Math.max(0, available)}px`;
            scroller.style.overflowY = 'auto';
            scroller.style.minHeight = '0';
            scroller.style.webkitOverflowScrolling = 'touch';
        };

        updateScrollerHeight();
        window.addEventListener('resize', updateScrollerHeight);

        if (!hasEventListener(scroller, 'wheel')) {
            scroller.addEventListener('wheel', (e) => {
                if (scroller.scrollHeight <= scroller.clientHeight + 1) return;
                scroller.scrollTop += e.deltaY;
                e.preventDefault();
            }, { passive: false });
            markEventListener(scroller, 'wheel');
        }
    }

    // 默认配置信息
    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到剪贴板
        MAX_FILE_SIZE: 5 * 1024 * 1024 // 最大文件大小(5MB)
    };

    // 获取用户配置并确保所有必需的字段都存在
    const userConfig = GM_getValue('userConfig', {});

    // 如果存在加密的 AUTH_CODE,先解密
    if (userConfig.AUTH_CODE) {
        userConfig.AUTH_CODE = deobfuscate(userConfig.AUTH_CODE);
    }

    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 (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(`
        :root {
            --img-ui-primary: #2563eb;
            --img-ui-bg: rgba(255, 255, 255, 0.85);
            --img-ui-text: #1e293b;
            --img-ui-border: rgba(226, 232, 240, 0.8);
            --img-ui-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
        }

        .img-upload-notification {
            position: fixed;
            top: 24px;
            right: 24px;
            padding: 12px 20px;
            border-radius: 12px;
            z-index: 10001;
            max-width: 320px;
            font-size: 14px;
            font-weight: 500;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            box-shadow: var(--img-ui-shadow);
            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            opacity: 0;
            transform: translateY(-20px) scale(0.95);
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .img-upload-notification.show {
            opacity: 1;
            transform: translateY(0) scale(1);
        }
        .img-upload-success {
            background-color: rgba(34, 197, 94, 0.9);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        .img-upload-error {
            background-color: rgba(239, 68, 68, 0.9);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        .img-upload-info {
            background-color: rgba(37, 99, 235, 0.9);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        .img-upload-close {
            cursor: pointer;
            font-size: 16px;
            opacity: 0.7;
            transition: opacity 0.2s;
            line-height: 1;
        }
        .img-upload-close:hover {
            opacity: 1;
        }

        .img-upload-sidebar {
            position: fixed;
            top: 0;
            right: -420px;
            width: 400px;
            height: 100vh;
            height: 100dvh;
            background: var(--img-ui-bg);
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
            border-left: 1px solid var(--img-ui-border);
            box-shadow: -10px 0 30px rgba(0, 0, 0, 0.05);
            z-index: 10000;
            transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
            display: flex;
            flex-direction: column;
            color: var(--img-ui-text);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            pointer-events: auto;
            overflow: hidden;
        }
        .img-upload-sidebar.show {
            right: 0;
        }
        .img-upload-sidebar-header {
            padding: 24px;
            border-bottom: 1px solid var(--img-ui-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .img-upload-sidebar-header h2 {
            margin: 0;
            font-size: 20px;
            font-weight: 700;
            color: #111827;
        }
        .img-upload-sidebar-content {
            flex: 1;
            padding: 24px;
            overflow-y: auto;
            min-height: 0;
            overscroll-behavior: contain;
            -webkit-overflow-scrolling: touch;
            touch-action: pan-y;
        }
        .img-upload-form-group {
            margin-bottom: 24px;
        }
        .img-upload-form-group label {
            display: block;
            margin-bottom: 8px;
            font-size: 14px;
            font-weight: 600;
            color: #374151;
        }
        .img-upload-help-text {
            margin-top: 6px;
            color: #6b7280;
            font-size: 12px;
            line-height: 1.4;
        }
        .img-upload-form-group input:not([type="checkbox"]):not([type="radio"]),
        .img-upload-form-group textarea,
        .img-upload-form-group select {
            width: 100%;
            padding: 10px 14px;
            border: 1px solid #e2e8f0;
            border-radius: 10px;
            font-size: 14px;
            background: rgba(255, 255, 255, 0.8);
            color: #1e293b;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            box-sizing: border-box;
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
        }
        .img-upload-form-group input:focus,
        .img-upload-form-group select:focus,
        .img-upload-form-group textarea:focus {
            outline: none;
            border-color: var(--img-ui-primary);
            background: white;
            box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05);
        }
        .img-upload-buttons {
            padding: 24px;
            border-top: 1px solid var(--img-ui-border);
            display: flex;
            gap: 12px;
            background: rgba(255, 255, 255, 0.5);
            position: sticky;
            bottom: 0;
            z-index: 1;
        }
        .img-upload-button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 10px;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        .img-upload-button-primary {
            background: var(--img-ui-primary);
            color: white;
        }
        .img-upload-button-primary:hover {
            background: #1d4ed8;
            transform: translateY(-1px);
        }
        .img-upload-button-secondary {
            background: #f3f4f6;
            color: #374151;
        }
        .img-upload-button-secondary:hover {
            background: #e5e7eb;
        }
        .img-upload-input-group {
            display: flex;
            align-items: center;
        }
        .img-upload-input-group input {
            border-top-right-radius: 0 !important;
            border-bottom-right-radius: 0 !important;
        }
        .img-upload-input-group-text {
            padding: 10px 14px;
            background: #f8fafc;
            border: 1px solid #e2e8f0;
            border-left: none;
            border-radius: 0 10px 10px 0;
            color: #64748b;
            font-size: 14px;
        }
        .img-upload-checkbox-label {
            display: flex !important;
            align-items: center;
            font-weight: 500 !important;
            cursor: pointer;
        }
        .img-upload-checkbox-label input {
            width: 18px;
            height: 18px;
            margin-right: 10px;
            cursor: pointer;
        }

        .img-upload-dropzone {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(37, 99, 235, 0.1);
            backdrop-filter: blur(4px);
            z-index: 9999;
            box-sizing: border-box;
        }
        .img-upload-dropzone.active {
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .img-upload-dropzone-message {
            background: white;
            padding: 32px 48px;
            border-radius: 24px;
            font-size: 20px;
            font-weight: 700;
            color: var(--img-ui-primary);
            box-shadow: 0 20px 50px rgba(0, 0, 0, 0.1);
            border: 2px dashed var(--img-ui-primary);
        }
        .img-upload-sidebar-tabs {
            display: flex;
            padding: 0 24px;
            border-bottom: 1px solid var(--img-ui-border);
            gap: 20px;
        }
        .img-upload-tab {
            padding: 12px 0;
            font-size: 14px;
            font-weight: 600;
            color: #6b7280;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            transition: all 0.2s;
        }
        .img-upload-tab.active {
            color: var(--img-ui-primary);
            border-bottom-color: var(--img-ui-primary);
        }
        .img-upload-tab-panel {
            display: none;
            flex-direction: column;
        }
        .img-upload-tab-panel.active {
            display: flex;
        }

        .img-upload-gallery-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
            gap: 12px;
            padding: 4px;
        }
        .img-upload-gallery-item {
            position: relative;
            aspect-ratio: 1;
            border-radius: 8px;
            overflow: hidden;
            background: #f1f5f9;
            border: 1px solid var(--img-ui-border);
            cursor: pointer;
            transition: transform 0.2s;
        }
        .img-upload-gallery-item:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        }
        .img-upload-gallery-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        .img-upload-gallery-actions {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            background: rgba(0, 0, 0, 0.6);
            backdrop-filter: blur(4px);
            padding: 4px;
            display: flex;
            justify-content: center;
            gap: 4px;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .img-upload-gallery-item:hover .img-upload-gallery-actions {
            opacity: 1;
        }
        .img-upload-gallery-btn {
            background: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 10px;
            cursor: pointer;
            transition: background 0.2s;
        }
        .img-upload-gallery-btn:hover {
            background: rgba(255, 255, 255, 0.4);
        }
        .img-upload-load-more {
            width: auto;
            align-self: center;
            padding: 8px 16px;
            margin: 16px 0;
            background: transparent;
            border: 1px solid var(--img-ui-border);
            border-radius: 20px;
            color: #64748b;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s;
        }
        .img-upload-gallery-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 4px;
            margin-bottom: 10px;
        }
        .img-upload-refresh-btn {
            background: none;
            border: none;
            color: var(--img-ui-primary);
            font-size: 13px;
            cursor: pointer;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 4px;
        }
        .img-upload-refresh-btn:hover {
            opacity: 0.8;
        }
        .img-upload-gallery-btn-danger:hover {
            background: rgba(239, 68, 68, 0.6) !important;
        }
    `);

    // 添加通知样式
    // 创建或获取配置侧边栏
    function getOrCreateSidebar() {
        let sidebar = document.getElementById('img-upload-sidebar');
        if (!sidebar) {
            sidebar = document.createElement('div');
            sidebar.id = 'img-upload-sidebar';
            sidebar.className = 'img-upload-sidebar';
            document.body.appendChild(sidebar);
        }
        return sidebar;
    }

    // 创建配置界面 (侧边栏模式)
    function createConfigModal() {
        const sidebar = getOrCreateSidebar();

        const content = `
            <div class="img-upload-sidebar-header">
                <div>
                    <h2>图床助手</h2>
                    <a href="https://cfbed.sanyue.de/api/" target="_blank" style="font-size: 12px; color: var(--img-ui-primary); text-decoration: none; font-weight: 500;">查看 API 文档</a>
                </div>
                <div class="img-upload-close" id="img-upload-sidebar-close">✕</div>
            </div>
            <div class="img-upload-sidebar-tabs">
                <div class="img-upload-tab active" data-tab="gallery">图库</div>
                <div class="img-upload-tab" data-tab="config">设置</div>
            </div>
            <div class="img-upload-sidebar-content">
                <!-- 图库面板 -->
                <div class="img-upload-tab-panel active" id="img-upload-panel-gallery">
                    <div class="img-upload-gallery-header">
                        <span style="font-size: 12px; color: #94a3b8;">${CONFIG.UPLOAD_PARAMS.uploadFolder || 'apiupload'} 目录</span>
                        <button id="img-upload-refresh-gallery" class="img-upload-refresh-btn">
                            <span>↻</span> 刷新列表
                        </button>
                    </div>
                    <div id="img-upload-gallery-container" class="img-upload-gallery-grid">
                        <div style="grid-column: 1/-1; text-align: center; color: #6b7280; padding: 40px 0;">
                            正在加载图片...
                        </div>
                    </div>
                    <div style="display: flex; justify-content: center; width: 100%;">
                        <button id="img-upload-load-more-btn" class="img-upload-load-more" style="display: none;">加载更多</button>
                    </div>
                </div>

                <!-- 设置面板 -->
                <div class="img-upload-tab-panel" id="img-upload-panel-config">
                    <form id="img-upload-config-form">
                        <div class="img-upload-form-group">
                            <label>认证码</label>
                            <input type="password" 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="telegram" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'telegram' ? 'selected' : ''}>Telegram</option>
                                <option value="cfr2" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'cfr2' ? 'selected' : ''}>CloudFlare R2</option>
                                <option value="s3" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 's3' ? 'selected' : ''}>S3 Compatible</option>
                                <option value="discord" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'discord' ? 'selected' : ''}>Discord</option>
                                <option value="huggingface" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'huggingface' ? 'selected' : ''}>HuggingFace</option>
                            </select>
                        </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>
                        <div class="img-upload-form-group">
                            <label>上传目录</label>
                            <input type="text" name="uploadFolder" value="${CONFIG.UPLOAD_PARAMS.uploadFolder}">
                            <div class="img-upload-help-text">相对路径,例如:apiupload</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>
                            <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>
                    </form>
                    <div class="img-upload-buttons">
                        <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-reset">重置</button>
                        <button type="submit" form="img-upload-config-form" class="img-upload-button img-upload-button-primary">保存设置</button>
                    </div>
                </div>
            </div>
        `;

        sidebar.innerHTML = content;
        setupSidebarScrolling(sidebar);

        // 使用 timeout 确保 DOM 渲染后再添加 show 类触发动画
        setTimeout(() => {
            sidebar.classList.add('show');
            // 默认加载图库
            fetchGallery(false);
        }, 10);

        // 事件处理
        const form = sidebar.querySelector('#img-upload-config-form');
        const closeBtn = sidebar.querySelector('#img-upload-sidebar-close');
        const resetBtn = sidebar.querySelector('#img-upload-reset');
        const tabs = sidebar.querySelectorAll('.img-upload-tab');
        const refreshBtn = sidebar.querySelector('#img-upload-refresh-gallery');
        const loadMoreBtn = sidebar.querySelector('#img-upload-load-more-btn');
        const galleryContainer = sidebar.querySelector('#img-upload-gallery-container');

        let galleryStart = 0;
        const galleryCount = 30;

        // 切换标签
        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                const targetPanel = tab.getAttribute('data-tab');
                tabs.forEach(t => t.classList.remove('active'));
                tab.classList.add('active');

                sidebar.querySelectorAll('.img-upload-tab-panel').forEach(p => p.classList.remove('active'));
                sidebar.querySelector(`#img-upload-panel-${targetPanel}`).classList.add('active');

                // 首次切换到图库时自动加载
                if (targetPanel === 'gallery' && galleryStart === 0) {
                    fetchGallery(false);
                }
            });
        });

        // 获取图片列表
        async function fetchGallery(isLoadMore = false) {
            if (!requireRuntimeConfig('获取图片列表')) {
                const msg = `未完成图床配置,请先在油猴菜单中打开「${MENU_LABEL_CONFIG}」填写认证码与服务器地址。`;
                if (!isLoadMore) {
                    galleryContainer.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #f44336; padding: 40px 0;">${escapeHtml(msg)}</div>`;
                }
                loadMoreBtn.style.display = 'none';
                loadMoreBtn.disabled = false;
                return;
            }

            if (!isLoadMore) {
                galleryStart = 0;
                galleryContainer.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #6b7280; padding: 40px 0;">正在加载...</div>';
                loadMoreBtn.style.display = 'none';
            } else {
                loadMoreBtn.textContent = '加载中...';
                loadMoreBtn.disabled = true;
            }

            const queryParams = new URLSearchParams({
                start: galleryStart,
                count: galleryCount,
                dir: CONFIG.UPLOAD_PARAMS.uploadFolder || 'apiupload'
            }).toString();

            GM_xmlhttpRequest({
                method: 'GET',
                url: `${CONFIG.SERVER_URL}/api/manage/list?${queryParams}`,
                headers: {
                    'Authorization': `Bearer ${CONFIG.AUTH_CODE}`
                },
                onload: function (response) {
                    loadMoreBtn.disabled = false;

                    if (response.status === 200) {
                        try {
                            const result = JSON.parse(response.responseText);
                            if (result.files && result.files.length > 0) {
                                if (!isLoadMore) galleryContainer.innerHTML = '';

                                result.files.forEach(file => {
                                    const baseUrl = CONFIG.SERVER_URL.replace(/\/+$/, '');
                                    const imageUrl = `${baseUrl}/file/${file.name.replace(/^\/+/, '')}`;

                                    const item = document.createElement('div');
                                    item.className = 'img-upload-gallery-item';
                                    item.innerHTML = `
                                        <img src="${imageUrl}" loading="lazy">
                                        <div class="img-upload-gallery-actions">
                                            <button class="img-upload-gallery-btn" data-action="copy">复制</button>
                                            <button class="img-upload-gallery-btn img-upload-gallery-btn-danger" data-action="delete">删除</button>
                                        </div>
                                    `;

                                    // 操作事件
                                    item.querySelector('[data-action="copy"]').addEventListener('click', (e) => {
                                        e.stopPropagation();
                                        copyToClipboard(imageUrl);
                                    });

                                    item.querySelector('[data-action="delete"]').addEventListener('click', (e) => {
                                        e.stopPropagation();
                                        if (confirm('确定要删除这张图片吗?此操作不可恢复。')) {
                                            if (!requireRuntimeConfig('删除图片')) return;
                                            const deleteUrl = `${CONFIG.SERVER_URL}/api/manage/delete/${encodeURIComponent(file.name.replace(/^\/+/, ''))}`;
                                            GM_xmlhttpRequest({
                                                method: 'GET',
                                                url: deleteUrl,
                                                headers: {
                                                    'Authorization': `Bearer ${CONFIG.AUTH_CODE}`
                                                },
                                                onload: function (response) {
                                                    if (response.status === 200) {
                                                        showNotification('图片已删除', 'success');
                                                        item.remove();
                                                    } else {
                                                        showNotification(buildHttpErrorMessage('删除图片', response), 'error');
                                                    }
                                                },
                                                onerror: function () {
                                                    showNotification(buildNetworkErrorMessage('删除图片'), 'error');
                                                }
                                            });
                                        }
                                    });

                                    galleryContainer.appendChild(item);
                                });

                                galleryStart += result.files.length;
                                if (result.files.length >= galleryCount) {
                                    loadMoreBtn.style.display = 'block';
                                    loadMoreBtn.textContent = '加载更多';
                                } else {
                                    loadMoreBtn.style.display = 'none';
                                }
                            } else {
                                if (!isLoadMore) {
                                    galleryContainer.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #6b7280; padding: 40px 0;">暂无图片</div>';
                                }
                                loadMoreBtn.style.display = 'none';
                            }
                        } catch (e) {
                            showNotification(`解析图片列表响应失败:${e.message}。请确认后端返回的是 JSON,且接口路径正确。`, 'error');
                        }
                    } else {
                        const msg = buildHttpErrorMessage('获取图片列表', response);
                        showNotification(msg, 'error');
                        if (!isLoadMore) {
                            galleryContainer.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #f44336; padding: 40px 0;">${escapeHtml(msg)}</div>`;
                        }
                        loadMoreBtn.style.display = 'none';
                    }
                },
                onerror: function () {
                    const msg = buildNetworkErrorMessage('获取图片列表');
                    showNotification(msg, 'error');
                    if (!isLoadMore) {
                        galleryContainer.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: #f44336; padding: 40px 0;">${escapeHtml(msg)}</div>`;
                    }
                    loadMoreBtn.disabled = false;
                }
            });
        }

        // 刷新列表
        refreshBtn.addEventListener('click', () => fetchGallery(false));

        // 加载更多
        loadMoreBtn.addEventListener('click', () => fetchGallery(true));

        function closeSidebar() {
            sidebar.classList.remove('show');
        }

        closeBtn.addEventListener('click', closeSidebar);

        resetBtn.addEventListener('click', () => {
            if (confirm('确定要重置所有配置到默认值吗?')) {
                CONFIG = { ...DEFAULT_CONFIG };
                GM_setValue('userConfig', {});
                showNotification('配置已重置为默认值!', 'success');
                closeSidebar();
            }
        });

        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').replace(/\/+$/, ''),
                    UPLOAD_PARAMS: {
                        ...DEFAULT_CONFIG.UPLOAD_PARAMS,
                        uploadChannel: formData.get('uploadChannel'),
                        uploadNameType: formData.get('uploadNameType'),
                        uploadFolder: formData.get('uploadFolder')
                    },
                    MARKDOWN_TEMPLATE: formData.get('MARKDOWN_TEMPLATE'),
                    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);

                // 保存时加密 AUTH_CODE
                const configToSave = JSON.parse(JSON.stringify(CONFIG));
                configToSave.AUTH_CODE = obfuscate(configToSave.AUTH_CODE);
                GM_setValue('userConfig', configToSave);

                showNotification('配置已更新!', 'success');
                closeSidebar();
            } catch (error) {
                showNotification(`保存配置失败:${error.message}。请检查服务器地址格式(需以 http:// 或 https:// 开头)以及各项输入是否完整。`, 'error');
            }
        });
    }

    // 添加日志函数
    function log(message, type = 'info') {
        const prefix = '[Image Uploader]';
        switch (type) {
            case 'error':
                console.error(`${prefix} ❌ ${message}`);
                break;
            case 'warn':
                console.warn(`${prefix} ⚠️ ${message}`);
                break;
            case 'success':
                console.log(`${prefix} ✅ ${message}`);
                break;
            default:
                console.log(`${prefix} ℹ️ ${message}`);
        }
    }

    function escapeHtml(text) {
        return String(text).replace(/[&<>"']/g, (ch) => {
            switch (ch) {
                case '&': return '&amp;';
                case '<': return '&lt;';
                case '>': return '&gt;';
                case '"': return '&quot;';
                case "'": return '&#39;';
                default: return ch;
            }
        });
    }

    function isPlaceholderAuthCode(value) {
        const v = String(value || '').trim();
        return !v || v === 'AUTH_CODE';
    }

    function isPlaceholderServerUrl(value) {
        const v = String(value || '').trim();
        return !v || v === 'https://SERVER_URL' || v.includes('SERVER_URL');
    }

    function getRuntimeConfigProblems() {
        const problems = [];
        if (isPlaceholderAuthCode(CONFIG.AUTH_CODE)) {
            problems.push('未配置认证码(AUTH_CODE/API Key)');
        }
        if (isPlaceholderServerUrl(CONFIG.SERVER_URL)) {
            problems.push('未配置服务器地址(SERVER_URL)');
        } else if (!/^https?:\/\//i.test(String(CONFIG.SERVER_URL || '').trim())) {
            problems.push('服务器地址必须以 http:// 或 https:// 开头');
        }
        return problems;
    }

    function requireRuntimeConfig(actionName, { notify = true } = {}) {
        const problems = getRuntimeConfigProblems();
        if (problems.length === 0) return true;

        const message = `${actionName}前请先完成配置:${problems.join(';')}。请在油猴菜单中打开「${MENU_LABEL_CONFIG}」进行设置。`;
        if (notify) showNotification(message, 'error');
        return false;
    }

    function safeParseJson(text) {
        if (typeof text !== 'string' || !text.trim()) return null;
        try {
            return JSON.parse(text);
        } catch {
            return null;
        }
    }

    function extractMessageFromBody(responseText) {
        const json = safeParseJson(responseText);
        if (!json) return '';
        if (typeof json === 'string') return json;
        if (Array.isArray(json)) return '';
        return String(json.message || json.msg || json.error || json.detail || '').trim();
    }

    function truncateForToast(text, maxLen = 120) {
        const s = String(text || '').replace(/\s+/g, ' ').trim();
        if (s.length <= maxLen) return s;
        return s.slice(0, maxLen) + '…';
    }

    function buildHttpErrorMessage(actionName, response) {
        const status = Number(response && response.status);
        const statusText = String((response && response.statusText) || '').trim();
        const bodyMessage = extractMessageFromBody(response && response.responseText);
        const detail = truncateForToast(bodyMessage || statusText);

        let base = '';
        switch (status) {
            case 400:
                base = `${actionName}失败(400):请求参数错误。请检查通道/目录/命名方式等参数是否为后端支持的值。`;
                break;
            case 401:
                base = `${actionName}失败(401):认证失败,认证码/API Key 错误或已过期。请打开「${MENU_LABEL_CONFIG}」更新 AUTH_CODE,并确认 SERVER_URL 指向你的图床服务。`;
                break;
            case 403:
                base = `${actionName}失败(403):无权限访问该接口。请确认该认证码具备对应权限(如“列表/删除/上传”)。`;
                break;
            case 404:
                base = `${actionName}失败(404):接口不存在。请检查 SERVER_URL 是否正确,以及后端版本是否支持该接口。`;
                break;
            case 413:
                base = `${actionName}失败(413):文件过大,超过服务器限制。可在「${MENU_LABEL_CONFIG}」调整“最大文件大小”,并确认后端上传限制。`;
                break;
            case 415:
                base = `${actionName}失败(415):不支持的文件类型。请上传图片(image/*)。`;
                break;
            case 429:
                base = `${actionName}失败(429):请求过于频繁,请稍后再试。`;
                break;
            default:
                if (status >= 500 && status <= 599) {
                    base = `${actionName}失败(${status}):服务器异常。请查看后端日志或稍后重试。`;
                } else if (status > 0) {
                    base = `${actionName}失败(${status})。`;
                } else {
                    base = `${actionName}失败:请求未完成(可能是网络/证书/拦截问题)。请检查 SERVER_URL 是否可访问。`;
                }
        }

        if (detail && !base.includes(detail)) {
            return `${base}(${detail})`;
        }
        return base;
    }

    function buildNetworkErrorMessage(actionName) {
        return `${actionName}失败:网络错误,无法连接到图床服务器。请检查 SERVER_URL 是否可访问/HTTPS 证书是否正常,并在「${MENU_LABEL_CONFIG}」确认配置。`;
    }

    // 显示通知的函数
    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 && err.message ? err.message : '浏览器禁止写入剪贴板'}。请手动复制链接。`, 'error');
        }
        document.body.removeChild(textarea);
    }

    // 检查文件大小
    function checkFileSize(file) {
        if (file.size > CONFIG.MAX_FILE_SIZE) {
            const sizeMb = (file.size / 1024 / 1024).toFixed(2);
            const limitMb = Math.round(CONFIG.MAX_FILE_SIZE / 1024 / 1024);
            showNotification(`文件过大:${sizeMb}MB,超过当前限制 ${limitMb}MB。可在「${MENU_LABEL_CONFIG}」调整“最大文件大小”,并确认后端限制。`, 'error');
            return false;
        }
        return true;
    }

    // 修改事件监听器添加方式
    function addPasteListener() {
        if (hasEventListener(document, 'paste')) {
            return;
        }

        document.addEventListener('paste', async function (event) {
            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;
                    }

                    if (!requireRuntimeConfig('上传图片')) {
                        return;
                    }
                    showNotification('正在上传图片,请稍候...', 'info');
                    await uploadImage(blob, activeElement);
                    break;
                }
            }

            if (!hasImage) {
                return;
            }
        });

        markEventListener(document, 'paste');
    }

    // 上传图片
    async function uploadImage(blob, targetElement) {
        if (!requireRuntimeConfig('上传图片')) return;

        const formData = new FormData();
        const filename = `pasted-image-${Date.now()}.png`;
        formData.append('file', blob, filename);

        log(`开始上传图片: ${filename} (${(blob.size / 1024).toFixed(2)}KB)`);
        log(`上传参数: ${JSON.stringify(CONFIG.UPLOAD_PARAMS, null, 2)}`);

        const queryParams = new URLSearchParams({
            ...CONFIG.UPLOAD_PARAMS
        }).toString();

        try {
            log(`正在发送请求到: ${CONFIG.SERVER_URL}/upload`);
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${CONFIG.SERVER_URL}/upload?${queryParams}`,
                headers: {
                    'Authorization': `Bearer ${CONFIG.AUTH_CODE}`
                },
                data: formData,
                onload: function (response) {
                    if (response.status === 200) {
                        try {
                            const result = JSON.parse(response.responseText);
                            log(`服务器响应: ${JSON.stringify(result, null, 2)}`);

                            if (result && result.length > 0) {
                                let imageUrl = result[0].src;
                                if (!imageUrl.startsWith('http')) {
                                    // 处理相对路径,确保 URL 拼接正确
                                    const baseUrl = CONFIG.SERVER_URL.replace(/\/+$/, '');
                                    const path = imageUrl.replace(/^\/+/, '');
                                    imageUrl = `${baseUrl}/file/${path}`;
                                }
                                log(`上传成功,图片URL: ${imageUrl}`, 'success');
                                insertMarkdownImage(imageUrl, targetElement, filename);
                                showNotification('图片上传成功!', 'success');

                                if (CONFIG.AUTO_COPY_URL) {
                                    log('自动复制URL到剪贴板');
                                    copyToClipboard(imageUrl);
                                }
                            } else {
                                const errorMsg = '上传成功但未获取到图片链接,请检查服务器响应';
                                log(errorMsg, 'error');
                                showNotification(errorMsg, 'error');
                            }
                        } catch (e) {
                            const errorMsg = `解析上传响应失败:${e.message}。后端可能返回了非 JSON 内容,请检查 SERVER_URL/反代配置。`;
                            log(errorMsg, 'error');
                            log(`原始响应: ${response.responseText}`, 'error');
                            showNotification(errorMsg, 'error');
                        }
                    } else {
                        const errorMsg = buildHttpErrorMessage('上传图片', response);
                        log(`上传失败: 状态码 ${response.status}`, 'error');
                        if (response && typeof response.responseText === 'string' && response.responseText.trim()) {
                            log(`原始响应: ${response.responseText}`, 'error');
                        }
                        showNotification(errorMsg, 'error');
                    }
                },
                onerror: function (error) {
                    const errorMsg = buildNetworkErrorMessage('上传图片');
                    log(`${errorMsg}: ${error}`, 'error');
                    showNotification(errorMsg, 'error');
                }
            });
        } catch (error) {
            const errorMsg = `上传过程发生错误:${error.message}`;
            log(errorMsg, 'error');
            showNotification(errorMsg, '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 registerMenuCommands() {
        // 如果已经存在菜单,先注销
        if (menuCommandId) {
            try {
                GM_unregisterMenuCommand(menuCommandId);
            } catch (e) {
                console.log('Unregister menu failed:', e);
            }
        }

        // 注册新菜单
        try {
            menuCommandId = GM_registerMenuCommand(MENU_LABEL_CONFIG, createConfigModal);
        } catch (e) {
            console.log('Register menu failed:', e);
            // 如果注册失败,尝试延迟重试
            setTimeout(registerMenuCommands, 1000);
        }
    }

    // 创建拖拽区域
    function createDropZone() {
        const dropZone = document.createElement('div');
        dropZone.className = 'img-upload-dropzone';

        const message = document.createElement('div');
        message.className = 'img-upload-dropzone-message';
        message.textContent = '释放鼠标上传图片';

        dropZone.appendChild(message);
        document.body.appendChild(dropZone);
        return dropZone;
    }

    // 修改拖拽事件监听器添加方式
    function handleDragAndDrop() {
        if (hasEventListener(document, 'drag')) {
            return;
        }

        const dropZone = createDropZone();
        let activeElement = null;

        // 处理拖拽文件
        async function handleFiles(files, targetElement) {
            for (const file of files) {
                if (file.type.startsWith('image/')) {
                    if (!checkFileSize(file)) {
                        continue;
                    }
                    if (!requireRuntimeConfig('上传图片')) {
                        continue;
                    }
                    showNotification('正在上传图片,请稍候...', 'info');
                    await uploadImage(file, targetElement);
                } else {
                    showNotification('不支持的文件类型:仅支持图片(image/*)', 'error');
                }
            }
        }

        // 监听拖拽事件
        document.addEventListener('dragenter', (e) => {
            e.preventDefault();
            activeElement = document.activeElement;
            if (activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
                dropZone.classList.add('active');
            }
        });

        document.addEventListener('dragover', (e) => {
            e.preventDefault();
        });

        document.addEventListener('dragleave', (e) => {
            e.preventDefault();
            const rect = document.documentElement.getBoundingClientRect();
            if (e.clientX <= rect.left || e.clientX >= rect.right ||
                e.clientY <= rect.top || e.clientY >= rect.bottom) {
                dropZone.classList.remove('active');
            }
        });

        document.addEventListener('drop', async (e) => {
            e.preventDefault();
            dropZone.classList.remove('active');

            if (activeElement && ['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
                const files = Array.from(e.dataTransfer.files);
                await handleFiles(files, activeElement);
            }
        });

        markEventListener(document, 'drag');
    }

    // 修改初始化函数
    function init() {
        // 检查是否已经初始化
        if (document[SCRIPT_NAMESPACE]) {
            log('脚本已经初始化,跳过');
            return;
        }
        document[SCRIPT_NAMESPACE] = true;

        log(`初始化图片上传脚本 v${GM_info.script.version}`);
        log(`当前配置: ${JSON.stringify(CONFIG, null, 2)}`);

        // 清理可能存在的旧实例
        cleanup();

        // 初始化功能
        addPasteListener();
        handleDragAndDrop();

        // 确保菜单注册成功
        const registerMenu = () => {
            if (!menuCommandId) {
                log('注册配置菜单');
                registerMenuCommands();
            }
        };

        // 立即注册一次
        registerMenu();

        // 在不同的时机尝试注册菜单
        window.addEventListener('load', registerMenu);
        document.addEventListener('readystatechange', registerMenu);

        // 定期检查菜单是否存在
        setInterval(registerMenu, 5000);

        log('初始化完成');
    }

    // 添加清理函数
    function cleanup() {
        // 移除可能存在的旧的拖拽区域
        const oldDropZones = document.querySelectorAll('.img-upload-dropzone');
        oldDropZones.forEach(zone => zone.remove());

        // 移除可能存在的旧的通知
        const oldNotifications = document.querySelectorAll('.img-upload-notification');
        oldNotifications.forEach(notification => notification.remove());

        // 注销可能存在的旧菜单
        if (menuCommandId) {
            try {
                GM_unregisterMenuCommand(menuCommandId);
                menuCommandId = null;
            } catch (e) {
                console.log('Cleanup menu failed:', e);
            }
        }
    }

    // 在页面 DOM 加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();