NodeSeek 编辑器增强个人修改版

为 NodeSeek 编辑器增加图片上传功能,增加了16图床

// ==UserScript==
// @name         NodeSeek 编辑器增强个人修改版
// @namespace    https://www.nodeseek.com/
// @version      0.0.10
// @description  为 NodeSeek 编辑器增加图片上传功能,增加了16图床
// @author       TomyJan
// @match        *://www.nodeseek.com/*
// @icon         https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png
// @grant        GM_xmlhttpRequest
// @license      MPL-2.0 License
// @supportURL   https://www.nodeseek.com/post-74493-1
// @homepageURL  https://www.nodeseek.com/post-74493-1
// ==/UserScript==

/**
 *
 *
 * 当前版本更新日志
 * 0.0.10 - 2024.08.11          !!!更新前注意备份您的配置!!!
 * - 新增 支持 Chevereto 图床
 */

(function () {
    'use strict';

    // 图床配置, 默认提供的是这位大佬的 https://www.nodeseek.com/post-38305-1 , 这个图床上传限制 5p / IP / 小时
    // 当 type 为 LskyPro 时以下所有配置项均有用, 为 Chevereto 时 url 和 token 有用, 为 Telegraph / EasyImages 时只有 url 有用
    // Telegraph 官网 https://telegra.ph/ 在大陆被阻断, 可以使用 https://github.com/cf-pages/Telegraph-Image 提供的服务或者自己部署
    // EasyImages 官网 https://png.cm/ 限制单 ip 每天上传 3 张, 项目地址 https://github.com/icret/EasyImages2.0, 这个图床真烂, 两套接口不统一下, 文档也不写几句话
    const imgHost = {
        type: "16best", // 图床类型, 支持 LskyPro / Telegraph / Chevereto / EasyImages
        url: "https://i.111666.best/image", // 图床地址, 带上协议头
        token: "aYDXetX-j4RfjmNF2ZE4M", // 图床 token, 可选, 不填则为游客上传, LskyPro 在 /user/tokens 生成, Chevereto 必填, 在 /settings/api 生成, EasyImages 填写则使用后端接口上传, 不填写则使用前端接口上传
        storageId: null, // 图床存储策略ID, 可选项, 不填则为默认策略, 普通用户可在上传页抓包得到, 管理员可以在后台看到
    };
    const mdImgName = 0; // 0(非 Telegraph): 使用图床返回的原始名称, 其他值则名称固定为该值
    const submitByKey = true; // 是否按下 Ctrl+Enter 后触发发帖动作

    // 页面加载完毕后载入功能
    window.addEventListener('load', initEditorEnhancer, false);

    function initEditorEnhancer() {
        // 监听粘贴事件
        document.addEventListener('paste', (event) => handlePasteEvt(event));

        // 给编辑器绑定拖拽事件
        var dropZone = document.getElementById('code-mirror-editor');
        // 阻止默认行为
        dropZone.addEventListener('dragover', function (e) {
            e.preventDefault();
            e.stopPropagation();
            e.dataTransfer.dropEffect = 'copy'; // 显示为复制图标
        });

        // 处理文件拖放
        dropZone.addEventListener('drop', function (e) {
            e.preventDefault();
            e.stopPropagation();

            log('正在处理拖放内容...');
            let imageFiles = [];
            for (let file of e.dataTransfer.files) {
                if (/^image\//i.test(file.type)) { // 确保只处理图片文件
                    imageFiles.push(file);
                    log(`拖放的文件名: ${file.name}`);
                }
            }
            log(`拖放的图片数量: ${imageFiles.length}`);
            if (imageFiles.length === 0) {
                log('你拖放的内容好像没有图片哦', 'red');
                return;
            }

            // 调整uploadImage函数以接受File对象数组而不是DataTransferItemList
            uploadImage(imageFiles.map(file => {
                return {
                    kind: 'file',
                    type: file.type,
                    getAsFile: () => file
                };
            }));
        });

        // 修改图片按钮的行为
        // 图片按钮
        let checkExist = setInterval(function () {
            const oldElement = document.querySelector('.toolbar-item.i-icon.i-icon-pic[title="图片"]');
            if (oldElement) {
                clearInterval(checkExist);
                const newElement = oldElement.cloneNode(true);
                oldElement.parentNode.replaceChild(newElement, oldElement);
                newElement.addEventListener('click', handleImgBtnClick);
            }
        }, 200);

        // 监听 Ctrl+Enter 快捷键
        if (submitByKey)
            document.addEventListener('keydown', function (event) {
                if (event.ctrlKey && event.key === 'Enter') {
                    // 获取按钮元素
                    const button = document.querySelector('.submit.btn');
                    // 触发点击事件
                    button.click();

                }
            });

    }

    // 粘贴事件处理
    function handlePasteEvt(event) {
        log('正在处理粘贴内容...');
        const items = (event.clipboardData || event.originalEvent.clipboardData).items;
        if (items.length === 0) {
            log('你粘贴的内容好像没有图片哦', 'red');
            return;
        }
        uploadImage(items)
    }

    // 图片按钮点击事件处理
    function handleImgBtnClick() {
        // 创建一个隐藏的文件输入元素
        const input = document.createElement('input');
        input.type = 'file';
        input.multiple = true; // 允许多选文件
        input.accept = 'image/*'; // 仅接受图片文件

        // 当文件被选择后的处理
        input.onchange = e => {
            const files = e.target.files; // 获取用户选择的文件列表
            if (files.length) {
                const items = [...files].map(file => ({
                    kind: 'file',
                    type: file.type,
                    getAsFile: () => file
                }));

                uploadImage(items);
            }
        };

        // 触发文件输入框的点击事件,打开文件选择窗口
        input.click();
    }

    // 处理并上传图片
    async function uploadImage(items) {
        let imageFiles = [];

        for (let item of items) {
            if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
                let blob = item.getAsFile();
                imageFiles.push(blob);
            }
        }

        if (imageFiles.length > 0) {
            event.preventDefault();
            for (let i = 0; i < imageFiles.length; i++) {
                if (imageFiles.length > 1)
                    log(`上传第 ${i + 1} / ${imageFiles.length} 张图片...`);
                else
                    log(`上传图片...`);
                let file = imageFiles[i];
                let formData = new FormData();
                formData.append('file', file);
                if (imgHost.type === 'LskyPro') {
                    if (imgHost.storageId) formData.append('strategy_id', imgHost.storageId);
                    await uploadToLsky(formData);
                } else if (imgHost.type === 'Telegraph') {
                    await uploadToTelegraph(formData);
                } else if (imgHost.type === 'Chevereto') {
                    await uploadToChevereto(file);
                } else if (imgHost.type === 'EasyImages') {
                    await uploadToEasyImages(file);
                } else if (imgHost.type === '16best') {
                    await uploadTo16Images(file)
                } else {
                    log(`暂不支持的图床类型: ${imgHost.type}, 取消上传`, 'red');
                    return;
                }
            }

        } else {
            log('你粘贴的内容好像没有图片哦', 'red');
        }
    }

    async function uploadToLsky(formData) {
        return new Promise((resolve, reject) => {
            let headers = {
                'Accept': 'application/json'
            };
            if (imgHost.token)
                headers['Authorization'] = `Bearer ${imgHost.token}`;

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/api/v1/upload`,
                headers: headers,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.message);
                    }
                    if (rspJson.status === true) {
                        // 图片上传成功
                        if (rspJson?.data?.links?.markdown)
                            insertToEditor(mdImgName === 0 ? rspJson.data.links.markdown : `![${mdImgName}](${rspJson.data.links.url})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${rspJson.message}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToTelegraph(formData) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/upload`,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    rspJson = rspJson[0];
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.message);
                    }
                    if (rspJson) {
                        // 图片上传成功
                        if (rspJson?.src)
                            insertToEditor(`![${mdImgName}](${imgHost.url}${rspJson.src})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToChevereto(file) {
        return new Promise((resolve, reject) => {
            let headers = {
                'Accept': 'application/json'
            };
            if (!imgHost.token) {
                log('Chevereto 图床需配置 token', 'red');
                reject('Chevereto 图床需要 token, 请填写 token 后重试');
                return;
            }
            headers['X-API-Key'] = imgHost.token;
            let formData = new FormData();
            formData.append('source', file);

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${imgHost.url}/api/1/upload`,
                headers: headers,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson?.success?.message || rspJson?.error?.message);
                    }
                    if (rspJson.status_code === 200) {
                        // 图片上传成功
                        let imgUrl = rspJson.image.url || rspJson.image.url_viewer || rspJson.image.url_short;
                        if (imgUrl)
                            insertToEditor(mdImgName === 0 ? `![${rspJson.image.filename}](${imgUrl})` : `![${mdImgName}](${imgUrl})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${rspJson?.success?.message || rspJson?.error?.message}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadToEasyImages(file) {
        return new Promise((resolve, reject) => {
            let url = imgHost.url;
            let formData = new FormData();
            if (imgHost.token) {
                // 带token, 使用后端接口上传
                url += '/api/index.php'
                formData.append('token', imgHost.token);
                formData.append('image', file);
            } else {
                // 不带token, 使用前端接口上传
                url += '/app/upload.php'
                formData.append('file', file);
                // 十位时间戳作为sign
                formData.append('sign', Math.floor(Date.now() / 1000));
            }


            GM_xmlhttpRequest({
                method: 'POST',
                url: url,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.result);
                    }
                    if (rspJson.code === 200) {
                        // 图片上传成功
                        if (rspJson?.url)
                            insertToEditor(`![${(mdImgName === 0 ? rspJson.srcName : mdImgName)}](${rspJson.url})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    async function uploadTo16Images(file) {
        return new Promise((resolve, reject) => {
            let url = imgHost.url;
            let formData = new FormData();

            // 不带token, 使用前端接口上传
            formData.append('file', file);
            let headers = {
                'Accept': 'application/json'
            };
            headers['Auth-Token'] = imgHost.token;

            GM_xmlhttpRequest({
                method: 'POST',
                url: url,
                headers: headers,
                data: formData,
                onload: (rsp) => {
                    let rspJson = JSON.parse(rsp.responseText);
                    if (rsp.status !== 200) {
                        log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
                        reject(rspJson.result);
                    }
                    if (rspJson.ok) {
                        // 图片上传成功
                        if (rspJson?.src)
                            insertToEditor(`![${(mdImgName === 0 ? rspJson.srcName : mdImgName)}](https://i.111666.best${rspJson.src})`);
                        else {
                            log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
                            insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
                        }
                    } else
                        log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
                    resolve();
                },
                onerror: (error) => {
                    log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
                    reject(error);
                }
            });
        });
    }

    function insertToEditor(markdownLink) {
        const codeMirrorElement = document.querySelector('.CodeMirror');
        if (codeMirrorElement) {
            const codeMirrorInstance = codeMirrorElement.CodeMirror;
            if (codeMirrorInstance) {
                const cursor = codeMirrorInstance.getCursor();
                codeMirrorInstance.replaceRange(`\n${markdownLink} \n`, cursor);
            }
        }
        if (markdownLink.startsWith('!['))
            log('图片已插入到编辑器~', 'green');
    }

    // 在编辑器打印日志
    function log(message, color = '') {
        if (!document.getElementById('editor-enhance-logs')) {
            initEditorLogDiv();
        }
        const logDiv = document.getElementById('editor-enhance-logs');
        logDiv.innerHTML = `<div${color ? ` style="color: ${color};` : ''}">&nbsp;&nbsp;&nbsp;${message}&nbsp;</div>`;

        console.log(`[NodeSeek-Editor-Enhance] ${message}`);
    }

    // 初始化显示日志的容器
    function initEditorLogDiv() {
        const logDiv = document.createElement('div');
        logDiv.id = 'editor-enhance-logs';
        logDiv.innerHTML = '';
        document.body.appendChild(logDiv);

        const editorToolbarDiv = document.querySelector('.mde-toolbar');
        editorToolbarDiv.appendChild(logDiv);
    }

})();