DeepFloodbeta

新站怎么开脚本?

  • 问问ai

  • 不开呗😄

  • 加个match试试

  • image

    // ==UserScript==
    // @name         NodeImage图片上传助手 (双站版)
    // @namespace    https://www.nodeimage.com/
    // @version      1.0.3
    // @description  在NodeSeek和DeepFlood编辑器中粘贴图片自动上传到NodeImage图床
    // @author       shuai
    // @match        *://www.nodeseek.com/*
    // @match        *://nodeseek.com/*
    // @match        *://www.deepflood.com/*
    // @match        *://deepflood.com/*
    // @match        *://nodeimage.com/*
    // @match        *://*.nodeimage.com/*
    // @icon         https://cdn.nodeimage.com/favicon.ico
    // @grant        GM_xmlhttpRequest
    // @grant        GM_addStyle
    // @grant        GM_setValue
    // @grant        GM_getValue
    // @grant        GM_deleteValue
    // @connect      nodeimage.com
    // @connect      api.nodeimage.com
    // @license      MIT
    // ==/UserScript==
    
    /**
     * @file NodeImage Uploader - Tampermonkey Script (双站支持版)
     * @author shuai
     * @version 1.0.3
     *
     * This script integrates the NodeImage image hosting service with the NodeSeek
     * and DeepFlood websites' editors. It allows users to upload images by pasting, 
     * dragging and dropping, or using a dedicated toolbar button.
     */
    
    (() => {
      'use strict';
    
      // ===== 全局配置 (Global Configuration) =====
      const APP = {
        api: {
          key: GM_getValue('nodeimage_apiKey', ''),
          setKey: key => {
            GM_setValue('nodeimage_apiKey', key);
            APP.api.key = key;
            UI.updateState();
          },
          clearKey: () => {
            GM_deleteValue('nodeimage_apiKey');
            APP.api.key = '';
            UI.updateState();
          },
          endpoints: {
            upload: 'https://api.nodeimage.com/api/upload',
            apiKey: 'https://api.nodeimage.com/api/user/api-key'
          }
        },
        site: {
          url: 'https://www.nodeimage.com'
        },
        storage: {
          keys: {
            loginCheck: 'nodeimage_login_check',
            loginStatus: 'nodeimage_login_status',
            logout: 'nodeimage_logout'
          },
          get: key => localStorage.getItem(APP.storage.keys[key]),
          set: (key, value) => localStorage.setItem(APP.storage.keys[key], value),
          remove: key => localStorage.removeItem(APP.storage.keys[key])
        },
        retry: {
          max: 2,
          delay: 1000
        },
        statusTimeout: 2000,
        auth: {
          recentLoginGracePeriod: 30000,
          loginCheckInterval: 3000,
          loginCheckTimeout: 300000
        },
        // 新增:支持的站点配置
        supportedSites: {
          'nodeseek.com': {
            editor: '.CodeMirror',
            toolbar: '.mde-toolbar',
            imgBtn: '.toolbar-item.i-icon.i-icon-pic[title="图片"]'
          },
          'deepflood.com': {
            editor: '.CodeMirror',
            toolbar: '.mde-toolbar',
            imgBtn: '.toolbar-item.i-icon.i-icon-pic[title="图片"]'
          }
        }
      };
    
      // ===== 动态选择器 (Dynamic Selectors) =====
      const SELECTORS = {
        get currentSite() {
          const hostname = window.location.hostname.replace('www.', '');
          return APP.supportedSites[hostname] || APP.supportedSites['nodeseek.com'];
        },
        get editor() { return this.currentSite.editor; },
        get toolbar() { return this.currentSite.toolbar; },
        get imgBtn() { return this.currentSite.imgBtn; },
        container: '#nodeimage-toolbar-container'
      };
    
      // ===== 状态常量 (Status Constants) =====
      const STATUS = {
        SUCCESS: { class: 'success', color: '#42d392' },
        ERROR: { class: 'error', color: '#f56c6c' },
        WARNING: { class: 'warning', color: '#e6a23c' },
        INFO: { class: 'info', color: '#0078ff' }
      };
    
      const MESSAGE = {
        READY: 'NodeImage已就绪',
        UPLOADING: '正在上传...',
        UPLOAD_SUCCESS: '上传成功!',
        LOGIN_EXPIRED: '登录已失效',
        LOGOUT: '已退出登录',
        RETRY: (current, max) => `重试上传 (${current}/${max})`
      };
    
      // ===== DOM缓存 (DOM Cache) =====
      const DOM = {
        editor: null,
        statusElements: new Set(),
        loginButtons: new Set(),
        getEditor: () => DOM.editor?.CodeMirror
      };
    
      // ===== 全局样式 (Global Styles) =====
      GM_addStyle(`
        #nodeimage-status {
          margin-left: 10px;
          display: inline-block;
          font-size: 14px;
          height: 28px;
          line-height: 28px;
          transition: all 0.3s ease;
        }
        #nodeimage-status.success { color: ${STATUS.SUCCESS.color}; }
        #nodeimage-status.error { color: ${STATUS.ERROR.color}; }
        #nodeimage-status.warning { color: ${STATUS.WARNING.color}; }
        #nodeimage-status.info { color: ${STATUS.INFO.color}; }
    
        .nodeimage-login-btn {
          cursor: pointer;
          margin-left: 10px;
          color: ${STATUS.WARNING.color};
          font-size: 14px;
          background: rgba(230, 162, 60, 0.1);
          padding: 3px 8px;
          border-radius: 4px;
          border: 1px solid rgba(230, 162, 60, 0.2);
        }
    
        .nodeimage-toolbar-container {
          display: flex;
          align-items: center;
          margin-left: 10px;
        }
      `);
    
      // ===== 工具函数 (Utility Functions) =====
      const Utils = {
        isNodeImageSite: () => /^(.*\.)?nodeimage\.com$/.test(window.location.hostname),
        
        // 新增:判断当前站点是否为支持的社区站点
        isCommunitySite: () => {
          const hostname = window.location.hostname.replace('www.', '');
          return Object.keys(APP.supportedSites).includes(hostname);
        },
    
        waitForElement: selector => new Promise(res => {
          const el = document.querySelector(selector);
          if (el) return res(el);
          new MutationObserver((_, o) => {
            const found = document.querySelector(selector);
            if (found) { o.disconnect(); res(found); }
          }).observe(document.body, { childList: true, subtree: true });
        }),
    
        isEditingInEditor: () => {
          const a = document.activeElement;
          return a && (a.classList.contains('CodeMirror') || a.closest('.CodeMirror') || a.tagName === 'TEXTAREA');
        },
    
        createFileInput: cb => {
          const i = Object.assign(document.createElement('input'), { type: 'file', multiple: true, accept: 'image/*' });
          i.onchange = e => cb([...e.target.files]);
          i.click();
        },
    
        delay: ms => new Promise(r => setTimeout(r, ms))
      };
    
      // ===== API通信 (API Communication) =====
      const API = {
        request: ({ url, method = 'GET', data = null, headers = {}, withAuth = false }) => {
          return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
              method,
              url,
              headers: {
                'Accept': 'application/json',
                ...(withAuth && APP.api.key ? { 'X-API-Key': APP.api.key } : {}),
                ...headers
              },
              data,
              withCredentials: true,
              responseType: 'json',
              onload: response => {
                if (response.status === 200 && response.response) {
                  resolve(response.response);
                } else {
                  reject(response);
                }
              },
              onerror: reject
            });
          });
        },
    
        checkLoginAndGetKey: async () => {
          try {
            const response = await API.request({ url: APP.api.endpoints.apiKey });
    
            if (response.api_key) {
              APP.api.setKey(response.api_key);
              return true;
            }
    
            if (response.error) {
              APP.api.clearKey();
            }
    
            return false;
          } catch (error) {
            APP.api.clearKey();
            return false;
          }
        },
    
        uploadImage: async (file, retries = 0) => {
          try {
            const formData = new FormData();
            formData.append('image', file);
    
            const result = await API.request({
              url: APP.api.endpoints.upload,
              method: 'POST',
              data: formData,
              withAuth: true
            });
    
            if (result.success) {
              return {
                url: result.links.direct,
                markdown: result.links.markdown
              };
            } else {
              const errorMsg = result.error || '未知错误';
    
              if (errorMsg.toLowerCase().match(/unauthorized|invalid api key|未授权|无效的api密钥/)) {
                APP.api.clearKey();
                throw new Error(MESSAGE.LOGIN_EXPIRED);
              }
    
              throw new Error(errorMsg);
            }
          } catch (error) {
            if (error.status === 401 || error.status === 403) {
              APP.api.clearKey();
              throw new Error(MESSAGE.LOGIN_EXPIRED);
            }
    
            if (retries < APP.retry.max) {
              setStatus(STATUS.WARNING.class, MESSAGE.RETRY(retries + 1, APP.retry.max));
    
              await Utils.delay(APP.retry.delay);
              return API.uploadImage(file, retries + 1);
            }
    
            throw error instanceof Error ? error : new Error(String(error));
          }
        }
      };
    
      // ===== UI与状态管理 (UI & Status Management) =====
      const setStatus = (cls, msg, ttl = 0) => {
        DOM.statusElements.forEach(el => { el.className = cls; el.textContent = msg; });
        if (ttl) return Utils.delay(ttl).then(UI.updateState);
      };
    
      const UI = {
        updateState: () => {
          const isLoggedIn = Boolean(APP.api.key);
    
          DOM.loginButtons.forEach(btn => {
            btn.style.display = isLoggedIn ? 'none' : 'inline-block';
          });
    
          DOM.statusElements.forEach(el => {
            if (isLoggedIn) {
              el.className = STATUS.SUCCESS.class;
              el.textContent = MESSAGE.READY;
            } else {
              el.textContent = '';
            }
          });
        },
    
        openLogin: () => {
          APP.storage.set('loginStatus', 'login_pending');
          window.open(APP.site.url, '_blank');
        },
    
        setupToolbar: toolbar => {
          if (!toolbar || toolbar.querySelector(SELECTORS.container)) return;
    
          const container = document.createElement('div');
          container.id = 'nodeimage-toolbar-container';
          container.className = 'nodeimage-toolbar-container';
          toolbar.appendChild(container);
    
          const imgBtn = toolbar.querySelector(SELECTORS.imgBtn);
          if (imgBtn) {
            const newBtn = imgBtn.cloneNode(true);
            imgBtn.parentNode.replaceChild(newBtn, imgBtn);
            newBtn.addEventListener('click', async () => {
              if (!APP.api.key || !(await Auth.checkLoginIfNeeded())) {
                UI.openLogin();
                return;
              }
    
              Utils.createFileInput(ImageHandler.handleFiles);
            });
          }
    
          const statusEl = document.createElement('div');
          statusEl.id = 'nodeimage-status';
          statusEl.className = STATUS.INFO.class;
          container.appendChild(statusEl);
          DOM.statusElements.add(statusEl);
    
          const loginBtn = document.createElement('div');
          loginBtn.className = 'nodeimage-login-btn';
          loginBtn.textContent = '点击登录NodeImage';
          loginBtn.addEventListener('click', UI.openLogin);
          loginBtn.style.display = 'none';
          container.appendChild(loginBtn);
          DOM.loginButtons.add(loginBtn);
    
          UI.updateState();
        }
      };
    
      // ===== 图片处理 (Image Handling) =====
      const ImageHandler = {
        handlePaste: e => {
          if (!Utils.isEditingInEditor()) return;
    
          const dt = e.clipboardData || e.originalEvent?.clipboardData;
          if (!dt) return;
    
          let files = [];
    
          if (dt.files && dt.files.length) {
            files = Array.from(dt.files).filter(f => f.type.startsWith('image/'));
          } else if (dt.items && dt.items.length) {
            files = Array.from(dt.items)
              .filter(i => i.kind === 'file' && i.type.startsWith('image/'))
              .map(i => i.getAsFile())
              .filter(Boolean);
          }
    
          if (files.length) {
            e.preventDefault();
            e.stopPropagation();
    
            if (!APP.api.key) {
              UI.openLogin();
              return;
            }
    
            ImageHandler.handleFiles(files);
          }
        },
    
        handleFiles: files => {
          if (!APP.api.key) {
            UI.openLogin();
            return;
          }
    
          files.filter(file => file?.type.startsWith('image/'))
            .forEach(ImageHandler.uploadAndInsert);
        },
    
        uploadAndInsert: async file => {
          setStatus(STATUS.INFO.class, MESSAGE.UPLOADING);
    
          try {
            const result = await API.uploadImage(file);
            ImageHandler.insertMarkdown(result.markdown);
    
            await setStatus(STATUS.SUCCESS.class, MESSAGE.UPLOAD_SUCCESS, APP.statusTimeout);
          } catch (error) {
            if (error.message === MESSAGE.LOGIN_EXPIRED) {
              await Auth.checkLoginIfNeeded(true);
            }
    
            const errorMessage = `上传失败: ${error.message}`;
            console.error('[NodeImage]', error);
    
            await setStatus(STATUS.ERROR.class, errorMessage, APP.statusTimeout);
          }
        },
    
        insertMarkdown: markdown => {
          const cm = DOM.getEditor();
          if (cm) {
            const cursor = cm.getCursor();
            cm.replaceRange(`\n${markdown}\n`, cursor);
          }
        }
      };
    
      // ===== 认证管理 (Authentication Management) =====
      const Auth = {
        checkLoginIfNeeded: async (forceCheck = false) => {
          if (APP.api.key && !forceCheck) {
            return true;
          }
    
          const isLoggedIn = await API.checkLoginAndGetKey();
    
          if (!isLoggedIn && APP.api.key) {
            setStatus(STATUS.WARNING.class, MESSAGE.LOGIN_EXPIRED);
          }
    
          UI.updateState();
    
          return isLoggedIn;
        },
    
        checkLogoutFlag: () => {
          if (APP.storage.get('logout') === 'true') {
            APP.api.clearKey();
            APP.storage.remove('logout');
            setStatus(STATUS.WARNING.class, MESSAGE.LOGOUT);
          }
        },
    
        checkRecentLogin: async () => {
          const lastLoginCheck = APP.storage.get('loginCheck');
          if (lastLoginCheck && (Date.now() - parseInt(lastLoginCheck) < APP.auth.recentLoginGracePeriod)) {
            await API.checkLoginAndGetKey();
            APP.storage.remove('loginCheck');
          }
        },
    
        setupStorageListener: () => {
          window.addEventListener('storage', event => {
            const { loginStatus, logout } = APP.storage.keys;
    
            if (event.key === loginStatus && event.newValue === 'login_success') {
              API.checkLoginAndGetKey();
              localStorage.removeItem(loginStatus);
            } else if (event.key === logout && event.newValue === 'true') {
              APP.api.clearKey();
              localStorage.removeItem(logout);
            }
          });
        },
    
        monitorLogout: () => {
          document.addEventListener('click', e => {
            const logoutButton = e.target.closest('#logoutBtn, .logout-btn');
            if (logoutButton || e.target.textContent?.match(/登出|注销|退出|logout|sign out/i)) {
              APP.storage.set('logout', 'true');
            }
          });
        },
    
        startLoginStatusCheck: () => {
          const checkLoginInterval = setInterval(async () => {
            try {
              const isLoggedIn = await API.checkLoginAndGetKey();
    
              if (isLoggedIn) {
                clearInterval(checkLoginInterval);
    
                APP.storage.remove('loginStatus');
                APP.storage.set('loginStatus', 'login_success');
                APP.storage.set('loginCheck', Date.now().toString());
              }
            } catch (error) {}
          }, APP.auth.loginCheckInterval);
    
          setTimeout(() => clearInterval(checkLoginInterval), APP.auth.loginCheckTimeout);
        },
    
        handleNodeImageSite: () => {
          if (['/login', '/register', '/'].includes(window.location.pathname)) {
            const loginForm = document.querySelector('form');
            if (loginForm) {
              loginForm.addEventListener('submit', () => {
                APP.storage.set('loginStatus', 'login_pending');
              });
            }
    
            if (APP.storage.get('loginStatus') === 'login_pending') {
              Auth.startLoginStatusCheck();
            }
          } else if (APP.storage.get('loginStatus') === 'login_pending') {
            Auth.checkLoginIfNeeded(true);
          }
    
          Auth.monitorLogout();
        }
      };
    
      // ===== 初始化 (Initialization) =====
      const init = async () => {
        // 如果在NodeImage网站,执行登录辅助逻辑
        if (Utils.isNodeImageSite()) {
          Auth.handleNodeImageSite();
          return;
        }
    
        // 如果在支持的社区站点,执行初始化逻辑
        if (Utils.isCommunitySite()) {
          // 1. 全局监听粘贴事件
          document.addEventListener('paste', ImageHandler.handlePaste);
    
          // 2. 页面获得焦点时检查登录状态
          window.addEventListener('focus', () => Auth.checkLoginIfNeeded());
    
          // 3. 等待编辑器加载并绑定拖拽事件
          Utils.waitForElement(SELECTORS.editor).then(editor => {
            DOM.editor = editor;
    
            editor.addEventListener('dragover', e => {
              e.preventDefault();
              e.dataTransfer.dropEffect = 'copy';
            });
    
            editor.addEventListener('drop', e => {
              e.preventDefault();
              ImageHandler.handleFiles(Array.from(e.dataTransfer.files));
            });
          });
    
          // 4. 等待工具栏加载并注入UI
          Utils.waitForElement(SELECTORS.toolbar).then(UI.setupToolbar);
    
          // 5. 监控DOM变化应对SPA
          const observer = new MutationObserver(() => {
            const toolbar = document.querySelector(SELECTORS.toolbar);
            if (toolbar && !toolbar.querySelector(SELECTORS.container)) {
              UI.setupToolbar(toolbar);
            }
          });
    
          observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'style']
          });
    
          // 6. 监听特定点击事件兼容tab切换
          document.addEventListener('click', e => {
            if (e.target.closest('.tab-option')) {
              setTimeout(() => {
                const toolbar = document.querySelector(SELECTORS.toolbar);
                if (toolbar && !toolbar.querySelector(SELECTORS.container)) {
                  UI.setupToolbar(toolbar);
                }
              }, 100);
            }
          });
    
          // 7. 执行认证状态检查
          Auth.checkLogoutFlag();
          Auth.setupStorageListener();
          await Auth.checkRecentLogin();
          await Auth.checkLoginIfNeeded();
        }
      };
    
      // 脚本从load事件后开始执行
      window.addEventListener('load', init);
    })();
    
    

    和Claude双排改了一下,好像能用了