NodeSeek 编辑器增强个人修改版

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

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

QingJ © 2025

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