Greasy Fork

Greasy Fork is available in English.

NodeSeek 编辑器图床增强脚本

在 NodeSeek 支持点击、拖拽和粘贴上传图片银星公益图床、16 图床兼容自建(兰空图床),并插入 Markdown 格式到编辑器

当前为 2025-06-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NodeSeek 编辑器图床增强脚本
// @namespace    http://tampermonkey.net/
// @version      2.3.1
// @description  在 NodeSeek 支持点击、拖拽和粘贴上传图片银星公益图床、16 图床兼容自建(兰空图床),并插入 Markdown 格式到编辑器
// @author       ZhangBreeze
// @match        https://www.nodeseek.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = 'image/*';
    fileInput.multiple = true;
    fileInput.style.display = 'none';
    document.body.appendChild(fileInput);

    const editorWrapper = document.querySelector('#cm-editor-wrapper');
    const codeMirror = document.querySelector('.CodeMirror.cm-s-default.cm-s-nsk.CodeMirror-wrap.CodeMirror-overlayscroll');
    const cmInstance = document.querySelector('.CodeMirror')?.CodeMirror;

    function addUploadHint(container) {
        if (!container) return;
        const existingHint = container.querySelector('.upload-hint-text');
        if (existingHint) return;
        const hint = document.createElement('div');
        hint.className = 'upload-hint-text';
        hint.textContent = '支持拖拽或粘贴上传图片';
        hint.style.position = 'absolute';
        hint.style.bottom = '5px';
        hint.style.right = '5px';
        hint.style.color = '#888';
        hint.style.fontSize = '12px';
        hint.style.zIndex = '10';
        hint.style.pointerEvents = 'none';
        container.style.position = 'relative';
        container.appendChild(hint);
    }

    if (editorWrapper) {
        addUploadHint(editorWrapper);
    } else if (codeMirror) {
        addUploadHint(codeMirror);
    }

    function showUploadHint(container, fileCount) {
        if (!container) return;
        const existingHints = document.querySelectorAll('[id^="upload-hint-"]');
        existingHints.forEach(hint => hint.remove());
        const uploadHint = document.createElement('div');
        uploadHint.textContent = `正在上传 ${fileCount} 张图片,请稍等`;
        uploadHint.style.position = 'absolute';
        uploadHint.style.top = '50%';
        uploadHint.style.left = '50%';
        uploadHint.style.transform = 'translate(-50%, -50%)';
        uploadHint.style.color = '#666';
        uploadHint.style.fontSize = '14px';
        uploadHint.style.background = 'rgba(0, 0, 0, 0.1)';
        uploadHint.style.padding = '5px 10px';
        uploadHint.style.borderRadius = '3px';
        uploadHint.style.zIndex = '20';
        uploadHint.style.maxWidth = '80%';
        uploadHint.style.whiteSpace = 'nowrap';
        uploadHint.style.overflow = 'hidden';
        uploadHint.style.textOverflow = 'ellipsis';
        uploadHint.id = 'upload-hint-' + (container === editorWrapper ? 'wrapper' : 'codemirror');
        container.appendChild(uploadHint);
    }

    function removeUploadHint(container) {
        const uploadHint = document.getElementById('upload-hint-' + (container === editorWrapper ? 'wrapper' : 'codemirror'));
        if (uploadHint) uploadHint.remove();
    }

    function addSettingsIcon() {
        const uploadIcon = document.querySelector('span.toolbar-item.i-icon.i-icon-pic');
        if (!uploadIcon) return;
        const existingSettingsIcon = uploadIcon.parentNode.querySelector('.settings-icon');
        if (existingSettingsIcon) return;
        const settingsIcon = document.createElement('span');
        settingsIcon.className = 'toolbar-item i-icon settings-icon';
        settingsIcon.style.cursor = 'pointer';
        settingsIcon.style.marginLeft = '5px';
        settingsIcon.style.display = 'inline-block';
        settingsIcon.style.verticalAlign = 'middle';
        settingsIcon.style.width = '16px';
        settingsIcon.style.height = '16px';
        settingsIcon.title = '选择图床';
        settingsIcon.innerHTML = `
            <svg style="width: 100%; height: 100%; fill: currentColor;">
                <use data-v-0f04b1f4="" href="#setting-two"></use>
            </svg>
        `;
        uploadIcon.parentNode.insertBefore(settingsIcon, uploadIcon.nextSibling);
        settingsIcon.addEventListener('click', () => {
            showSettingsModal();
        });
    }

    function observeToolbar() {
        const targetNode = document.body;
        const config = { childList: true, subtree: true };
        const callback = (mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const uploadIcon = document.querySelector('span.toolbar-item.i-icon.i-icon-pic');
                    if (uploadIcon) {
                        addSettingsIcon();
                    }
                }
            }
        };
        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
        // Initial check in case the toolbar is already present
        addSettingsIcon();
    }

    observeToolbar();

    function showSettingsModal() {
        const existingModal = document.querySelector('#image-host-settings-modal');
        if (existingModal) existingModal.remove();

        const modal = document.createElement('div');
        modal.id = 'image-host-settings-modal';
        modal.style.position = 'fixed';
        modal.style.top = '50%';
        modal.style.left = '50%';
        modal.style.transform = 'translate(-50%, -50%)';
        modal.style.background = 'linear-gradient(135deg, #ffffff, #f0f4f8)';
        modal.style.padding = '25px';
        modal.style.borderRadius = '12px';
        modal.style.boxShadow = '0 4px 20px rgba(0,0,0,0.15)';
        modal.style.zIndex = '1000';
        modal.style.width = '350px';
        modal.style.fontFamily = "'Segoe UI', Arial, sans-serif";
        modal.style.color = '#333';

        // 获取当前保存的设置
        const currentHost = GM_getValue('imageHost', 'lankong');
        const currentSixteenToken = GM_getValue('sixteenToken', '');
        const currentLankongToken = GM_getValue('lankongCustomToken', '');
        const currentLankongApi = GM_getValue('lankongCustomApi', '');
        // 获取 Cloudflare ImgBed 设置
        const currentCloudflareImgbedApi = GM_getValue('cloudflareImgbedApi');
        const currentCloudflareImgbedAuthCode = GM_getValue('cloudflareImgbedAuthCode');
        // 获取 Cloudflare ImgBed 压缩设置,默认勾选 (true)
        const currentCloudflareImgbedCompress = GM_getValue('cloudflareImgbedCompress', true); // <-- 默认值改为 true


        modal.innerHTML = `
            <h3 style="margin: 0 0 15px 0; font-size: 20px; font-weight: 600; color: #2c3e50;">图床设置</h3>
            <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">选择图床:</label>
            <select id="image-host-select" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);">
                <option value="lankong" ${currentHost === 'lankong' ? 'selected' : ''}>银星图床</option>
                <option value="lankong-custom" ${currentHost === 'lankong-custom' ? 'selected' : ''}>LSKY 接口</option>
                <option value="sixteen" ${currentHost === 'sixteen' ? 'selected' : ''}>16 图床</option>
                <option value="uhsea" ${currentHost === 'uhsea' ? 'selected' : ''}>屋舍图床</option>
                <option value="cloudflare-imgbed" ${currentHost === 'cloudflare-imgbed' ? 'selected' : ''}>Cloudflare ImgBed</option>
            </select>

            <div id="lankong-token-section" style="display: ${currentHost === 'lankong-custom' ? 'block' : 'none'};">
                <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">兰空图床 API 端点:</label>
                <input type="text" id="lankong-api-input" value="${currentLankongApi}" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);" placeholder="https://example.com/api/v1/upload">
                <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">兰空图床 Token:</label>
                <input type="text" id="lankong-token-input" value="${currentLankongToken}" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);" placeholder="请输入 Token">
            </div>

            <div id="sixteen-token-section" style="display: ${currentHost === 'sixteen' ? 'block' : 'none'};">
                <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">16 图床 Auth-Token:</label>
                <input type="text" id="sixteen-token-input" value="${currentSixteenToken}" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);" placeholder="请输入 Auth-Token">
            </div>

            <!-- Cloudflare ImgBed 设置区域 -->
            <div id="cloudflare-imgbed-section" style="display: ${currentHost === 'cloudflare-imgbed' ? 'block' : 'none'};">
                <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">Cloudflare ImgBed 域名:</label>
                <input type="text" id="cloudflare-imgbed-api-input" value="${currentCloudflareImgbedApi}" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);" placeholder="https://img.yourdomain.link">
                <label style="display: block; margin-bottom: 8px; font-size: 14px; color: #34495e;">Cloudflare ImgBed Auth Code:</label>
                <input type="text" id="cloudflare-imgbed-auth-input" value="${currentCloudflareImgbedAuthCode}" style="width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #dcdcdc; border-radius: 6px; background: #fff; font-size: 14px; color: #333; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);" placeholder="请输入 Auth Code">

                <!-- 添加服务器端压缩选项 -->
                <div style="margin-bottom: 15px; display: flex; align-items: center;">
                    <input type="checkbox" id="cloudflare-imgbed-compress-checkbox" ${currentCloudflareImgbedCompress ? 'checked' : ''} style="margin-right: 8px;">
                    <label for="cloudflare-imgbed-compress-checkbox" style="font-size: 14px; color: #34495e; cursor: pointer;">开启服务器端压缩</label>
                </div>
                 <!-- 结束服务器端压缩选项 -->
            </div>
             <!-- 结束 Cloudflare ImgBed 设置区域 -->

            <div style="text-align: right;">
                <button id="save-settings-btn" style="background: linear-gradient(90deg, #4CAF50, #45a049); color: white; padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.3s;">保存</button>
                <button id="close-settings-btn" style="background: linear-gradient(90deg, #f44336, #e53935); color: white; padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; margin-left: 10px; transition: background 0.3s;">关闭</button>
            </div>
        `;

        const overlay = document.createElement('div');
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.background = 'rgba(0,0,0,0.4)';
        overlay.style.zIndex = '999';

        document.body.appendChild(overlay);
        document.body.appendChild(modal);

        const hostSelect = document.querySelector('#image-host-select');
        const lankongTokenSection = document.querySelector('#lankong-token-section');
        const sixteenTokenSection = document.querySelector('#sixteen-token-section');
        const cloudflareImgbedSection = document.querySelector('#cloudflare-imgbed-section'); // 获取 Cloudflare ImgBed 区域元素

        // 修改下拉菜单的事件监听器
        hostSelect.addEventListener('change', () => {
            lankongTokenSection.style.display = hostSelect.value === 'lankong-custom' ? 'block' : 'none';
            sixteenTokenSection.style.display = hostSelect.value === 'sixteen' ? 'block' : 'none';
            cloudflareImgbedSection.style.display = hostSelect.value === 'cloudflare-imgbed' ? 'block' : 'none'; // 控制新区域的显示/隐藏
        });

        document.querySelector('#save-settings-btn').addEventListener('click', () => {
            const selectedHost = hostSelect.value;
            GM_setValue('imageHost', selectedHost); // 保存当前选择的图床类型

            // 保存各图床的特定设置
            if (selectedHost === 'sixteen') {
                const sixteenTokenInput = document.querySelector('#sixteen-token-input').value;
                GM_setValue('sixteenToken', sixteenTokenInput);
            } else if (selectedHost === 'lankong-custom') {
                const lankongTokenInput = document.querySelector('#lankong-token-input').value;
                const lankongApiInput = document.querySelector('#lankong-api-input').value;
                GM_setValue('lankongCustomToken', lankongTokenInput);
                GM_setValue('lankongCustomApi', lankongApiInput);
            } else if (selectedHost === 'cloudflare-imgbed') { // 保存 Cloudflare ImgBed 设置
                 const cloudflareImgbedApiInput = document.querySelector('#cloudflare-imgbed-api-input').value;
                 const cloudflareImgbedAuthInput = document.querySelector('#cloudflare-imgbed-auth-input').value;
                 const cloudflareImgbedCompressCheckbox = document.querySelector('#cloudflare-imgbed-compress-checkbox').checked; // 获取压缩选项的状态

                 GM_setValue('cloudflareImgbedApi', cloudflareImgbedApiInput);
                 GM_setValue('cloudflareImgbedAuthCode', cloudflareImgbedAuthInput);
                 GM_setValue('cloudflareImgbedCompress', cloudflareImgbedCompressCheckbox); // 保存压缩选项的状态
            }
            // 注意:对于 lankong 和 uhsea,没有额外需要保存的设置

            modal.remove();
            overlay.remove();
        });

        document.querySelector('#close-settings-btn').addEventListener('click', () => {
            modal.remove();
            overlay.remove();
        });

        // Hover effects remain the same
        const saveBtn = document.querySelector('#save-settings-btn');
        const closeBtn = document.querySelector('#close-settings-btn');
        saveBtn.addEventListener('mouseover', () => {
            saveBtn.style.background = 'linear-gradient(90deg, #45a049, #4CAF50)';
        });
        saveBtn.addEventListener('mouseout', () => {
            saveBtn.style.background = 'linear-gradient(90deg, #4CAF50, #45a049)';
        });
        closeBtn.addEventListener('mouseover', () => {
            closeBtn.style.background = 'linear-gradient(90deg, #e53935, #f44336)';
        });
        closeBtn.addEventListener('mouseout', () => {
            closeBtn.style.background = 'linear-gradient(90deg, #f44336, #e53935)';
        });
    }

    let isUploading = false;

    document.addEventListener('click', function(e) {
        const target = e.target.closest('span.toolbar-item.i-icon.i-icon-pic');
        if (target && !isUploading) {
            e.preventDefault();
            e.stopPropagation();
            fileInput.click();
        }
    }, true);

    fileInput.addEventListener('change', function(e) {
        if (e.target.files && e.target.files.length > 0 && !isUploading) {
            isUploading = true;
            const files = Array.from(e.target.files);
            uploadMultipleFiles(files, editorWrapper || codeMirror).finally(() => {
                isUploading = false;
                fileInput.value = '';
            });
        }
    });

    // Drag and drop handlers remain the same
    if (editorWrapper) {
        editorWrapper.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (!isUploading) editorWrapper.style.border = '2px dashed #000';
        });
        editorWrapper.addEventListener('dragleave', (e) => {
            e.preventDefault();
            e.stopPropagation();
            editorWrapper.style.border = '';
        });
        editorWrapper.addEventListener('drop', (e) => {
            e.preventDefault();
            e.stopPropagation();
            editorWrapper.style.border = '';
            if (e.dataTransfer.files && e.dataTransfer.files.length > 0 && !isUploading) {
                isUploading = true;
                const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
                if (files.length > 0) {
                    uploadMultipleFiles(files, editorWrapper).finally(() => isUploading = false);
                } else {
                    isUploading = false;
                }
            }
        });
    }

    // Paste handler remains the same
    // 只绑定一次 paste 事件,优先绑定到 editorWrapper,如果不存在则绑定到 codeMirror,避免重复上传
    if (editorWrapper) {
        editorWrapper.addEventListener('paste', (e) => {
            const items = (e.clipboardData || e.originalEvent.clipboardData).items;
            const imageFiles = [];
            for (let i = 0; i < items.length; i++) {
                if (items[i].type.indexOf('image') !== -1) {
                    const file = items[i].getAsFile();
                    if (file) imageFiles.push(file);
                }
            }
            if (imageFiles.length > 0 && !isUploading) {
                e.preventDefault();
                isUploading = true;
                uploadMultipleFiles(imageFiles, editorWrapper).finally(() => isUploading = false);
            }
        });
    } else if (codeMirror) {
        codeMirror.addEventListener('paste', (e) => {
            const items = (e.clipboardData || e.originalEvent.clipboardData).items;
            const imageFiles = [];
            for (let i = 0; i < items.length; i++) {
                if (items[i].type.indexOf('image') !== -1) {
                    const file = items[i].getAsFile();
                    if (file) imageFiles.push(file);
                }
            }
            if (imageFiles.length > 0 && !isUploading) {
                e.preventDefault();
                isUploading = true;
                uploadMultipleFiles(imageFiles, codeMirror).finally(() => isUploading = false);
            }
        });
    }

     if (codeMirror) {
        codeMirror.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (!isUploading) codeMirror.style.border = '2px dashed #000';
        });
        codeMirror.addEventListener('dragleave', (e) => {
            e.preventDefault();
            e.stopPropagation();
            codeMirror.style.border = '';
        });
        codeMirror.addEventListener('drop', (e) => {
            e.preventDefault();
            e.stopPropagation();
            codeMirror.style.border = '';
            if (e.dataTransfer.files && e.dataTransfer.files.length > 0 && !isUploading) {
                isUploading = true;
                const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
                if (files.length > 0) {
                    uploadMultipleFiles(files, codeMirror).finally(() => isUploading = false);
                } else {
                    isUploading = false;
                }
            }
        });
    }


    async function uploadMultipleFiles(files, container) {
        if (files.length === 0) return;
        showUploadHint(container, files.length);
        const selectedHost = GM_getValue('imageHost', 'lankong'); // 获取当前选择的图床
        const uploadPromises = files.map(file => {
            const formData = new FormData();
            // 根据选择的图床调整 FormData 的文件字段名
            // Cloudflare-ImgBed README 示例使用 'file' 字段名
            if (selectedHost === 'lankong' || selectedHost === 'lankong-custom' || selectedHost === 'uhsea' || selectedHost === 'cloudflare-imgbed') {
                 formData.append('file', file, file.name);
            } else { // 16 图床等可能使用 'image'
                 formData.append('image', file, file.name);
            }

            return uploadToImageHost(formData, file.name, selectedHost); // 传递选中的图床类型
        });
        try {
            await Promise.all(uploadPromises);
        } catch (error) {
            console.error('批量上传失败:', error);
             alert('部分或全部图片上传失败,请查看控制台了解详情。'); // 错误提示
        } finally {
            removeUploadHint(container);
        }
    }

    function uploadToImageHost(formData, fileName, host) {
        return new Promise((resolve, reject) => {
            const selectedHost = host; // 使用传入的host参数
            let apiUrl, headers = {}; // 初始化headers为空对象

            if (selectedHost === 'cloudflare-imgbed') { // 处理 Cloudflare ImgBed 选项
                const baseApiUrl = GM_getValue('cloudflareImgbedApi', '').trim();
                const authCode = GM_getValue('cloudflareImgbedAuthCode', '').trim();
                // 获取压缩选项的状态,默认值也改为 true
                const enableCompress = GM_getValue('cloudflareImgbedCompress', true); // <-- 默认值改为 true

                 if (!baseApiUrl) {
                    console.error('Cloudflare ImgBed 需要设置域名');
                    reject(new Error('Cloudflare ImgBed 需要设置域名'));
                    return;
                }
                if (!authCode) {
                    console.error('Cloudflare ImgBed 需要设置 Auth Code');
                    reject(new Error('Cloudflare ImgBed 需要设置 Auth Code'));
                    return;
                }

                // 根据 README 示例构建上传 URL,authCode 作为查询参数
                // 确保 baseApiUrl 不以斜杠结尾,除非它就是根路径 "/"
                const cleanedBaseUrl = baseApiUrl.endsWith('/') && baseApiUrl !== '/' ? baseApiUrl.slice(0, -1) : baseApiUrl;
                apiUrl = `${cleanedBaseUrl}/upload?authCode=${encodeURIComponent(authCode)}`;

                // 始终添加 serverCompress 参数,值为 enableCompress 的布尔值字符串形式
                apiUrl += '&serverCompress=' + enableCompress; // <-- 始终添加参数,值为 true 或 false

                // 根据 README 示例,不需要 Authorization 头
                // headers = {}; // Already initialized

                // Cloudflare ImgBed 的上传逻辑
                 GM_xmlhttpRequest({
                    method: 'POST',
                    url: apiUrl,
                    headers: headers, // 使用空的headers对象
                    data: formData,
                    onload: (response) => {
                        try {
                            // 根据你提供的 README 示例解析响应 [ { "src": "/file/..." } ]
                            const jsonResponse = JSON.parse(response.responseText);
                            if (response.status >= 200 && response.status < 300 && Array.isArray(jsonResponse) && jsonResponse.length > 0 && jsonResponse[0].src) {
                                 // 拼接完整的图片 URL: 域名 + src 路径
                                 const imageUrl = cleanedBaseUrl + jsonResponse[0].src;
                                 const markdownImage = `![${fileName.split('.').slice(0, -1).join('.')}](${imageUrl})`;
                                 console.log('Cloudflare-ImgBed 上传成功,Markdown:', markdownImage);
                                 insertToEditor(markdownImage);
                                 resolve();
                             }
                            else {
                                console.error('Cloudflare-ImgBed 上传成功但返回格式无效或失败:', jsonResponse);
                                // 尝试显示服务器返回的错误信息,虽然示例没有明确错误格式,但以防万一
                                const errorMessage = jsonResponse && (jsonResponse.message || jsonResponse.error || JSON.stringify(jsonResponse));
                                reject(new Error(`上传失败:服务器返回无效响应或错误 (${response.status}): ${errorMessage}`));
                            }
                        } catch (error) {
                            console.error('解析 Cloudflare-ImgBed 响应错误:', error);
                            reject(new Error(`解析服务器响应失败: ${error.message}`));
                        }
                    },
                    onerror: (error) => {
                        console.error('Cloudflare-ImgBed 上传错误详情:', error);
                        reject(new Error(`上传请求失败: ${error.statusText || error.message || JSON.stringify(error)}`));
                    },
                    ontimeout: () => {
                        console.error('Cloudflare-ImgBed 请求超时');
                        reject(new Error('上传请求超时'));
                    },
                    timeout: 30000 // 适当增加超时时间
                });

            } else if (selectedHost === 'lankong') {
                apiUrl = 'https://img.sss.wiki/api/index.php';
                headers = {}; // 不需要特殊头

                // 字段名改为 image,移除 file 字段
                if (formData.has('file')) {
                    const file = formData.get('file');
                    formData.delete('file');
                    formData.append('image', file, file.name);
                }
                // 固定 token
                formData.append('token', '7e2314b2b32aaf146c0bdfa460c1784b');

                GM_xmlhttpRequest({
                    method: 'POST',
                    url: apiUrl,
                    headers: headers,
                    data: formData,
                    timeout: 10000,
                    onload: (response) => {
                        try {
                            const jsonResponse = JSON.parse(response.responseText);
                            // 假设返回格式为 { url: "图片链接" }
                            if (response.status === 200 && jsonResponse && jsonResponse.url) {
                                const imageUrl = jsonResponse.url;
                                const markdownImage = `![${fileName.split('.').slice(0, -1).join('.')}](${imageUrl})`;
                                console.log('银星图床上传成功,Markdown:', markdownImage);
                                insertToEditor(markdownImage);
                                resolve();
                            } else {
                                console.error('银星图床上传成功但未获取到有效链接:', jsonResponse);
                                reject(new Error('Invalid response from 银星图床'));
                            }
                        } catch (error) {
                            console.error('解析银星图床响应错误:', error);
                            reject(error);
                        }
                    },
                    onerror: (error) => { console.error('银星图床上传错误详情:', error); reject(error); },
                    ontimeout: () => { console.error('银星图床请求超时'); reject(new Error('Timeout')); }
                });

            } else if (selectedHost === 'lankong-custom') {
                const api = GM_getValue('lankongCustomApi', '').trim();
                const token = GM_getValue('lankongCustomToken', '').trim();
                if (!api) { console.error('兰空图床需要设置 API 端点'); reject(new Error('兰空图床需要设置 API 端点')); return; }
                if (!token) { console.error('兰空图床需要设置 Token'); reject(new Error('兰空图床需要设置 Token')); return; }
                apiUrl = api;
                headers = { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' };
                 // 原有 Lankong Custom 上传逻辑
                 GM_xmlhttpRequest({
                    method: 'POST', url: apiUrl, headers: headers, data: formData, timeout: 10000,
                    onload: (response) => {
                        try {
                            const jsonResponse = JSON.parse(response.responseText);
                            if (response.status === 200 && jsonResponse && jsonResponse.data && jsonResponse.data.links && jsonResponse.data.links.url) {
                                const imageUrl = jsonResponse.data.links.url;
                                const markdownImage = `![${fileName.split('.').slice(0, -1).join('.')}](${imageUrl})`;
                                console.log('兰空图床上传成功,Markdown:', markdownImage);
                                insertToEditor(markdownImage);
                                resolve();
                            } else {
                                console.error('兰空图床上传成功但未获取到有效链接:', jsonResponse);
                                reject(new Error('Invalid response from 兰空图床'));
                            }
                        } catch (error) {
                            console.error('解析兰空图床响应错误:', error);
                            reject(error);
                        }
                    },
                    onerror: (error) => { console.error('兰空图床上传错误详情:', error); reject(error); },
                    ontimeout: () => { console.error('兰空图床请求超时'); reject(new Error('Timeout')); }
                 });

            } else if (selectedHost === 'uhsea') {
                apiUrl = 'https://uhsea.com/Frontend/upload';
                headers = {}; // Uhsea 可能不需要特定头
                 // 原有 Uhsea 上传逻辑
                 GM_xmlhttpRequest({
                     method: 'POST', url: apiUrl, headers: headers, data: formData, timeout: 10000,
                     onload: (response) => {
                         try {
                             const jsonResponse = JSON.parse(response.responseText);
                             if (response.status === 200 && jsonResponse && jsonResponse.data) {
                                 const imageUrl = jsonResponse.data;
                                 const markdownImage = `![${fileName.split('.').slice(0, -1).join('.')}](${imageUrl})`;
                                 console.log('屋舍图床上传成功,Markdown:', markdownImage);
                                 insertToEditor(markdownImage);
                                 resolve();
                             } else {
                                 console.error('屋舍图床上传成功但未获取到有效链接:', jsonResponse);
                                 reject(new Error('Invalid response from Uhsea'));
                             }
                         } catch (error) {
                             console.error('解析屋舍图床响应错误:', error);
                             reject(error);
                         }
                     },
                     onerror: (error) => { console.error('屋舍图床上传错误详情:', error); reject(error); },
                     ontimeout: () => { console.error('屋舍图床请求超时'); reject(new Error('Timeout')); }
                 });

            } else if (selectedHost === 'sixteen') {
                 apiUrl = 'https://i.111666.best/image'; // 16 图床的上传地址
                 const token = GM_getValue('sixteenToken', '').trim();
                 if (!token) { console.error('16 图床需要设置 Auth-Token'); reject(new Error('16 图床需要设置 Auth-Token')); return; }
                 headers = { 'Auth-Token': token };
                 // 原有 16 图床上传逻辑
                 GM_xmlhttpRequest({
                     method: 'POST', url: apiUrl, headers: headers, data: formData, timeout: 10000,
                     onload: (response) => {
                         try {
                             if (response.status === 200 && response.responseText) {
                                 const jsonResponse = JSON.parse(response.responseText);
                                 if (jsonResponse.ok && jsonResponse.src) {
                                     // 16图床返回的src是路径,需要拼接域名
                                     const imageUrl = `https://i.111666.best${jsonResponse.src}`;
                                     const markdownImage = `![${fileName.split('.').slice(0, -1).join('.')}](${imageUrl})`;
                                     console.log('16 图床上传成功,Markdown:', markdownImage);
                                     insertToEditor(markdownImage);
                                     resolve();
                                 } else {
                                     console.error('16 图床返回的响应无效:', jsonResponse);
                                     reject(new Error('Invalid response from 16 图床'));
                                 }
                             } else {
                                  console.error('16 图床上传失败:', response.responseText);
                                  reject(new Error(`Upload failed on 16 图床: ${response.status} ${response.statusText}`));
                             }
                         } catch (error) {
                             console.error('解析 16 图床响应错误:', error);
                             reject(error);
                         }
                     },
                     onerror: (error) => { console.error('16 图床上传错误详情:', error); reject(error); },
                     ontimeout: () => { console.error('16 图床请求超时'); reject(new Error('Timeout')); }
                 });
            }
            // 可以根据需要添加更多的 else if 来支持其他图床类型
            else {
                console.error(`未知的图床选项: ${selectedHost}`);
                reject(new Error(`未知的图床选项: ${selectedHost}`));
            }
        });
    }


    function insertToEditor(markdown) {
        if (cmInstance) {
            const cursor = cmInstance.getCursor();
            cmInstance.replaceRange(markdown + '\n', cursor);
            console.log('已插入 Markdown 到编辑器');
        } else {
            const editable = document.querySelector('.CodeMirror textarea') || document.querySelector('textarea');
            if (editable) {
                const start = editable.selectionStart;
                const end = editable.selectionEnd;
                editable.value = editable.value.substring(0, start) + markdown + '\n' + editable.value.substring(end);
                editable.selectionStart = editable.selectionEnd = start + markdown.length + 1;
                console.log('已插入 Markdown 到 textarea');
                const event = new Event('input', { bubbles: true });
                editable.dispatchEvent(event);
            } else {
                console.error('未找到可编辑的 CodeMirror 实例或 textarea');
            }
        }
    }
})();