// ==UserScript==// @name NodeSeek粘贴上传图床并插入Markdown(Lsky/CFBed二选一)// @namespace https://nodeseek.com/// @version 1.2.0// @description 在 nodeseek.com 论坛中,Ctrl+V 粘贴图片后自动上传图床并插入 Markdown 链接,同时复制直链到剪贴板// @author baba// @match https://www.nodeseek.com/*// @match https://nodeseek.com/*// @run-at document-start// @grant GM_xmlhttpRequest// @grant GM_setClipboard// @connect *// ==/UserScript==(function () { 'use strict'; // ========== 配置区(按你的实际情况改) ========== // 后端类型:'lsky'(兰空图床) 或 'cfbed'(Cloudflare ImgBed) const BACKEND = 'lsky'; // 'lsky' | 'cfbed' // —— Lsky(兰空图床)参数 ——(示例为你贴的 309999.xyz) const LSKY = { api: 'https://xxx/api/v1/upload', // /api/v1/upload token: 'Bearer 2|0d9Gmsthmxxxxx8C8MsecIGXSnwZ', // 必填:Bearer 前缀+空格 // 上传成功后优先取 markdown 链接,也会同时把直链复制到剪贴板 }; // —— CFBed 参数(如果你改用 cfbed.sanyue.de/自建站)—— const CFBED = { base: 'https://xxxxxi', // 不要尾斜杠 // 二选一:推荐 Token;或使用 authCode 查询参数 apiToken: 'imgbed_SkC1xxxxxbkPoIfaucfZ8RE', authCode: '', // e.g. 'abcdef' uploadOptions: { // 对应文档参数,可按需修改 uploadChannel: 'telegram', // 'telegram' | 'cfr2' | 's3' uploadFolder: '', uploadNameType: 'default', // 'default' | 'index' | 'origin' | 'short' autoRetry: 'true', returnFormat: 'full' // 'full' 让返回直接是完整URL } }; // 插入到编辑器的 Markdown 模板({url} 会被替换) const MD_TPL = ''; // 尝试把 URL 也插到当前输入框;设为 false 就只写入 CodeMirror const ALSO_INSERT_IN_ACTIVE_INPUT = true; // ========== 工具函数 ========== const toast = (txt, ms = 2000) => { try { const bar = document.createElement('div'); bar.textContent = txt; Object.assign(bar.style, { position: 'fixed', left: '12px', bottom: '12px', background: 'rgba(0,0,0,.8)', color: '#fff', padding: '8px 12px', borderRadius: '8px', zIndex: 999999, fontSize: '12px', maxWidth: '75vw' }); document.documentElement.appendChild(bar); setTimeout(() => bar.remove(), ms); } catch {} }; const copyText = async (t) => { try { if (typeof GM_setClipboard === 'function') GM_setClipboard(t, { type: 'text' }); else await navigator.clipboard.writeText(t); } catch {} }; const pickImageFromClipboard = (ev) => { const items = (ev.clipboardData || ev.originalEvent?.clipboardData)?.items || []; for (const it of items) { if (it.kind === 'file' && it.type && it.type.startsWith('image/')) { return it.getAsFile(); } } return null; }; // 某些来源(微信/企微等)可能只给了 text/html 里的 <img src="data:..."> const tryExtractDataURLImage = (ev) => { const html = (ev.clipboardData || ev.originalEvent?.clipboardData)?.getData('text/html') || ''; const m = html.match(/src=["'](data:image\/[a-zA-Z0-9+.-]+;base64,[^"']+)["']/); if (!m) return null; const dataUrl = m[1]; const [meta, b64] = dataUrl.split(','); const mime = meta.match(/data:(.*);base64/)[1] || 'image/png'; const bin = atob(b64); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); return new File([u8], `pasted_${Date.now()}.png`, { type: mime }); }; const insertToCodeMirror = (text) => { const cmEl = document.querySelector('.CodeMirror'); if (!cmEl) return false; const cm = cmEl.CodeMirror; if (!cm) return false; const cur = cm.getCursor(); cm.replaceRange(text + '\n', cur); return true; }; const insertToActiveInput = (text) => { const el = document.activeElement; if (!el) return false; const isInput = el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && /^(text|search|url|email|tel)$/i.test(el.type)); if (isInput) { const start = el.selectionStart ?? el.value.length; const end = el.selectionEnd ?? el.value.length; const v = el.value; el.value = v.slice(0, start) + text + v.slice(end); const newPos = start + text.length; el.setSelectionRange(newPos, newPos); el.dispatchEvent(new Event('input', { bubbles: true })); return true; } if (el.isContentEditable) { const sel = getSelection(); if (!sel || sel.rangeCount === 0) return false; const range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(text)); range.collapse(false); return true; } return false; }; // ========== 上传实现 ========== const uploadToLsky = (file) => new Promise((resolve, reject) => { const form = new FormData(); form.append('file', file); GM_xmlhttpRequest({ method: 'POST', url: LSKY.api, headers: { 'Authorization': LSKY.token, 'Accept': 'application/json' }, data: form, onload: (res) => { try { const json = JSON.parse(res.responseText || '{}'); if (res.status === 200 && json?.data) { // 优先 markdown 链接;兜底使用 url/links.url const md = json?.data?.links?.markdown; const url = json?.data?.links?.url || json?.data?.url; resolve({ md, url }); } else { reject(new Error('上传成功但响应结构异常')); } } catch (e) { reject(e); } }, onerror: (e) => reject(new Error('网络/跨域失败')), }); }); const uploadToCFBed = (file) => new Promise((resolve, reject) => { // 兼容 CFBed:POST /upload?params... const params = new URLSearchParams({ ...CFBED.uploadOptions }); if (!CFBED.apiToken && CFBED.authCode) params.set('authCode', CFBED.authCode); const url = `${CFBED.base.replace(/\/+$/,'')}/upload?${params.toString()}`; GM_xmlhttpRequest({ method: 'POST', url, headers: CFBED.apiToken ? { 'Authorization': `Bearer ${CFBED.apiToken}` } : {}, data: (function () { const f = new FormData(); f.append('file', file); return f; })(), onload: (res) => { try { const json = JSON.parse(res.responseText || '[]'); // 可能是 [{src:"..." }] 或 {data:[{src:"..."}]} let src = null; if (Array.isArray(json) && json[0]?.src) src = json[0].src; else if (json?.data && Array.isArray(json.data) && json.data[0]?.src) src = json.data[0].src; if (!src) return reject(new Error('未获取到图片地址')); // returnFormat=full 时已是完整URL;否则补域名 let full = src; if (/^\/[^/]/.test(src)) full = CFBED.base.replace(/\/+$/,'') + src; // 自构 Markdown resolve({ md: MD_TPL.replace('{url}', full), url: full }); } catch (e) { reject(e); } }, onerror: () => reject(new Error('网络/跨域失败')), }); }); const upload = (file) => BACKEND === 'lsky' ? uploadToLsky(file) : uploadToCFBed(file); // ========== 主流程:监听粘贴 ========== document.addEventListener('paste', async (event) => { try { // 先看是否有 file/image/* let file = pickImageFromClipboard(event); // 回退:尝试从 data URL 抠图 if (!file) file = tryExtractDataURLImage(event); if (!file) return; // 非图片粘贴,放行 event.preventDefault(); // 阻断默认把图片blob塞编辑器 toast('正在上传图片…'); const { md, url } = await upload(file); if (!md && !url) throw new Error('上传成功但未拿到链接'); const mdText = md || MD_TPL.replace('{url}', url); const insertedToCM = insertToCodeMirror(mdText); if (!insertedToCM && ALSO_INSERT_IN_ACTIVE_INPUT) { // 如果没有 CodeMirror,就尝试插到当前输入区 insertToActiveInput(mdText); } if (url) await copyText(url); // 同时把直链复制到剪贴板 toast('图片已上传并插入,URL已复制'); } catch (err) { console.error('[NodeSeek Paste Uploader]', err); toast('上传失败:' + (err?.message || '未知错误'), 3000); } }, true);})();gpt改的,能用,有需要的拿。有问题请大佬指点
评论 (0)