油猴脚本:AI 验证码自动识别填充

// ==UserScript==
// @name         AI验证码自动识别填充 (专家优化版)
// @namespace    https://github.com/ezyshu/UserScript
// @version      1.0.0
// @author       ezyshu (Optimized by Top Programmer Expert)
// @description  【专家优化版】根据您的指示进行全面优化:增强安全性、减小体积、提升识别率和稳定性。自动识别网页上的验证码并填充到输入框中。
// @license      Apache-2.0
// @icon         https://raw.githubusercontent.com/ezyshu/UserScript/refs/heads/main/CAPTCHA-automatic-recognition/src/assets/logo.png
// @match        *://*/*
// @require      https://unpkg.com/vue@3.4.38/dist/vue.global.prod.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @downloadURL https://update.greasyfork.org/scripts/540822/AI%E9%AA%8C%E8%AF%81%E7%A0%81%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E5%A1%AB%E5%85%85.user.js
// @updateURL https://update.greasyfork.org/scripts/540822/AI%E9%AA%8C%E8%AF%81%E7%A0%81%E8%87%AA%E5%8A%A8%E8%AF%86%E5%88%AB%E5%A1%AB%E5%85%85.meta.js
// ==/UserScript==

(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const o=document.createElement("style");o.textContent=t,document.head.append(o)})(` .captcha-recognition-icon{display:inline-block;width:20px;height:20px;vertical-align:middle;margin-left:5px;background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>');background-size:contain;cursor:pointer;z-index:999;opacity:.7;transition:opacity .2s}.captcha-recognition-icon:hover{opacity:1}.input-group-append{position:relative}.input-group-append .captcha-recognition-icon{position:absolute;left:100%}.captcha-recognition-loading{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2v4"/><path d="M12 18v4"/><path d="M4.93 4.93l2.83 2.83"/><path d="M16.24 16.24l2.83 2.83"/><path d="M2 12h4"/><path d="M18 12h4"/><path d="M4.93 19.07l2.83-2.83"/><path d="M16.24 7.76l2.83-2.83"/></svg>');animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.captcha-recognition-success{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>')}.captcha-recognition-error{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>')}body.captcha-settings-open{overflow:hidden}.captcha-settings-modal{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2147483647;text-align:left}.captcha-settings-content{background-color:#fff;padding:20px;border-radius:8px;width:400px;max-width:90%;max-height:90vh;overflow-y:auto;box-shadow:0 4px 12px #00000026}.captcha-settings-content h3{margin-top:0;color:#333;font-size:18px;margin-bottom:16px;text-align:center}.captcha-settings-item{margin-bottom:12px}.captcha-settings-item label{display:block;margin-bottom:4px;color:#555;font-size:14px}.captcha-settings-item input,.captcha-settings-item select,.captcha-settings-item textarea{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.captcha-settings-item textarea{resize:vertical;min-height:80px}.captcha-settings-buttons{display:flex;justify-content:flex-end;margin-top:20px;gap:10px}.captcha-settings-buttons button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px;transition:background-color .2s}.captcha-settings-buttons button:first-child{background-color:#1a73e8;color:#fff}.captcha-settings-buttons button:first-child:hover{background-color:#1557b0}.captcha-settings-buttons button:last-child{background-color:#f1f1f1;color:#333}.captcha-settings-buttons button:last-child:hover{background-color:#e4e4e4}.dev-settings-button{position:fixed;bottom:20px;right:20px;padding:10px 15px;background-color:#1a73e8;color:#fff;border-radius:4px;cursor:pointer;z-index:9999;font-size:14px;box-shadow:0 2px 5px #0003;transition:background-color .2s}.dev-settings-button:hover{background-color:#1557b0}#captcha-toast-container{position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none}.captcha-toast{min-width:250px;max-width:350px;padding:12px 16px;border-radius:4px;box-shadow:0 4px 12px #00000026;color:#fff;font-size:14px;opacity:0;transform:translateY(-20px);transition:all .3s ease;pointer-events:auto;word-break:break-word}.captcha-toast-show{opacity:1;transform:translateY(0)}.captcha-toast-hide{opacity:0;transform:translateY(-20px)}.captcha-toast-info{background-color:#1a73e8}.captcha-toast-success{background-color:#4caf50}.captcha-toast-error{background-color:#f44336} `);

