返回列表 发布新帖
查看: 598|回复: 6

分享一个粘贴图片自动上传图床的油猴脚本(适用奶昔论坛)

发表于 2024-12-19 15:33:23 | 查看全部 |阅读模式

立刻注册账号,享受更清爽的界面!

您需要 登录 才可以下载或查看,没有账号?注册

×
但是这个脚本不支持hostloc,然后由于是油猴脚本都是js.所以用GPT帮忙改了下. 现在能支持hostloc了. 其它类似hostloc 的discuz bbs 理论也能用.你们自己添加// @match 匹配url 页面试试. 我只测试了hostloc.

下一次更新 希望看能不能搞上imgur免费图床.个人还是觉得imgur非常稳定的.
  1. // ==UserScript==
  2. // @name         奶昔论坛 编辑器增强
  3. // @namespace    https://forum.naixi.net/
  4. // @version      0.1.00
  5. // @description  为 奶昔论坛 编辑器增加图片上传功能
  6. // @author       Zhiyang
  7. // @match        *://forum.naixi.net/*
  8. // @icon         https://forum.naixi.net/static/image/common/logo.svg
  9. // @grant        GM_xmlhttpRequest
  10. // @license      MPL-2.0 License
  11. // @supportURL   https://forum.naixi.net/thread-2295-1-1.html
  12. // @homepageURL  https://forum.naixi.net/thread-2295-1-1.html
  13. // ==/UserScript==

  14. /**
  15. *
  16. *
  17. * 当前版本更新日志
  18. * 0.0.11 - 2024.12.18          !!!更新前注意备份您的配置!!!
  19. * - 新增 支持 HOSTLOC
  20. * 0.0.11 - 2024.10.05          !!!更新前注意备份您的配置!!!
  21. * - 新增 支持 0-RTT/telegraph 项目
  22. */

  23. (function () {
  24.     'use strict';

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

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

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

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

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

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

  68.             // 调整uploadImage函数以接受File对象数组而不是DataTransferItemList
  69.             uploadImage(imageFiles.map(file => {
  70.                 return {
  71.                     kind: 'file',
  72.                     type: file.type,
  73.                     getAsFile: () => file
  74.                 };
  75.             }));
  76.         });

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

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

  96.                 }
  97.             });

  98.     }

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

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

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

  125.                 uploadImage(items);
  126.             }
  127.         };

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

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

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

  140.         if (imageFiles.length > 0) {
  141.             event.preventDefault();
  142.             for (let i = 0; i < imageFiles.length; i++) {
  143.                 if (imageFiles.length > 1)
  144.                     log(`上传第 ${i + 1} / ${imageFiles.length} 张图片...`);
  145.                 else
  146.                     log(`上传图片...`);
  147.                 let file = imageFiles[i];
  148.                 let formData = new FormData();
  149.                 formData.append('file', file);
  150.                 if (imgHost.type === 'LskyPro') {
  151.                     if (imgHost.storageId) formData.append('strategy_id', imgHost.storageId);
  152.                     await uploadToLsky(formData);
  153.                 } else if (imgHost.type === 'Telegraph') {
  154.                     await uploadToTelegraph(formData);
  155.                 } else if (imgHost.type === 'Telegraph2') {
  156.                     await uploadToTelegraph2(formData);
  157.                 } else if (imgHost.type === 'Chevereto') {
  158.                     await uploadToChevereto(file);
  159.                 } else if (imgHost.type === 'EasyImages') {
  160.                     await uploadToEasyImages(file);
  161.                 } else {
  162.                     log(`暂不支持的图床类型: ${imgHost.type}, 取消上传`, 'red');
  163.                     return;
  164.                 }
  165.             }

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

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

  177.             GM_xmlhttpRequest({
  178.                 method: 'POST',
  179.                 url: `${imgHost.url}/api/v1/upload`,
  180.                 headers: headers,
  181.                 data: formData,
  182.                 onload: (rsp) => {
  183.                     let rspJson = JSON.parse(rsp.responseText);
  184.                     if (rsp.status !== 200) {
  185.                         log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  186.                         reject(rspJson.message);
  187.                     }
  188.                     if (rspJson.status === true) {
  189.                         // 图片上传成功
  190.                         if (rspJson?.data?.links?.markdown)
  191.                             insertToEditor(mdImgName === 0 ? rspJson.data.links.markdown : `![${mdImgName}](${rspJson.data.links.url})`);
  192.                         else {
  193.                             log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  194.                             insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  195.                         }
  196.                     } else
  197.                         log(`图片上传失败: ${rspJson.message}`, 'red');
  198.                     resolve();
  199.                 },
  200.                 onerror: (error) => {
  201.                     log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  202.                     reject(error);
  203.                 }
  204.             });
  205.         });
  206.     }

  207.     async function uploadToTelegraph(formData) {
  208.         return new Promise((resolve, reject) => {
  209.             GM_xmlhttpRequest({
  210.                 method: 'POST',
  211.                 url: `${imgHost.url}/upload`,
  212.                 data: formData,
  213.                 onload: (rsp) => {
  214.                     let rspJson = JSON.parse(rsp.responseText);
  215.                     rspJson = rspJson[0];
  216.                     if (rsp.status !== 200) {
  217.                         log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  218.                         reject(rspJson.message);
  219.                     }
  220.                     if (rspJson) {
  221.                         // 图片上传成功
  222.                         if (rspJson?.src)
  223.                             insertToEditor(`![${mdImgName}](${imgHost.url}${rspJson.src})`);
  224.                         else {
  225.                             log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  226.                             insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  227.                         }
  228.                     } else
  229.                         log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
  230.                     resolve();
  231.                 },
  232.                 onerror: (error) => {
  233.                     log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  234.                     reject(error);
  235.                 }
  236.             });
  237.         });
  238.     }

  239.     async function uploadToTelegraph2(formData) {
  240.         return new Promise((resolve, reject) => {
  241.             GM_xmlhttpRequest({
  242.                 method: 'POST',
  243.                 url: `${imgHost.url}/upload`,
  244.                 data: formData,
  245.                 onload: (rsp) => {
  246.                     let rspJson = JSON.parse(rsp.responseText);

  247.                     if (rsp.status !== 200 || !rspJson || !rspJson.data) {
  248.                         log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  249.                         reject(rspJson?.message || '图片上传失败,缺少 data 字段');
  250.                     }

  251.                     const result = rspJson.data;

  252.                     // 图片上传成功
  253.                     if (result) {

  254.                         insertToCursor(`${result}`, `${mdImgName}`);
  255.                         log('图片上传成功', 'green');
  256.                         resolve(result);
  257.                     } else {
  258.                         log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');

  259.                         insertToCursor(null, null, `图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)}`)
  260.                         resolve();
  261.                     }
  262.                 },
  263.                 onerror: (error) => {
  264.                     log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  265.                     reject(error);
  266.                 }
  267.             });
  268.         });
  269.     }

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

  283.             GM_xmlhttpRequest({
  284.                 method: 'POST',
  285.                 url: `${imgHost.url}/api/1/upload`,
  286.                 headers: headers,
  287.                 data: formData,
  288.                 onload: (rsp) => {
  289.                     let rspJson = JSON.parse(rsp.responseText);
  290.                     if (rsp.status !== 200) {
  291.                         log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  292.                         reject(rspJson?.success?.message || rspJson?.error?.message);
  293.                     }
  294.                     if (rspJson.status_code === 200) {
  295.                         // 图片上传成功
  296.                         let imgUrl = rspJson.image.url || rspJson.image.url_viewer || rspJson.image.url_short;
  297.                         if (imgUrl)
  298.                             insertToEditor(mdImgName === 0 ? `![${rspJson.image.filename}](${imgUrl})` : `![${mdImgName}](${imgUrl})`);
  299.                         else {
  300.                             log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  301.                             insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  302.                         }
  303.                     } else
  304.                         log(`图片上传失败: ${rspJson?.success?.message || rspJson?.error?.message}`, 'red');
  305.                     resolve();
  306.                 },
  307.                 onerror: (error) => {
  308.                     log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  309.                     reject(error);
  310.                 }
  311.             });
  312.         });
  313.     }

  314.     async function uploadToEasyImages(file) {
  315.         return new Promise((resolve, reject) => {
  316.             let url = imgHost.url;
  317.             let formData = new FormData();
  318.             if (imgHost.token) {
  319.                 // 带token, 使用后端接口上传
  320.                 url += '/api/index.php'
  321.                 formData.append('token', imgHost.token);
  322.                 formData.append('image', file);
  323.             } else {
  324.                 // 不带token, 使用前端接口上传
  325.                 url += '/app/upload.php'
  326.                 formData.append('file', file);
  327.                 // 十位时间戳作为sign
  328.                 formData.append('sign', Math.floor(Date.now() / 1000));
  329.             }


  330.             GM_xmlhttpRequest({
  331.                 method: 'POST',
  332.                 url: url,
  333.                 data: formData,
  334.                 onload: (rsp) => {
  335.                     let rspJson = JSON.parse(rsp.responseText);
  336.                     if (rsp.status !== 200) {
  337.                         log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  338.                         reject(rspJson.result);
  339.                     }
  340.                     if (rspJson.code === 200) {
  341.                         // 图片上传成功
  342.                         if (rspJson?.url)
  343.                             insertToEditor(`![${(mdImgName === 0 ? rspJson.srcName : mdImgName)}](${rspJson.url})`);
  344.                         else {
  345.                             log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  346.                             insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  347.                         }
  348.                     } else
  349.                         log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
  350.                     resolve();
  351.                 },
  352.                 onerror: (error) => {
  353.                     log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  354.                     reject(error);
  355.                 }
  356.             });
  357.         });
  358.     }

  359.     function insertToCursor(sourceUrl, sourceName, errorString) {
  360.         const codeMirrorElement = document.querySelector('.CodeMirror');
  361.         const discuzEditorTextarea = document.querySelector('#e_textarea');


  362.         if (codeMirrorElement) { // nodeseek 使用markdown语法
  363.             const fullLink = `![${sourceName}](${sourceUrl})`
  364.             const codeMirrorInstance = codeMirrorElement.CodeMirror;
  365.             if (codeMirrorInstance) {
  366.                 const cursor = codeMirrorInstance.getCursor();

  367.                 codeMirrorInstance.replaceRange(`\n${fullLink} \n`, cursor);
  368.             }
  369.         } else if (discuzEditorTextarea) { //discuz 使用 BBS code语法
  370.             const fullLink = `[img]${sourceUrl}[/img]`
  371.             const cursorPosition = discuzEditorTextarea.selectionStart; // 获取光标位置
  372.             const textBefore = discuzEditorTextarea.value.substring(0, cursorPosition); // 光标前的文本
  373.             const textAfter = discuzEditorTextarea.value.substring(cursorPosition); // 光标后的文本

  374.             const markdownLink = fullLink; // 你要插入的内容
  375.             discuzEditorTextarea.value = `${textBefore}${markdownLink}${textAfter}`; // 拼接插入的文本

  376.             // 更新光标位置到插入文本的末尾
  377.             discuzEditorTextarea.selectionStart = discuzEditorTextarea.selectionEnd = cursorPosition + markdownLink.length;

  378.             // 让光标保持在文本框中
  379.             discuzEditorTextarea.focus();
  380.         } else {
  381.             log('出现错误: 未找到编辑栏', 'red');
  382.         }

  383.     }

  384.     function insertToEditor(fullLink, sourceName, sourceUrl) {

  385.         const codeMirrorElement = document.querySelector('.CodeMirror');

  386.         if (codeMirrorElement) { // nodeseek 使用markdown语法
  387.             // nodeseek 还是用老逻辑 完整链接
  388.             const codeMirrorInstance = codeMirrorElement.CodeMirror;
  389.             if (codeMirrorInstance) {
  390.                 const cursor = codeMirrorInstance.getCursor();
  391.                 codeMirrorInstance.replaceRange(`\n${fullLink} \n`, cursor);
  392.             }
  393.         }
  394.         if (fullLink.startsWith('!['))
  395.             log('图片已插入到编辑器~', 'green');
  396.     }

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

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

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

  412.         const editorToolbarDiv = document.querySelector('.mde-toolbar') || document.querySelector('#e_controls');
  413.         editorToolbarDiv.appendChild(logDiv);
  414.     }

  415. })();
复制代码
爱生活,爱奶昔~
发表于 2024-12-20 00:31:41 | 查看全部
好东西啊,试试看哈
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 2024-12-20 00:44:51 | 查看全部
以后传图片岂不是很方便
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 2024-12-20 10:48:49 | 查看全部
好东西啊,试试看哈
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 2024-12-20 17:30:12 | 查看全部
好像拖放或者粘贴都无效哈
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 2024-12-20 19:07:27 | 查看全部
直接粘贴确实方便
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

发表于 2025-1-3 22:59:33 | 查看全部
这个确实方便
爱生活,爱奶昔~
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

  • 关注公众号
  • 添加微信客服
© 2025 Naixi Networks 沪ICP备13020230号-1|沪公网安备 31010702007642号
关灯 在本版发帖
扫一扫添加微信客服
返回顶部
快速回复 返回顶部 返回列表