Image Uploader to Markdown to CloudFlare-ImgBed

Upload pasted images to CloudFlare-ImgBed and insert as markdown format. Support clipboard images and custom configuration. , CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed

目前為 2025-03-14 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Image Uploader to Markdown to CloudFlare-ImgBed
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Upload pasted images to CloudFlare-ImgBed and insert as markdown format. Support clipboard images and custom configuration. , CloudFlare-ImgBed : https://github.com/MarSeventh/CloudFlare-ImgBed
  6. // @author calg
  7. // @match *://*/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @license MIT
  14. // @icon https://raw.githubusercontent.com/MarSeventh/CloudFlare-ImgBed/refs/heads/main/logo.png
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // 默认配置信息
  21. const DEFAULT_CONFIG = {
  22. AUTH_CODE: 'AUTH_CODE', // 替换为你的认证码
  23. SERVER_URL: 'https://SERVER_URL', // 替换为实际的服务器地址
  24. UPLOAD_PARAMS: {
  25. serverCompress: true,
  26. uploadChannel: 'telegram', // 可选 telegram 和 cfr2
  27. autoRetry: true,
  28. uploadNameType: 'index', // 可选值为[default, index, origin, short]
  29. returnFormat: 'full',
  30. uploadFolder: 'apiupload' // 指定上传目录,用相对路径表示,例如上传到img/test目录需填img/test
  31. },
  32. NOTIFICATION_DURATION: 3000, // 通知显示时间(毫秒)
  33. MARKDOWN_TEMPLATE: '![{filename}]({url})', // Markdown 模板
  34. AUTO_COPY_URL: false, // 是否自动复制URL到剪贴板
  35. ALLOWED_HOSTS: ['*'], // 允许在哪些网站上运行,* 表示所有网站
  36. MAX_FILE_SIZE: 5 * 1024 * 1024 // 最大文件大小(5MB)
  37. };
  38.  
  39. // 获取用户配置并确保所有必需的字段都存在
  40. const userConfig = GM_getValue('userConfig', {});
  41. let CONFIG = {};
  42. // 深度合并配置
  43. function mergeConfig(target, source) {
  44. for (const key in source) {
  45. if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
  46. target[key] = target[key] || {};
  47. mergeConfig(target[key], source[key]);
  48. } else {
  49. target[key] = source[key];
  50. }
  51. }
  52. return target;
  53. }
  54. // 确保所有默认配置项都存在
  55. CONFIG = mergeConfig({...DEFAULT_CONFIG}, userConfig);
  56. // 验证配置的完整性
  57. function validateConfig() {
  58. if (!Array.isArray(CONFIG.ALLOWED_HOSTS)) {
  59. CONFIG.ALLOWED_HOSTS = DEFAULT_CONFIG.ALLOWED_HOSTS;
  60. }
  61. if (typeof CONFIG.NOTIFICATION_DURATION !== 'number') {
  62. CONFIG.NOTIFICATION_DURATION = DEFAULT_CONFIG.NOTIFICATION_DURATION;
  63. }
  64. if (typeof CONFIG.MAX_FILE_SIZE !== 'number') {
  65. CONFIG.MAX_FILE_SIZE = DEFAULT_CONFIG.MAX_FILE_SIZE;
  66. }
  67. if (typeof CONFIG.MARKDOWN_TEMPLATE !== 'string') {
  68. CONFIG.MARKDOWN_TEMPLATE = DEFAULT_CONFIG.MARKDOWN_TEMPLATE;
  69. }
  70. if (typeof CONFIG.AUTO_COPY_URL !== 'boolean') {
  71. CONFIG.AUTO_COPY_URL = DEFAULT_CONFIG.AUTO_COPY_URL;
  72. }
  73. }
  74. validateConfig();
  75.  
  76. // 添加通知样式
  77. GM_addStyle(`
  78. .img-upload-notification {
  79. position: fixed;
  80. top: 20px;
  81. right: 20px;
  82. padding: 15px 20px;
  83. border-radius: 5px;
  84. z-index: 9999;
  85. max-width: 300px;
  86. font-size: 14px;
  87. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  88. transition: all 0.3s ease;
  89. opacity: 0;
  90. transform: translateX(20px);
  91. }
  92. .img-upload-notification.show {
  93. opacity: 1;
  94. transform: translateX(0);
  95. }
  96. .img-upload-success {
  97. background-color: #4caf50;
  98. color: white;
  99. }
  100. .img-upload-error {
  101. background-color: #f44336;
  102. color: white;
  103. }
  104. .img-upload-info {
  105. background-color: #2196F3;
  106. color: white;
  107. }
  108. .img-upload-close {
  109. float: right;
  110. margin-left: 10px;
  111. cursor: pointer;
  112. opacity: 0.8;
  113. }
  114. .img-upload-close:hover {
  115. opacity: 1;
  116. }
  117.  
  118. .img-upload-modal {
  119. position: fixed;
  120. top: 50%;
  121. left: 50%;
  122. transform: translate(-50%, -50%);
  123. background: white;
  124. padding: 20px;
  125. border-radius: 8px;
  126. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  127. z-index: 10000;
  128. max-width: 600px;
  129. width: 90%;
  130. max-height: 80vh;
  131. overflow-y: auto;
  132. }
  133. .img-upload-modal-overlay {
  134. position: fixed;
  135. top: 0;
  136. left: 0;
  137. right: 0;
  138. bottom: 0;
  139. background: rgba(0, 0, 0, 0.5);
  140. z-index: 9999;
  141. }
  142. .img-upload-modal h2 {
  143. margin: 0 0 20px;
  144. color: #333;
  145. font-size: 18px;
  146. }
  147. .img-upload-form-group {
  148. margin-bottom: 20px;
  149. }
  150. .img-upload-form-group label {
  151. display: block;
  152. margin-bottom: 8px;
  153. color: #333;
  154. font-weight: 500;
  155. }
  156. .img-upload-help-text {
  157. margin-top: 4px;
  158. color: #666;
  159. font-size: 12px;
  160. }
  161. .img-upload-form-group input[type="text"],
  162. .img-upload-form-group input[type="number"],
  163. .img-upload-form-group textarea {
  164. width: 100%;
  165. padding: 8px;
  166. border: 1px solid #ddd;
  167. border-radius: 4px;
  168. font-size: 14px;
  169. box-sizing: border-box;
  170. }
  171. .img-upload-form-group textarea {
  172. min-height: 100px;
  173. font-family: monospace;
  174. }
  175. .img-upload-form-group input[type="checkbox"] {
  176. margin-right: 8px;
  177. }
  178. .img-upload-buttons {
  179. display: flex;
  180. justify-content: flex-end;
  181. gap: 10px;
  182. margin-top: 20px;
  183. }
  184. .img-upload-button {
  185. padding: 8px 16px;
  186. border: none;
  187. border-radius: 4px;
  188. cursor: pointer;
  189. font-size: 14px;
  190. transition: background-color 0.2s;
  191. }
  192. .img-upload-button-primary {
  193. background: #2196F3;
  194. color: white;
  195. }
  196. .img-upload-button-secondary {
  197. background: #e0e0e0;
  198. color: #333;
  199. }
  200. .img-upload-button:hover {
  201. opacity: 0.9;
  202. }
  203. .img-upload-error {
  204. color: #ffffff;
  205. font-size: 12px;
  206. margin-top: 4px;
  207. }
  208. .img-upload-info-icon {
  209. display: inline-block;
  210. width: 16px;
  211. height: 16px;
  212. background: #2196F3;
  213. color: white;
  214. border-radius: 50%;
  215. text-align: center;
  216. line-height: 16px;
  217. font-size: 12px;
  218. margin-left: 4px;
  219. cursor: help;
  220. }
  221. .img-upload-form-group select {
  222. width: 100%;
  223. padding: 8px;
  224. border: 1px solid #ddd;
  225. border-radius: 4px;
  226. font-size: 14px;
  227. background-color: white;
  228. }
  229. .img-upload-input-group {
  230. display: flex;
  231. align-items: center;
  232. }
  233. .img-upload-input-group input {
  234. flex: 1;
  235. border-top-right-radius: 0;
  236. border-bottom-right-radius: 0;
  237. }
  238. .img-upload-input-group-text {
  239. padding: 8px 12px;
  240. background: #f5f5f5;
  241. border: 1px solid #ddd;
  242. border-left: none;
  243. border-radius: 0 4px 4px 0;
  244. color: #666;
  245. }
  246. .img-upload-checkbox-label {
  247. display: flex !important;
  248. align-items: center;
  249. font-weight: normal !important;
  250. }
  251. .img-upload-checkbox-label input {
  252. margin-right: 8px;
  253. }
  254. `);
  255.  
  256. // 显示通知的函数
  257. function showNotification(message, type = 'info') {
  258. const notification = document.createElement('div');
  259. notification.className = `img-upload-notification img-upload-${type}`;
  260. const closeBtn = document.createElement('span');
  261. closeBtn.className = 'img-upload-close';
  262. closeBtn.textContent = '✕';
  263. closeBtn.onclick = () => removeNotification(notification);
  264. const messageSpan = document.createElement('span');
  265. messageSpan.textContent = message;
  266. notification.appendChild(closeBtn);
  267. notification.appendChild(messageSpan);
  268. document.body.appendChild(notification);
  269.  
  270. // 添加显示动画
  271. setTimeout(() => notification.classList.add('show'), 10);
  272.  
  273. // 自动消失
  274. const timeout = setTimeout(() => removeNotification(notification), CONFIG.NOTIFICATION_DURATION);
  275. // 鼠标悬停时暂停消失
  276. notification.addEventListener('mouseenter', () => clearTimeout(timeout));
  277. notification.addEventListener('mouseleave', () => setTimeout(() => removeNotification(notification), 1000));
  278. }
  279.  
  280. // 移除通知
  281. function removeNotification(notification) {
  282. notification.classList.remove('show');
  283. setTimeout(() => {
  284. if (notification.parentNode) {
  285. notification.parentNode.removeChild(notification);
  286. }
  287. }, 300);
  288. }
  289.  
  290. // 复制文本到剪贴板
  291. function copyToClipboard(text) {
  292. const textarea = document.createElement('textarea');
  293. textarea.value = text;
  294. textarea.style.position = 'fixed';
  295. textarea.style.opacity = '0';
  296. document.body.appendChild(textarea);
  297. textarea.select();
  298. try {
  299. document.execCommand('copy');
  300. showNotification('链接已复制到剪贴板!', 'success');
  301. } catch (err) {
  302. showNotification('复制失败:' + err.message, 'error');
  303. }
  304. document.body.removeChild(textarea);
  305. }
  306.  
  307. // 检查文件大小
  308. function checkFileSize(file) {
  309. if (file.size > CONFIG.MAX_FILE_SIZE) {
  310. showNotification(`文件大小超过限制(${Math.round(CONFIG.MAX_FILE_SIZE/1024/1024)}MB)`, 'error');
  311. return false;
  312. }
  313. return true;
  314. }
  315.  
  316. // 检查当前网站是否允许上传
  317. function isAllowedHost() {
  318. const currentHost = window.location.hostname;
  319. return CONFIG.ALLOWED_HOSTS.includes('*') || CONFIG.ALLOWED_HOSTS.includes(currentHost);
  320. }
  321.  
  322. // 监听所有文本输入区域的粘贴事件
  323. function addPasteListener() {
  324. document.addEventListener('paste', async function(event) {
  325. if (!isAllowedHost()) return;
  326.  
  327. const activeElement = document.activeElement;
  328. if (!activeElement || !['INPUT', 'TEXTAREA'].includes(activeElement.tagName)) {
  329. return;
  330. }
  331.  
  332. const items = event.clipboardData.items;
  333. let hasImage = false;
  334. for (let item of items) {
  335. if (item.type.startsWith('image/')) {
  336. hasImage = true;
  337. event.preventDefault();
  338. const blob = item.getAsFile();
  339. if (!checkFileSize(blob)) {
  340. return;
  341. }
  342.  
  343. showNotification('正在上传图片,请稍候...', 'info');
  344. await uploadImage(blob, activeElement);
  345. break;
  346. }
  347. }
  348.  
  349. if (!hasImage) {
  350. return;
  351. }
  352. });
  353. }
  354.  
  355. // 上传图片
  356. async function uploadImage(blob, targetElement) {
  357. const formData = new FormData();
  358. const filename = `pasted-image-${Date.now()}.png`;
  359. formData.append('file', blob, filename);
  360.  
  361. const queryParams = new URLSearchParams({
  362. authCode: CONFIG.AUTH_CODE,
  363. ...CONFIG.UPLOAD_PARAMS
  364. }).toString();
  365.  
  366. try {
  367. GM_xmlhttpRequest({
  368. method: 'POST',
  369. url: `${CONFIG.SERVER_URL}/upload?${queryParams}`,
  370. data: formData,
  371. onload: function(response) {
  372. if (response.status === 200) {
  373. try {
  374. const result = JSON.parse(response.responseText);
  375. if (result && result.length > 0) {
  376. const imageUrl = result[0].src;
  377. insertMarkdownImage(imageUrl, targetElement, filename);
  378. showNotification('图片上传成功!', 'success');
  379. if (CONFIG.AUTO_COPY_URL) {
  380. copyToClipboard(imageUrl);
  381. }
  382. } else {
  383. showNotification('上传成功但未获取到图片链接,请检查服务器响应', 'error');
  384. }
  385. } catch (e) {
  386. showNotification('解析服务器响应失败:' + e.message, 'error');
  387. }
  388. } else {
  389. let errorMsg = '上传失败';
  390. try {
  391. const errorResponse = JSON.parse(response.responseText);
  392. errorMsg += ':' + (errorResponse.message || response.statusText);
  393. } catch (e) {
  394. errorMsg += `(状态码:${response.status})`;
  395. }
  396. showNotification(errorMsg, 'error');
  397. }
  398. },
  399. onerror: function(error) {
  400. showNotification('网络错误:无法连接到图床服务器', 'error');
  401. }
  402. });
  403. } catch (error) {
  404. showNotification('上传过程发生错误:' + error.message, 'error');
  405. }
  406. }
  407.  
  408. // 在输入框中插入 Markdown 格式的图片链接
  409. function insertMarkdownImage(imageUrl, element, filename) {
  410. const markdownImage = CONFIG.MARKDOWN_TEMPLATE
  411. .replace('{url}', imageUrl)
  412. .replace('{filename}', filename.replace(/\.[^/.]+$/, '')); // 移除文件扩展名
  413. const start = element.selectionStart;
  414. const end = element.selectionEnd;
  415. const text = element.value;
  416.  
  417. element.value = text.substring(0, start) + markdownImage + text.substring(end);
  418. element.selectionStart = element.selectionEnd = start + markdownImage.length;
  419. element.focus();
  420. }
  421.  
  422. // 创建配置界面
  423. function createConfigModal() {
  424. const overlay = document.createElement('div');
  425. overlay.className = 'img-upload-modal-overlay';
  426. const modal = document.createElement('div');
  427. modal.className = 'img-upload-modal';
  428. const content = `
  429. <h2>图床上传配置</h2>
  430. <form id="img-upload-config-form">
  431. <div class="img-upload-form-group">
  432. <label>认证码</label>
  433. <input type="text" name="AUTH_CODE" value="${CONFIG.AUTH_CODE}" required>
  434. <div class="img-upload-help-text">用于验证上传请求的密钥</div>
  435. </div>
  436. <div class="img-upload-form-group">
  437. <label>服务器地址</label>
  438. <input type="text" name="SERVER_URL" value="${CONFIG.SERVER_URL}" required>
  439. <div class="img-upload-help-text">图床服务器的URL地址</div>
  440. </div>
  441. <div class="img-upload-form-group">
  442. <label>上传通道</label>
  443. <select name="uploadChannel">
  444. <option value="cfr2" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'cfr2' ? 'selected' : ''}>CloudFlare R2</option>
  445. <option value="telegram" ${CONFIG.UPLOAD_PARAMS.uploadChannel === 'telegram' ? 'selected' : ''}>Telegram</option>
  446. </select>
  447. <div class="img-upload-help-text">选择图片上传的存储通道</div>
  448. </div>
  449. <div class="img-upload-form-group">
  450. <label>文件命名方式</label>
  451. <select name="uploadNameType">
  452. <option value="default" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'default' ? 'selected' : ''}>默认(前缀_原名)</option>
  453. <option value="index" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'index' ? 'selected' : ''}>仅前缀</option>
  454. <option value="origin" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'origin' ? 'selected' : ''}>仅原名</option>
  455. <option value="short" ${CONFIG.UPLOAD_PARAMS.uploadNameType === 'short' ? 'selected' : ''}>短链接</option>
  456. </select>
  457. <div class="img-upload-help-text">选择上传后的文件命名方式</div>
  458. </div>
  459. <div class="img-upload-form-group">
  460. <label>上传目录</label>
  461. <input type="text" name="uploadFolder" value="${CONFIG.UPLOAD_PARAMS.uploadFolder}">
  462. <div class="img-upload-help-text">指定上传目录,使用相对路径,例如:img/test</div>
  463. </div>
  464. <div class="img-upload-form-group">
  465. <label>通知显示时间</label>
  466. <input type="number" name="NOTIFICATION_DURATION" value="${CONFIG.NOTIFICATION_DURATION}" min="1000" step="500">
  467. <div class="img-upload-help-text">通知消息显示的时间(毫秒)</div>
  468. </div>
  469. <div class="img-upload-form-group">
  470. <label>Markdown模板</label>
  471. <input type="text" name="MARKDOWN_TEMPLATE" value="${CONFIG.MARKDOWN_TEMPLATE}">
  472. <div class="img-upload-help-text">支持 {filename} {url} 两个变量</div>
  473. </div>
  474. <div class="img-upload-form-group">
  475. <label>允许的网站</label>
  476. <input type="text" name="ALLOWED_HOSTS" value="${CONFIG.ALLOWED_HOSTS.join(',')}">
  477. <div class="img-upload-help-text">输入域名,用逗号分隔。使用 * 表示允许所有网站</div>
  478. </div>
  479. <div class="img-upload-form-group">
  480. <label>最大文件大小</label>
  481. <div class="img-upload-input-group">
  482. <input type="number" name="MAX_FILE_SIZE" value="${CONFIG.MAX_FILE_SIZE / 1024 / 1024}" min="1" step="1">
  483. <span class="img-upload-input-group-text">MB</span>
  484. </div>
  485. </div>
  486. <div class="img-upload-form-group">
  487. <label class="img-upload-checkbox-label">
  488. <input type="checkbox" name="AUTO_COPY_URL" ${CONFIG.AUTO_COPY_URL ? 'checked' : ''}>
  489. 自动复制URL到剪贴板
  490. </label>
  491. </div>
  492. <div class="img-upload-buttons">
  493. <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-cancel">取消</button>
  494. <button type="button" class="img-upload-button img-upload-button-secondary" id="img-upload-reset">重置默认值</button>
  495. <button type="submit" class="img-upload-button img-upload-button-primary">保存</button>
  496. </div>
  497. </form>
  498. `;
  499. modal.innerHTML = content;
  500. document.body.appendChild(overlay);
  501. document.body.appendChild(modal);
  502.  
  503. // 事件处理
  504. const form = modal.querySelector('#img-upload-config-form');
  505. const cancelBtn = modal.querySelector('#img-upload-cancel');
  506. const resetBtn = modal.querySelector('#img-upload-reset');
  507.  
  508. function closeModal() {
  509. document.body.removeChild(overlay);
  510. document.body.removeChild(modal);
  511. }
  512.  
  513. overlay.addEventListener('click', closeModal);
  514. cancelBtn.addEventListener('click', closeModal);
  515. resetBtn.addEventListener('click', () => {
  516. if (confirm('确定要重置所有配置到默认值吗?')) {
  517. CONFIG = {...DEFAULT_CONFIG};
  518. GM_setValue('userConfig', {});
  519. showNotification('配置已重置为默认值!', 'success');
  520. closeModal();
  521. }
  522. });
  523.  
  524. form.addEventListener('submit', (e) => {
  525. e.preventDefault();
  526. try {
  527. const formData = new FormData(form);
  528. const newConfig = {
  529. AUTH_CODE: formData.get('AUTH_CODE'),
  530. SERVER_URL: formData.get('SERVER_URL'),
  531. UPLOAD_PARAMS: {
  532. ...DEFAULT_CONFIG.UPLOAD_PARAMS,
  533. uploadChannel: formData.get('uploadChannel'),
  534. uploadNameType: formData.get('uploadNameType'),
  535. uploadFolder: formData.get('uploadFolder')
  536. },
  537. NOTIFICATION_DURATION: parseInt(formData.get('NOTIFICATION_DURATION')),
  538. MARKDOWN_TEMPLATE: formData.get('MARKDOWN_TEMPLATE'),
  539. ALLOWED_HOSTS: formData.get('ALLOWED_HOSTS').split(',').map(h => h.trim()),
  540. MAX_FILE_SIZE: parseFloat(formData.get('MAX_FILE_SIZE')) * 1024 * 1024,
  541. AUTO_COPY_URL: formData.get('AUTO_COPY_URL') === 'on'
  542. };
  543.  
  544. CONFIG = mergeConfig({...DEFAULT_CONFIG}, newConfig);
  545. GM_setValue('userConfig', CONFIG);
  546. showNotification('配置已更新!', 'success');
  547. closeModal();
  548. } catch (error) {
  549. showNotification('配置格式错误:' + error.message, 'error');
  550. }
  551. });
  552.  
  553. // 防止点击模态框时关闭
  554. modal.addEventListener('click', (e) => e.stopPropagation());
  555. }
  556.  
  557. // 修改注册配置菜单函数
  558. function registerMenuCommands() {
  559. GM_registerMenuCommand('配置图床参数', createConfigModal);
  560. }
  561.  
  562. // 初始化
  563. function init() {
  564. if (!isAllowedHost()) return;
  565. addPasteListener();
  566. registerMenuCommands();
  567. }
  568.  
  569. init();
  570. })();