Boltp for NodeSeek

在NodeSeek论坛的编辑器中快速把图片上传到闪电图床并引用

  1. // ==UserScript==
  2. // @name Boltp for NodeSeek
  3. // @namespace https://www.nodeseek.com/
  4. // @version 0.1.0
  5. // @description 在NodeSeek论坛的编辑器中快速把图片上传到闪电图床并引用
  6. // @author Boltp Team
  7. // @match *://www.nodeseek.com/*
  8. // @icon https://www.boltp.com/logo.ico
  9. // @grant GM_xmlhttpRequest
  10. // @license MPL-2.0 License
  11. // @supportURL https://www.boltp.com/page/NodeSeek
  12. // @homepageURL https://www.boltp.com/page/NodeSeek
  13. // ==/UserScript==
  14.  
  15. /**
  16. *
  17. *
  18. * 当前版本更新日志
  19. * 0.0.11 - 2025.01.01 !!!更新前注意备份您的配置!!!
  20. * - 新增 支持 0-RTT/telegraph 项目
  21. */
  22.  
  23. (function () {
  24. 'use strict';
  25.  
  26. // 图床配置, 默认提供的是这位大佬的 https://www.nodeseek.com/post-38305-1 , 这个图床上传限制 5p / IP / 小时
  27. // 当 type 为 LskyPro 时以下所有配置项均有用, 为 Chevereto 时 url 和 token 有用, 为 Telegraph / EasyImages 时只有 url 有用
  28. // Telegraph 官网 https://telegra.ph/ 在大陆被阻断, 可以使用 https://github.com/cf-pages/Telegraph-Image 提供的服务或者自己部署
  29. // Telegraph2 by @Xiefengshang 使用的是 https://github.com/0-RTT/telegraph 项目(个人考虑到其缓存做的更好所以使用)
  30. // EasyImages 官网 https://png.cm/ 限制单 ip 每天上传 3 张, 项目地址 https://github.com/icret/EasyImages2.0, 这个图床真烂, 两套接口不统一下, 文档也不写几句话
  31. const imgHost = {
  32. type: "LskyPro", // 图床类型, 支持 LskyPro / Telegraph / Telegraph2 / Chevereto / EasyImages
  33. url: "https://www.boltp.com", // 图床地址, 带上协议头
  34. token: null, // 图床 token, 可选, 不填则为游客上传, LskyPro 在 /user/tokens 生成, Chevereto 必填, 在 /settings/api 生成, EasyImages 填写则使用后端接口上传, 不填写则使用前端接口上传
  35. storageId: null, // 图床存储策略ID, 可选项, 不填则为默认策略, 普通用户可在上传页抓包得到, 管理员可以在后台看到
  36. };
  37. const mdImgName = 0; // 0(非 Telegraph): 使用图床返回的原始名称, 其他值则名称固定为该值
  38. const submitByKey = true; // 是否按下 Ctrl+Enter 后触发发帖动作
  39.  
  40. // 页面加载完毕后载入功能
  41. window.addEventListener('load', initEditorEnhancer, false);
  42.  
  43. function initEditorEnhancer() {
  44. // 监听粘贴事件
  45. document.addEventListener('paste', (event) => handlePasteEvt(event));
  46.  
  47. // 给编辑器绑定拖拽事件
  48. var dropZone = document.getElementById('code-mirror-editor');
  49. // 阻止默认行为
  50. dropZone.addEventListener('dragover', function (e) {
  51. e.preventDefault();
  52. e.stopPropagation();
  53. e.dataTransfer.dropEffect = 'copy'; // 显示为复制图标
  54. });
  55.  
  56. // 处理文件拖放
  57. dropZone.addEventListener('drop', function (e) {
  58. e.preventDefault();
  59. e.stopPropagation();
  60.  
  61. log('正在处理拖放内容...');
  62. let imageFiles = [];
  63. for (let file of e.dataTransfer.files) {
  64. if (/^image\//i.test(file.type)) { // 确保只处理图片文件
  65. imageFiles.push(file);
  66. log(`拖放的文件名: ${file.name}`);
  67. }
  68. }
  69. log(`拖放的图片数量: ${imageFiles.length}`);
  70. if (imageFiles.length === 0) {
  71. log('你拖放的内容好像没有图片哦', 'red');
  72. return;
  73. }
  74.  
  75. // 调整uploadImage函数以接受File对象数组而不是DataTransferItemList
  76. uploadImage(imageFiles.map(file => {
  77. return {
  78. kind: 'file',
  79. type: file.type,
  80. getAsFile: () => file
  81. };
  82. }));
  83. });
  84.  
  85. // 修改图片按钮的行为
  86. // 图片按钮
  87. let checkExist = setInterval(function () {
  88. const oldElement = document.querySelector('.toolbar-item.i-icon.i-icon-pic[title="图片"]');
  89. if (oldElement) {
  90. clearInterval(checkExist);
  91. const newElement = oldElement.cloneNode(true);
  92. oldElement.parentNode.replaceChild(newElement, oldElement);
  93. newElement.addEventListener('click', handleImgBtnClick);
  94. }
  95. }, 200);
  96.  
  97. // 监听 Ctrl+Enter 快捷键
  98. if (submitByKey)
  99. document.addEventListener('keydown', function (event) {
  100. if (event.ctrlKey && event.key === 'Enter') {
  101. // 获取按钮元素
  102. const button = document.querySelector('.submit.btn');
  103. // 触发点击事件
  104. button.click();
  105.  
  106. }
  107. });
  108.  
  109. }
  110.  
  111. // 粘贴事件处理
  112. function handlePasteEvt(event) {
  113. log('正在处理粘贴内容...');
  114. const items = (event.clipboardData || event.originalEvent.clipboardData).items;
  115. if (items.length === 0) {
  116. log('你粘贴的内容好像没有图片哦', 'red');
  117. return;
  118. }
  119. uploadImage(items)
  120. }
  121.  
  122. // 图片按钮点击事件处理
  123. function handleImgBtnClick() {
  124. // 创建一个隐藏的文件输入元素
  125. const input = document.createElement('input');
  126. input.type = 'file';
  127. input.multiple = true; // 允许多选文件
  128. input.accept = 'image/*'; // 仅接受图片文件
  129.  
  130. // 当文件被选择后的处理
  131. input.onchange = e => {
  132. const files = e.target.files; // 获取用户选择的文件列表
  133. if (files.length) {
  134. const items = [...files].map(file => ({
  135. kind: 'file',
  136. type: file.type,
  137. getAsFile: () => file
  138. }));
  139.  
  140. uploadImage(items);
  141. }
  142. };
  143.  
  144. // 触发文件输入框的点击事件,打开文件选择窗口
  145. input.click();
  146. }
  147.  
  148. // 处理并上传图片
  149. async function uploadImage(items) {
  150. let imageFiles = [];
  151.  
  152. for (let item of items) {
  153. if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
  154. let blob = item.getAsFile();
  155. imageFiles.push(blob);
  156. }
  157. }
  158.  
  159. if (imageFiles.length > 0) {
  160. event.preventDefault();
  161. for (let i = 0; i < imageFiles.length; i++) {
  162. if (imageFiles.length > 1)
  163. log(`上传第 ${i + 1} / ${imageFiles.length} 张图片...`);
  164. else
  165. log(`上传图片...`);
  166. let file = imageFiles[i];
  167. let formData = new FormData();
  168. formData.append('file', file);
  169. if (imgHost.type === 'LskyPro') {
  170. if (imgHost.storageId) formData.append('strategy_id', imgHost.storageId);
  171. await uploadToLsky(formData);
  172. } else if (imgHost.type === 'Telegraph') {
  173. await uploadToTelegraph(formData);
  174. } else if (imgHost.type === 'Telegraph2') {
  175. await uploadToTelegraph2(formData);
  176. } else if (imgHost.type === 'Chevereto') {
  177. await uploadToChevereto(file);
  178. } else if (imgHost.type === 'EasyImages') {
  179. await uploadToEasyImages(file);
  180. } else {
  181. log(`暂不支持的图床类型: ${imgHost.type}, 取消上传`, 'red');
  182. return;
  183. }
  184. }
  185.  
  186. } else {
  187. log('你粘贴的内容好像没有图片哦', 'red');
  188. }
  189. }
  190.  
  191. async function uploadToLsky(formData) {
  192. return new Promise((resolve, reject) => {
  193. let headers = {
  194. 'Accept': 'application/json'
  195. };
  196. if (imgHost.token)
  197. headers['Authorization'] = `Bearer ${imgHost.token}`;
  198.  
  199. GM_xmlhttpRequest({
  200. method: 'POST',
  201. url: `${imgHost.url}/api/v1/upload`,
  202. headers: headers,
  203. data: formData,
  204. onload: (rsp) => {
  205. let rspJson = JSON.parse(rsp.responseText);
  206. if (rsp.status !== 200) {
  207. log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  208. reject(rspJson.message);
  209. }
  210. if (rspJson.status === true) {
  211. // 图片上传成功
  212. if (rspJson?.data?.links?.markdown)
  213. insertToEditor(mdImgName === 0 ? rspJson.data.links.markdown : `![${mdImgName}](${rspJson.data.links.url})`);
  214. else {
  215. log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  216. insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  217. }
  218. } else
  219. log(`图片上传失败: ${rspJson.message}`, 'red');
  220. resolve();
  221. },
  222. onerror: (error) => {
  223. log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  224. reject(error);
  225. }
  226. });
  227. });
  228. }
  229.  
  230. async function uploadToTelegraph(formData) {
  231. return new Promise((resolve, reject) => {
  232. GM_xmlhttpRequest({
  233. method: 'POST',
  234. url: `${imgHost.url}/upload`,
  235. data: formData,
  236. onload: (rsp) => {
  237. let rspJson = JSON.parse(rsp.responseText);
  238. rspJson = rspJson[0];
  239. if (rsp.status !== 200) {
  240. log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  241. reject(rspJson.message);
  242. }
  243. if (rspJson) {
  244. // 图片上传成功
  245. if (rspJson?.src)
  246. insertToEditor(`![${mdImgName}](${imgHost.url}${rspJson.src})`);
  247. else {
  248. log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  249. insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  250. }
  251. } else
  252. log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
  253. resolve();
  254. },
  255. onerror: (error) => {
  256. log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  257. reject(error);
  258. }
  259. });
  260. });
  261. }
  262.  
  263. async function uploadToTelegraph2(formData) {
  264. return new Promise((resolve, reject) => {
  265. GM_xmlhttpRequest({
  266. method: 'POST',
  267. url: `${imgHost.url}/upload`,
  268. data: formData,
  269. onload: (rsp) => {
  270. let rspJson = JSON.parse(rsp.responseText);
  271.  
  272. if (rsp.status !== 200 || !rspJson || !rspJson.data) {
  273. log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  274. reject(rspJson?.message || '图片上传失败,缺少 data 字段');
  275. }
  276.  
  277. const result = rspJson.data;
  278.  
  279. // 图片上传成功
  280. if (result) {
  281. insertToEditor(`![${mdImgName}](${result})`);
  282. log('图片上传成功');
  283. resolve(result);
  284. } else {
  285. log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  286. insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)}`);
  287. resolve();
  288. }
  289. },
  290. onerror: (error) => {
  291. log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  292. reject(error);
  293. }
  294. });
  295. });
  296. }
  297.  
  298. async function uploadToChevereto(file) {
  299. return new Promise((resolve, reject) => {
  300. let headers = {
  301. 'Accept': 'application/json'
  302. };
  303. if (!imgHost.token) {
  304. log('Chevereto 图床需配置 token', 'red');
  305. reject('Chevereto 图床需要 token, 请填写 token 后重试');
  306. return;
  307. }
  308. headers['X-API-Key'] = imgHost.token;
  309. let formData = new FormData();
  310. formData.append('source', file);
  311.  
  312. GM_xmlhttpRequest({
  313. method: 'POST',
  314. url: `${imgHost.url}/api/1/upload`,
  315. headers: headers,
  316. data: formData,
  317. onload: (rsp) => {
  318. let rspJson = JSON.parse(rsp.responseText);
  319. if (rsp.status !== 200) {
  320. log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  321. reject(rspJson?.success?.message || rspJson?.error?.message);
  322. }
  323. if (rspJson.status_code === 200) {
  324. // 图片上传成功
  325. let imgUrl = rspJson.image.url || rspJson.image.url_viewer || rspJson.image.url_short;
  326. if (imgUrl)
  327. insertToEditor(mdImgName === 0 ? `![${rspJson.image.filename}](${imgUrl})` : `![${mdImgName}](${imgUrl})`);
  328. else {
  329. log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  330. insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  331. }
  332. } else
  333. log(`图片上传失败: ${rspJson?.success?.message || rspJson?.error?.message}`, 'red');
  334. resolve();
  335. },
  336. onerror: (error) => {
  337. log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  338. reject(error);
  339. }
  340. });
  341. });
  342. }
  343.  
  344. async function uploadToEasyImages(file) {
  345. return new Promise((resolve, reject) => {
  346. let url = imgHost.url;
  347. let formData = new FormData();
  348. if (imgHost.token) {
  349. // 带token, 使用后端接口上传
  350. url += '/api/index.php'
  351. formData.append('token', imgHost.token);
  352. formData.append('image', file);
  353. } else {
  354. // 不带token, 使用前端接口上传
  355. url += '/app/upload.php'
  356. formData.append('file', file);
  357. // 十位时间戳作为sign
  358. formData.append('sign', Math.floor(Date.now() / 1000));
  359. }
  360.  
  361.  
  362. GM_xmlhttpRequest({
  363. method: 'POST',
  364. url: url,
  365. data: formData,
  366. onload: (rsp) => {
  367. let rspJson = JSON.parse(rsp.responseText);
  368. if (rsp.status !== 200) {
  369. log(`图片上传失败: ${rsp.status} ${rsp.statusText}`, 'red');
  370. reject(rspJson.result);
  371. }
  372. if (rspJson.code === 200) {
  373. // 图片上传成功
  374. if (rspJson?.url)
  375. insertToEditor(`![${(mdImgName === 0 ? rspJson.srcName : mdImgName)}](${rspJson.url})`);
  376. else {
  377. log('图片上传成功, 但接口返回有误, 原始返回已粘贴到编辑器', 'red');
  378. insertToEditor(`图片上传成功, 但接口返回有误: ${JSON.stringify(rspJson)})`);
  379. }
  380. } else
  381. log(`图片上传失败: ${JSON.stringify(rspJson)}`, 'red');
  382. resolve();
  383. },
  384. onerror: (error) => {
  385. log(`图片上传失败: ${error.status} ${error.statusText}`, 'red');
  386. reject(error);
  387. }
  388. });
  389. });
  390. }
  391.  
  392.  
  393. function insertToEditor(markdownLink) {
  394. const codeMirrorElement = document.querySelector('.CodeMirror');
  395. if (codeMirrorElement) {
  396. const codeMirrorInstance = codeMirrorElement.CodeMirror;
  397. if (codeMirrorInstance) {
  398. const cursor = codeMirrorInstance.getCursor();
  399. codeMirrorInstance.replaceRange(`\n${markdownLink} \n`, cursor);
  400. }
  401. }
  402. if (markdownLink.startsWith('!['))
  403. log('图片已插入到编辑器~', 'green');
  404. }
  405.  
  406. // 在编辑器打印日志
  407. function log(message, color = '') {
  408. if (!document.getElementById('editor-enhance-logs')) {
  409. initEditorLogDiv();
  410. }
  411. const logDiv = document.getElementById('editor-enhance-logs');
  412. logDiv.innerHTML = `<div${color ? ` style="color: ${color};` : ''}">&nbsp;&nbsp;&nbsp;${message}&nbsp;</div>`;
  413.  
  414. console.log(`[NodeSeek-Editor-Enhance] ${message}`);
  415. }
  416.  
  417. // 初始化显示日志的容器
  418. function initEditorLogDiv() {
  419. const logDiv = document.createElement('div');
  420. logDiv.id = 'editor-enhance-logs';
  421. logDiv.innerHTML = '';
  422. document.body.appendChild(logDiv);
  423.  
  424. const editorToolbarDiv = document.querySelector('.mde-toolbar');
  425. editorToolbarDiv.appendChild(logDiv);
  426. }
  427.  
  428. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址