(function (vue) {
  'use strict';

  const _export_sfc = (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key, val] of props) {
      target[key] = val;
    }
    return target;
  };
  const _sfc_main = {
    data() {
      return {
        // 设置项
        settings: {
          apiType: "openai",
          // openai, gemini
          // OpenAI设置
          openaiKey: "",
          openaiApiUrl: "",
          openaiModel: "",
          openaiPrompt: "",
          // 自定义提示词
          // Gemini设置
          geminiKey: "",
          geminiApiUrl: "",
          geminiModel: "",
          geminiPrompt: "",
          // 自定义提示词
          // 自动识别设置
          autoRecognize: false,
          // 是否启用自动识别
          // 剪贴板设置
          copyToClipboard: true
          // 是否自动复制到剪贴板
        },
        // 是否显示设置面板
        showSettings: false,
        // 配置选项
        config: {
          // 【优化】扩展验证码图片选择器,覆盖更多场景
          captchaSelector: [
            'img[src*="captcha"]',
            'img[src*="verify"]',
            'img[src*="vcode"]',
            'img[src*="code"]',
            'img[src*="yzm"]', // 中文拼音
            'img[alt*="验证码"]',
            'img[title*="验证码"]',
            'img[class*="captcha"]',
            'img[id*="captcha"]',
            'canvas[id*="captcha"]', // 支持canvas验证码
            '.captcha-img',
            '.verify-code-img',
          ].join(','),
          // 相关输入框选择器 (通常在验证码图片附近的输入框)
          inputSelector: 'input[name*="captcha"], input[name*="verify"], input[placeholder*="验证码"], input[placeholder*="captcha"]',
          // 【优化】更精确的提示词模板
          captchaPrompts: {
            default: '请识别这张验证码图片,不要任何解释,只返回识别出的内容。',
            math: '这是一个数学计算题验证码,请计算并直接返回最终的数字结果。',
            chinese: '请识别图片中的汉字,只返回汉字内容。',
          },
        },
        // 用于在模板中访问环境变量
        process: {
          env: {
            NODE_ENV: "production"
          }
        }
      };
    },
    methods: {
      // 【新增】API密钥加密函数 (简单混淆)
      encryptKey(key) {
        if (!key) return "";
        // 简单的混淆,实际可以采用更强的加密
        return btoa(key.split("").reverse().join(""));
      },

      // 【新增】API密钥解密函数
      decryptKey(encryptedKey) {
        if (!encryptedKey) return "";
        try {
          return atob(encryptedKey).split("").reverse().join("");
        } catch (e) {
          // 兼容未加密的旧数据
          return encryptedKey;
        }
      },

      /**
       * 加载用户设置
       */
      loadSettings() {
        try {
          let savedSettingsStr;
          if (typeof GM_getValue !== "undefined") {
            savedSettingsStr = GM_getValue("captchaSettings");
          } else {
            savedSettingsStr = localStorage.getItem("captchaSettings");
          }

          if (savedSettingsStr) {
            const savedSettings = JSON.parse(savedSettingsStr);
            // 【优化】加载时解密API Key
            savedSettings.openaiKey = this.decryptKey(savedSettings.openaiKey);
            savedSettings.geminiKey = this.decryptKey(savedSettings.geminiKey);
            this.settings = { ...this.settings, ...savedSettings };
          }
        } catch (error) {
          console.error("加载设置失败:", error);
        }
      },
      /**
       * 显示设置面板
       */
      openSettings() {
        document.body.classList.add("captcha-settings-open");
        this.showSettings = true;
      },
      /**
       * 关闭设置面板
       */
      closeSettings() {
        document.body.classList.remove("captcha-settings-open");
        this.showSettings = false;
      },
      /**
       * 保存用户设置
       */
      saveSettings() {
        try {
          // 【优化】保存时加密API Key
          const settingsToSave = {
            ...this.settings,
            openaiKey: this.encryptKey(this.settings.openaiKey),
            geminiKey: this.encryptKey(this.settings.geminiKey),
          };

          const settingsStr = JSON.stringify(settingsToSave);

          if (typeof GM_setValue !== "undefined") {
            GM_setValue("captchaSettings", settingsStr);
          } else {
            localStorage.setItem("captchaSettings", settingsStr);
          }
          this.closeSettings();
          this.showToast("设置已保存!", "success");
        } catch (error) {
          console.error("保存设置失败:", error);
          this.showToast("保存设置失败,请查看控制台获取更多信息。", "error");
        }
      },
      /**
       * 使用AI识别验证码
       * @param {HTMLImageElement} captchaImg - 验证码图片元素
       * @returns {Promise<string>} - 识别结果
       */
      async recognizeCaptcha(captchaImg) {
        if (!this.isApiConfigured()) {
          this.showToast("请先配置验证码识别API", "error");
          this.openSettings();
          return "";
        }

        const base64Image = await this.imageToBase64(captchaImg);
        if (!base64Image) {
            this.showToast("图片转Base64失败", "error");
            return "";
        }

        // 【优化】智能检测验证码类型并获取对应提示词
        const captchaType = this.detectCaptchaType(captchaImg);
        const prompt = this.config.captchaPrompts[captchaType];

        let result = "";
        try {
          switch (this.settings.apiType) {
            case "openai":
              result = await this.recognizeWithOpenAI(base64Image, prompt);
              break;
            case "gemini":
              result = await this.recognizeWithGemini(base64Image, prompt);
              break;
          }
          return result;
        } catch (error) {
          console.error("验证码识别API调用失败:", error);
          // 错误信息在重试逻辑中统一处理
          throw error;
        }
      },
      /**
       * 【新增】带重试机制的验证码识别
       * @param {HTMLImageElement} captchaImg - 验证码图片元素
       * @param {number} maxRetries - 最大重试次数
       * @returns {Promise<string>} - 识别结果
       */
      async recognizeCaptchaWithRetry(captchaImg, maxRetries = 3) {
          this.showToast("正在识别验证码...", "info");
          for (let i = 0; i < maxRetries; i++) {
              try {
                  const result = await this.recognizeCaptcha(captchaImg);
                  // 简单的结果合理性校验
                  if (result && result.length >= 1 && result.length <= 10) {
                      this.showToast(`识别成功: ${result}`, "success");
                      return result;
                  }
                  // 如果结果不合理,也当作一次失败
                  console.warn(`第 ${i + 1} 次识别结果不合理: ${result}`);
              } catch (error) {
                  console.error(`第 ${i + 1} 次识别失败:`, error);
                  if (i === maxRetries - 1) {
                      this.showToast("识别失败: " + (error.message || "未知错误"), "error");
                      throw error; // 最后一次失败则抛出异常
                  }
                  // 延迟重试
                  await new Promise(resolve => setTimeout(resolve, 1000));
              }
          }
          this.showToast("多次尝试后识别失败", "error");
          return "";
      },
      /**
       * 检查API是否配置
       */
      isApiConfigured() {
        switch (this.settings.apiType) {
          case "openai":
            return !!this.settings.openaiKey;
          case "gemini":
            return !!this.settings.geminiKey;
          default:
            return false;
        }
      },
      /**
       * 【优化】将图片转换为base64格式,并进行预处理
       */
      imageToBase64(imgElement) {
        return new Promise((resolve, reject) => {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            const img = new Image();
            img.crossOrigin = 'anonymous'; // 尝试解决跨域问题

            img.onload = () => {
                canvas.width = img.width;
                canvas.height = img.height;
                ctx.drawImage(img, 0, 0);

                // 可选:图片锐化或二值化处理,以提高识别率
                // const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                // applySharpenFilter(imageData.data, canvas.width, canvas.height); // 假设有锐化函数
                // ctx.putImageData(imageData, 0, 0);

                resolve(canvas.toDataURL('image/png').split(',')[1]);
            };
            img.onerror = () => {
                console.error("图片加载失败,无法转换为Base64");
                reject(new Error("Image load error"));
            };
            img.src = imgElement.src;
        });
      },
      /**
       * 【新增】智能判断验证码类型
       * @param {HTMLImageElement} imgElement
       * @returns {'default' | 'math' | 'chinese'}
       */
      detectCaptchaType(imgElement) {
        const src = (imgElement.src || '').toLowerCase();
        const alt = (imgElement.alt || '').toLowerCase();
        const title = (imgElement.title || '').toLowerCase();
        const text = src + alt + title;

        if (text.includes('math') || text.includes('计算')) {
          return 'math';
        }
        if (text.includes('chinese') || text.includes('中文') || text.includes('汉字')) {
          return 'chinese';
        }
        return 'default';
      },
      /**
       * 使用OpenAI API识别验证码
       */
      async recognizeWithOpenAI(base64Image, prompt) {
        const apiUrl = this.settings.openaiApiUrl || "https://api.openai.com/v1/chat/completions";
        const model = this.settings.openaiModel || "gpt-4o-mini";
        const finalPrompt = this.settings.openaiPrompt || prompt; // 优先使用用户自定义的全局提示词

        const response = await this.request({
          method: "POST",
          url: apiUrl,
          data: {
            model,
            messages: [
              {
                role: "user",
                content: [
                  { type: "text", text: finalPrompt },
                  { type: "image_url", image_url: { url: `data:image/png;base64,${base64Image}` } }
                ]
              }
            ],
            max_tokens: 30
          },
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${this.settings.openaiKey}`
          }
        });
        return response.data.choices[0].message.content.trim();
      },
      /**
       * 使用Google Gemini API识别验证码
       */
      async recognizeWithGemini(base64Image, prompt) {
        const model = this.settings.geminiModel || "gemini-1.5-flash-latest";
        const baseApiUrl = this.settings.geminiApiUrl || "https://generativelanguage.googleapis.com/v1beta/models";
        const apiUrl = `${baseApiUrl}/${model}:generateContent`;
        const finalPrompt = this.settings.geminiPrompt || prompt; // 优先使用用户自定义的全局提示词

        const response = await this.request({
          method: "POST",
          url: `${apiUrl}?key=${this.settings.geminiKey}`,
          data: {
            contents: [
              {
                parts: [
                  { text: finalPrompt },
                  { inline_data: { mime_type: "image/png", data: base64Image } }
                ]
              }
            ],
            generationConfig: {
              temperature: 0,
              maxOutputTokens: 30
            }
          },
          headers: {
            "Content-Type": "application/json"
          }
        });
        if (response.data.candidates && response.data.candidates.length > 0) {
          const candidate = response.data.candidates[0];
          if (candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
            const text = candidate.content.parts[0].text || "";
            return text.replace(/[^a-zA-Z0-9]/g, "").trim();
          }
        }
        return "";
      },
      /**
       * 查找页面上的验证码图片和相关输入框
       */
      findCaptchaElements() {
        const captchaImages = document.querySelectorAll(
          this.config.captchaSelector
        );
        if (captchaImages.length === 0) {
          return [];
        }
        const elements = [];
        captchaImages.forEach((img) => {
          let inputField = null;
          const inputs = document.querySelectorAll(this.config.inputSelector);
          if (inputs.length > 0) {
            let minDistance = Infinity;
            let closestInput = null;
            inputs.forEach((input) => {
              const distance = this.getDistance(img, input);
              if (distance < minDistance) {
                minDistance = distance;
                closestInput = input;
              }
            });
            if (minDistance < 300) {
              inputField = closestInput;
            }
          }
          if (!inputField) {
            let parent = img.parentElement;
            let level = 0;
            while (parent && level < 5) {
              const nearInputs = parent.querySelectorAll('input[type="text"]');
              if (nearInputs.length > 0) {
                inputField = nearInputs[0];
                break;
              }
              parent = parent.parentElement;
              level++;
            }
          }
          if (inputField) {
            elements.push({ captchaImg: img, inputField });
          }
        });
        return elements;
      },
      /**
       * 计算两个元素之间的距离
       */
      getDistance(el1, el2) {
        const rect1 = el1.getBoundingClientRect();
        const rect2 = el2.getBoundingClientRect();
        const x1 = rect1.left + rect1.width / 2;
        const y1 = rect1.top + rect1.height / 2;
        const x2 = rect2.left + rect2.width / 2;
        const y2 = rect2.top + rect2.height / 2;
        return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
      },
      /**
       * 在验证码图片旁添加识别图标
       */
      addIconsToCaptchas() {
        const elements = this.findCaptchaElements();
        elements.forEach(({ captchaImg, inputField }) => {
          const existingIcon = captchaImg.nextElementSibling;
          if (existingIcon && existingIcon.classList.contains("captcha-recognition-icon")) {
            return;
          }
          const icon = document.createElement("div");
          icon.classList.add("captcha-recognition-icon");
          icon.title = "点击识别验证码";
          if (captchaImg.nextSibling) {
            captchaImg.parentNode.insertBefore(icon, captchaImg.nextSibling);
          } else {
            captchaImg.parentNode.appendChild(icon);
          }
          icon.addEventListener("click", async () => {
            this.processCaptcha(captchaImg, inputField, icon);
          });
        });
      },
      /**
       * 处理验证码识别
       */
      async processCaptcha(captchaImg, inputField, icon) {
        try {
          icon.classList.add("captcha-recognition-loading");
          // 【优化】调用带重试的识别函数
          const text = await this.recognizeCaptchaWithRetry(captchaImg);
          if (!text) {
            icon.classList.remove("captcha-recognition-loading");
            icon.classList.add("captcha-recognition-error");
            setTimeout(() => {
              icon.classList.remove("captcha-recognition-error");
            }, 2e3);
            return;
          }
          inputField.value = text;
          inputField.dispatchEvent(new Event("input", { bubbles: true }));
          inputField.dispatchEvent(new Event("change", { bubbles: true }));
          if (this.settings.copyToClipboard) {
            try {
              await navigator.clipboard.writeText(text);
              this.showToast(`验证码已识别: ${text} (已复制)`, "success");
            } catch (clipboardError) {
              // Fallback for older browsers or insecure contexts
              const textarea = document.createElement("textarea");
              textarea.value = text;
              textarea.style.position = "fixed";
              textarea.style.opacity = "0";
              document.body.appendChild(textarea);
              textarea.select();
              document.execCommand("copy");
              document.body.removeChild(textarea);
              this.showToast(`验证码已识别: ${text} (已复制)`, "success");
            }
          } else {
            this.showToast(`验证码已识别: ${text}`, "success");
          }
          icon.classList.remove("captcha-recognition-loading");
          icon.classList.add("captcha-recognition-success");
          setTimeout(() => {
            icon.classList.remove("captcha-recognition-success");
          }, 2e3);
        } catch (error) {
          console.error("处理验证码失败:", error);
          icon.classList.remove("captcha-recognition-loading");
          icon.classList.add("captcha-recognition-error");
          setTimeout(() => {
            icon.classList.remove("captcha-recognition-error");
          }, 2e3);
        }
      },
      /**
       * 监听DOM变化,自动为新添加的验证码添加识别图标
       */
      setupMutationObserver() {
        const observer = new MutationObserver((mutations) => {
          let hasNewCaptcha = false;
          let newCaptchaElements = [];
          mutations.forEach((mutation) => {
            if (mutation.type === "childList" && mutation.addedNodes.length) {
              mutation.addedNodes.forEach((node) => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                  const captchas = node.querySelectorAll(
                    this.config.captchaSelector
                  );
                  if (captchas.length > 0) {
                    hasNewCaptcha = true;
                    captchas.forEach((captcha) => {
                      newCaptchaElements.push(captcha);
                    });
                  }
                }
              });
            }
            if (mutation.type === "attributes" && mutation.attributeName === "src" && mutation.target.matches && mutation.target.matches(this.config.captchaSelector)) {
              hasNewCaptcha = true;
              newCaptchaElements.push(mutation.target);
            }
          });
          if (hasNewCaptcha) {
            this.addIconsToCaptchas();
            if (this.settings.autoRecognize) {
              setTimeout(() => {
                const elements = this.findCaptchaElements();
                elements.forEach(({ captchaImg, inputField }) => {
                  if (newCaptchaElements.includes(captchaImg)) {
                    let icon;
                    const existingIcon = captchaImg.nextElementSibling;
                    if (existingIcon && existingIcon.classList.contains("captcha-recognition-icon")) {
                      icon = existingIcon;
                    } else {
                      icon = document.createElement("div");
                      icon.classList.add("captcha-recognition-icon");
                      if (captchaImg.nextSibling) {
                        captchaImg.parentNode.insertBefore(
                          icon,
                          captchaImg.nextSibling
                        );
                      } else {
                        captchaImg.parentNode.appendChild(icon);
                      }
                    }
                    this.processCaptcha(captchaImg, inputField, icon);
                  }
                });
              }, 500);
            }
          }
        });
        observer.observe(document.body, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ["src"]
        });
      },
      /**
       * 注册油猴菜单
       */
      registerMenuCommands() {
        if (typeof GM_registerMenuCommand !== "undefined") {
          GM_registerMenuCommand("验证码识别设置", () => {
            this.openSettings();
          });
        }
      },
      /**
       * 初始化插件
       */
      init() {
        this.registerMenuCommands();
        this.loadSettings();
        setTimeout(() => {
          this.addIconsToCaptchas();
          this.setupMutationObserver();
          const elements = this.findCaptchaElements();
          if (elements.length > 0) {
            if (this.settings.autoRecognize) {
              this.showToast(
                `检测到 ${elements.length} 个验证码,正在自动识别...`,
                "info"
              );
              elements.forEach(({ captchaImg, inputField }) => {
                const icon = captchaImg.nextElementSibling;
                if (icon && icon.classList.contains("captcha-recognition-icon")) {
                  this.processCaptcha(captchaImg, inputField, icon);
                }
              });
            } else {
              this.showToast(
                `检测到 ${elements.length} 个验证码,点击图标开始识别`,
                "info"
              );
            }
          }
        }, 1e3);
      },
      /**
       * 【优化】通用请求函数,移除axios,仅使用GM_xmlhttpRequest
       * @param {Object} config - 请求配置
       * @returns {Promise} - 请求结果
       */
      request(config) {
        if (typeof GM_xmlhttpRequest === "undefined") {
          this.showToast("错误: GM_xmlhttpRequest 未定义,脚本无法发送请求。", "error");
          return Promise.reject(new Error("GM_xmlhttpRequest is not defined."));
        }
        return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: config.method || "GET",
            url: config.url,
            data: config.data ? JSON.stringify(config.data) : undefined,
            headers: config.headers || {},
            responseType: "json",
            onload: (response) => {
              if (response.status >= 200 && response.status < 300) {
                resolve({ data: response.response });
              } else {
                let errorMsg = `请求失败,状态码: ${response.status}`;
                if(response.response && response.response.error && response.response.error.message) {
                    errorMsg += ` - ${response.response.error.message}`;
                }
                reject(new Error(errorMsg));
              }
            },
            onerror: (error) => {
              reject(new Error("网络请求错误: " + error.details));
            },
            ontimeout: () => {
              reject(new Error("请求超时"));
            }
          });
        });
      },
      /**
       * 显示Toast提示
       */
      showToast(message, type = "info") {
        let toastContainer = document.getElementById("captcha-toast-container");
        if (!toastContainer) {
          toastContainer = document.createElement("div");
          toastContainer.id = "captcha-toast-container";
          document.body.appendChild(toastContainer);
        }
        const toast = document.createElement("div");
        toast.className = `captcha-toast captcha-toast-${type}`;
        toast.textContent = message;
        if (toastContainer.firstChild) {
          toastContainer.insertBefore(toast, toastContainer.firstChild);
        } else {
          toastContainer.appendChild(toast);
        }
        setTimeout(() => {
          toast.classList.add("captcha-toast-show");
        }, 10);
        setTimeout(() => {
          toast.classList.remove("captcha-toast-show");
          toast.classList.add("captcha-toast-hide");
          setTimeout(() => {
            if (toast.parentNode) {
              toast.parentNode.removeChild(toast);
            }
          }, 300);
        }, 3e3);
      }
    },
    mounted() {
      this.init();
    }
  };
  const _hoisted_1 = { class: "captcha-recognition-container" };
  const _hoisted_2 = {
    key: 1,
    class: "captcha-settings-modal"
  };
  const _hoisted_3 = { class: "captcha-settings-content" };
  const _hoisted_4 = /* @__PURE__ */ vue.createElementVNode("h3", null, "验证码识别设置", -1);
  const _hoisted_5 = { class: "captcha-settings-item" };
  const _hoisted_6 = /* @__PURE__ */ vue.createElementVNode("label", null, "API类型:", -1);
  const _hoisted_7 = /* @__PURE__ */ vue.createElementVNode("option", { value: "openai" }, "OpenAI", -1);
  const _hoisted_8 = /* @__PURE__ */ vue.createElementVNode("option", { value: "gemini" }, "Google Gemini", -1);
  const _hoisted_9 = [
    _hoisted_7,
    _hoisted_8
  ];
  const _hoisted_10 = { key: 0 };
  const _hoisted_11 = { class: "captcha-settings-item" };
  const _hoisted_12 = /* @__PURE__ */ vue.createElementVNode("label", null, "OpenAI API Key:", -1);
  const _hoisted_13 = { class: "captcha-settings-item" };
  const _hoisted_14 = /* @__PURE__ */ vue.createElementVNode("label", null, "自定义API地址 (可选):", -1);
  const _hoisted_15 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空使用默认地址", -1);
  const _hoisted_16 = { class: "captcha-settings-item" };
  const _hoisted_17 = /* @__PURE__ */ vue.createElementVNode("label", null, "模型 (可选):", -1);
  const _hoisted_18 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空使用默认模型 gpt-4o-mini", -1);
  const _hoisted_19 = { class: "captcha-settings-item" };
  const _hoisted_20 = /* @__PURE__ */ vue.createElementVNode("label", null, "自定义全局提示词 (可选):", -1);
  const _hoisted_21 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空则根据验证码类型自动选择提示词", -1);
  const _hoisted_22 = { key: 1 };
  const _hoisted_23 = { class: "captcha-settings-item" };
  const _hoisted_24 = /* @__PURE__ */ vue.createElementVNode("label", null, "Google Gemini API Key:", -1);
  const _hoisted_25 = { class: "captcha-settings-item" };
  const _hoisted_26 = /* @__PURE__ */ vue.createElementVNode("label", null, "自定义API地址 (可选):", -1);
  const _hoisted_27 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空使用默认地址", -1);
  const _hoisted_28 = { class: "captcha-settings-item" };
  const _hoisted_29 = /* @__PURE__ */ vue.createElementVNode("label", null, "模型 (可选):", -1);
  const _hoisted_30 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空使用默认模型 gemini-1.5-flash-latest", -1);
  const _hoisted_31 = { class: "captcha-settings-item" };
  const _hoisted_32 = /* @__PURE__ */ vue.createElementVNode("label", null, "自定义全局提示词 (可选):", -1);
  const _hoisted_33 = /* @__PURE__ */ vue.createElementVNode("small", null, "留空则根据验证码类型自动选择提示词", -1);
  const _hoisted_34 = { class: "captcha-settings-item" };
  const _hoisted_35 = /* @__PURE__ */ vue.createElementVNode("label", null, "自动识别:", -1);
  const _hoisted_36 = { style: { "display": "flex", "align-items": "center" } };
  const _hoisted_37 = /* @__PURE__ */ vue.createElementVNode("label", {
    for: "autoRecognize",
    style: { "margin-bottom": "0" }
  }, "验证码图片变化时自动识别", -1);
  const _hoisted_38 = { class: "captcha-settings-item" };
  const _hoisted_39 = /* @__PURE__ */ vue.createElementVNode("label", null, "自动复制到剪贴板:", -1);
  const _hoisted_40 = { style: { "display": "flex", "align-items": "center" } };
  const _hoisted_41 = /* @__PURE__ */ vue.createElementVNode("label", {
    for: "copyToClipboard",
    style: { "margin-bottom": "0" }
  }, "自动复制到剪贴板", -1);
  const _hoisted_42 = { class: "captcha-settings-buttons" };
  function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
    return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
      $data.process.env.NODE_ENV === "development" && !$data.showSettings ? (vue.openBlock(), vue.createElementBlock("div", {
        key: 0,
        class: "dev-settings-button",
        onClick: _cache[0] || (_cache[0] = (...args) => $options.openSettings && $options.openSettings(...args))
      }, " 打开设置 ")) : vue.createCommentVNode("", true),
      $data.showSettings ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2, [
        vue.createElementVNode("div", _hoisted_3, [
          _hoisted_4,
          vue.createElementVNode("div", _hoisted_5, [
            _hoisted_6,
            vue.withDirectives(vue.createElementVNode("select", {
              "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => $data.settings.apiType = $event)
            }, _hoisted_9, 512), [
              [vue.vModelSelect, $data.settings.apiType]
            ])
          ]),
          $data.settings.apiType === "openai" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10, [
            vue.createElementVNode("div", _hoisted_11, [
              _hoisted_12,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "password",
                "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => $data.settings.openaiKey = $event),
                placeholder: "sk-..."
              }, null, 512), [
                [vue.vModelText, $data.settings.openaiKey]
              ])
            ]),
            vue.createElementVNode("div", _hoisted_13, [
              _hoisted_14,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "text",
                "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => $data.settings.openaiApiUrl = $event),
                placeholder: "https://api.openai.com/v1/chat/completions"
              }, null, 512), [
                [vue.vModelText, $data.settings.openaiApiUrl]
              ]),
              _hoisted_15
            ]),
            vue.createElementVNode("div", _hoisted_16, [
              _hoisted_17,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "text",
                "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => $data.settings.openaiModel = $event),
                placeholder: "gpt-4o-mini"
              }, null, 512), [
                [vue.vModelText, $data.settings.openaiModel]
              ]),
              _hoisted_18
            ]),
            vue.createElementVNode("div", _hoisted_19, [
              _hoisted_20,
              vue.withDirectives(vue.createElementVNode("textarea", {
                "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => $data.settings.openaiPrompt = $event),
                rows: "3"
              }, null, 512), [
                [vue.vModelText, $data.settings.openaiPrompt]
              ]),
              _hoisted_21
            ])
          ])) : vue.createCommentVNode("", true),
          $data.settings.apiType === "gemini" ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_22, [
            vue.createElementVNode("div", _hoisted_23, [
              _hoisted_24,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "password",
                "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => $data.settings.geminiKey = $event),
                placeholder: "输入Gemini API Key"
              }, null, 512), [
                [vue.vModelText, $data.settings.geminiKey]
              ])
            ]),
            vue.createElementVNode("div", _hoisted_25, [
              _hoisted_26,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "text",
                "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => $data.settings.geminiApiUrl = $event),
                placeholder: "https://generativelanguage.googleapis.com/v1beta/models"
              }, null, 512), [
                [vue.vModelText, $data.settings.geminiApiUrl]
              ]),
              _hoisted_27
            ]),
            vue.createElementVNode("div", _hoisted_28, [
              _hoisted_29,
              vue.withDirectives(vue.createElementVNode("input", {
                type: "text",
                "onUpdate:modelValue": _cache[8] || (_cache[8] = ($event) => $data.settings.geminiModel = $event),
                placeholder: "gemini-1.5-flash-latest"
              }, null, 512), [
                [vue.vModelText, $data.settings.geminiModel]
              ]),
              _hoisted_30
            ]),
            vue.createElementVNode("div", _hoisted_31, [
              _hoisted_32,
              vue.withDirectives(vue.createElementVNode("textarea", {
                "onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => $data.settings.geminiPrompt = $event),
                rows: "3"
              }, null, 512), [
                [vue.vModelText, $data.settings.geminiPrompt]
              ]),
              _hoisted_33
            ])
          ])) : vue.createCommentVNode("", true),
          vue.createElementVNode("div", _hoisted_34, [
            _hoisted_35,
            vue.createElementVNode("div", _hoisted_36, [
              vue.withDirectives(vue.createElementVNode("input", {
                type: "checkbox",
                "onUpdate:modelValue": _cache[10] || (_cache[10] = ($event) => $data.settings.autoRecognize = $event),
                id: "autoRecognize",
                style: { "width": "auto", "margin-right": "8px" }
              }, null, 512), [
                [vue.vModelCheckbox, $data.settings.autoRecognize]
              ]),
              _hoisted_37
            ])
          ]),
          vue.createElementVNode("div", _hoisted_38, [
            _hoisted_39,
            vue.createElementVNode("div", _hoisted_40, [
              vue.withDirectives(vue.createElementVNode("input", {
                type: "checkbox",
                "onUpdate:modelValue": _cache[11] || (_cache[11] = ($event) => $data.settings.copyToClipboard = $event),
                id: "copyToClipboard",
                style: { "width": "auto", "margin-right": "8px" }
              }, null, 512), [
                [vue.vModelCheckbox, $data.settings.copyToClipboard]
              ]),
              _hoisted_41
            ])
          ]),
          vue.createElementVNode("div", _hoisted_42, [
            vue.createElementVNode("button", {
              onClick: _cache[12] || (_cache[12] = (...args) => $options.saveSettings && $options.saveSettings(...args))
            }, "保存设置"),
            vue.createElementVNode("button", {
              onClick: _cache[13] || (_cache[13] = (...args) => $options.closeSettings && $options.closeSettings(...args))
            }, "取消")
          ])
        ])
      ])) : vue.createCommentVNode("", true)
    ]);
  }
  const App = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
  const app = vue.createApp(App);
  app.mount(
    (() => {
      const appDiv = document.createElement("div");
      document.body.append(appDiv);
      return appDiv;
    })()
  );

})(Vue);

用claude和gemini精简了一下,用的gemini-2.5-flash-lite-preview-06-17模型和以下的提示词

# Role: 验证码识别专家

## Profile
- language: 中文
- description: 一个专为高精度识别验证码而设计的AI模型。能够快速、准确地从复杂的图像中提取字符或计算数学表达式的结果,并能有效对抗常见的干扰元素。
- background: 基于海量、多样的验证码图像数据集进行深度训练,精通各种字符扭曲、粘连、遮挡和背景干扰的识别技术,具备强大的泛化能力。
- personality: 精确、高效、客观、直接。只关注任务本身,不产生任何与结果无关的额外信息。
- expertise: 计算机视觉、高级光学字符识别(OCR)、图像预处理与去噪、模式识别、基础算术逻辑。
- target_audience: 需要自动化处理验证码的开发者、自动化测试工程师、数据科学家。

## Skills

1. 核心识别能力
   - 高精度字符识别: 准确识别大小写英文字母、数字,并能精确区分外形相似的字符(如:0和O,1和l,g和9)。
   - 数学运算处理: 识别并解析图片中的数学算式(如:3+5*2),并计算出最终的数值结果。
   - 强抗干扰能力: 自动过滤和忽略图像中的干扰线、噪点、斑块、背景纹理等非关键信息。
   - 字符分割技术: 即使在字符粘连、重叠或间距不等的情况下,也能有效地将其分离以便独立识别。

2. 辅助处理能力
   - 图像预处理: 自动对输入图像进行灰度化、二值化、去噪等操作,以提升识别的准确率。
   - 快速响应: 以极低的延迟返回识别结果,满足实时性要求。
   - 结果格式化: 严格按照指定的格式输出,确保输出的纯净性,便于程序调用。
   - 鲁棒性: 对于不同字体、大小、颜色、角度的字符组合均有较高的识别成功率。

## Rules

1. 基本原则:
   - 结果唯一: 输出内容必须是且仅是验证码的识别结果。
   - 绝对精确: 尽最大努力确保字符识别的大小写和数值计算的准确性。
   - 任务聚焦: 仅处理验证码内容,忽略图像中的任何其他元素。
   - 保持静默: 除最终结果外,不输出任何提示、标签、解释或说明。

2. 行为准则:
   - 直接输出结果: 若为字符型验证码,直接返回字符串;若为计算题,直接返回计算后的数字。
   - 严格区分大小写: 必须准确识别并返回字符的原始大小写形式(例如'W'和'w'是不同字符)。
   - 精准区分易混淆字符: 必须对数字“0”和字母“O”、数字“1”和字母“l”等易混淆字符进行准确区分。
   - 自动执行运算: 遇到数学表达式时,必须完成计算并仅返回最终的阿拉伯数字结果。

3. 限制条件:
   - 禁止任何解释: 不得对识别过程、结果的置信度或遇到的困难进行任何说明。
   - 禁止附加文本: 返回的最终结果前后不能有任何空格、引号、标签或“答案是:”等引导性词语。
   - 禁止互动: 不得向用户提问或请求更清晰的图片。
   - 禁止失败提示: 即使无法完全识别,也应根据已识别内容尽力输出,而不是返回“无法识别”之类的自然语言。

## Workflows

- 目标: 接收一张验证码图片,精准、快速地返回其内容或计算结果。
- 步骤 1: 接收图像并进行分析,判断验证码类型(字符型或数学计算型)。
- 步骤 2: 应用图像预处理技术,对图像进行降噪、增强和二值化,以凸显关键字符,消除干扰线和背景。
- 步骤 3: 对处理后的图像进行字符分割,然后逐一识别。对于数学题,则识别数字和运算符。
- 步骤 4: 整合识别结果。如果是字符,则按顺序拼接成字符串;如果是数学题,则执行运算。
- 步骤 5: 输出最终结果。确保输出内容绝对纯净,符合Rules中的所有规定。
- 预期结果: 一个不包含任何多余信息的字符串(如“aB5fG”)或一个数字(如“28”)。


## Initialization
作为验证码识别专家,你必须遵守上述Rules,按照Workflows执行任务。


6 个赞