自动提取政和验证码

接着奏乐接着舞 Find an img with src containing "api-uaa/v1/validate/code", get Base64, and send it for captcha code

// ==UserScript==
// @name         自动提取政和验证码
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  接着奏乐接着舞 Find an img with src containing "api-uaa/v1/validate/code", get Base64, and send it for captcha code
// @match        *://*/*
// @match        *://*/*
// @require      http://libs.baidu.com/jquery/2.0.0/jquery.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// 显示识别结果弹框,并在 3 秒后自动关闭,带进度条
function showCaptchaResultPopup(resultText) {
    // 首先检查是否已有弹框存在,如果存在,则移除之前的弹框
    const existingModal = document.querySelector('.captcha-result-modal');
    if (existingModal) {
        existingModal.remove();  // 移除之前的弹框
    }

    // 创建新的弹框容器
    const modal = document.createElement('div');
    modal.className = 'captcha-result-modal';  // 给弹框加一个类名,方便以后查找
    modal.style.position = 'fixed';
    modal.style.top = '10px';
    modal.style.right = '20px';
    modal.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
    modal.style.color = 'white';
    modal.style.padding = '10px 20px';  // 减小弹框的高度
    modal.style.borderRadius = '10px';
    modal.style.zIndex = '999999'; // 确保在页面最上层
    modal.style.width = '250px'; // 设置弹框宽度
    modal.style.boxSizing = 'border-box';

    // 创建弹框标题
    const title = document.createElement('h3');
    title.innerText = '验证码识别结果';
    title.style.margin = '0 0 5px';
    title.style.fontSize = '16px';  // 减小字体大小
    modal.appendChild(title);

    // 创建弹框内容
    const content = document.createElement('p');
    content.innerText = resultText;  // 展示识别结果
    content.style.fontSize = '14px';  // 减小字体大小
    modal.appendChild(content);

    // 创建关闭按钮
    const closeButton = document.createElement('button');
    closeButton.innerText = '×';
    closeButton.style.position = 'absolute';
    closeButton.style.top = '5px';
    closeButton.style.right = '5px';
    closeButton.style.padding = '0px';
    closeButton.style.backgroundColor = '#FF4D4F'; // 红色背景
    closeButton.style.color = 'white';
    closeButton.style.border = 'none';
    closeButton.style.borderRadius = '50%'; // 圆形按钮
    closeButton.style.cursor = 'pointer';
    closeButton.style.fontSize = '20px';  // 增大字体
    closeButton.style.width = '20px';  // 定义宽度
    closeButton.style.height = '20px'; // 定义高度
    closeButton.style.transition = 'background-color 0.3s';  // 添加平滑过渡效果

    // 绑定关闭按钮事件
    closeButton.addEventListener('click', function () {
        modal.remove();  // 关闭弹框
    });

    // 将关闭按钮添加到弹框
    modal.appendChild(closeButton);

    // 创建进度条容器
    const progressBarContainer = document.createElement('div');
    progressBarContainer.style.height = '5px';
    progressBarContainer.style.backgroundColor = '#444';
    progressBarContainer.style.borderRadius = '5px';
    progressBarContainer.style.overflow = 'hidden';
    modal.appendChild(progressBarContainer);

    // 创建进度条
    const progressBar = document.createElement('div');
    progressBar.style.height = '100%';
    progressBar.style.backgroundColor = '#4caf50'; // 进度条颜色
    progressBar.style.width = '100%';  // 初始进度为 100%
    progressBarContainer.appendChild(progressBar);

    // 将弹框添加到页面
    document.body.appendChild(modal);

    // 设置倒计时,3秒后自动关闭
    let timeLeft = 5; // 5 秒倒计时
    let interval;

    // 更新进度条和倒计时
    function updateProgressBar() {
        timeLeft -= 0.05;
        progressBar.style.width = (timeLeft / 5) * 100 + '%';  // 更新进度条

        if (timeLeft <= 0) {
            clearInterval(interval);  // 清除定时器
            modal.remove();  // 关闭弹框
        }
    }

    // 启动进度条更新定时器
    interval = setInterval(updateProgressBar, 50); // 每 50 毫秒更新一次进度条

    // 鼠标悬浮时暂停进度条和倒计时
    modal.addEventListener('mouseover', () => {
        clearInterval(interval);  // 暂停进度条更新
    });

    // 鼠标离开时重新开始进度条
    modal.addEventListener('mouseleave', () => {
        interval = setInterval(updateProgressBar, 50);  // 恢复进度条更新
    });
}


// 处理图片验证码
function processImageCaptcha(imgElement) {
    if (!imgElement || !imgElement.src) return;
    // 打印图片信息
    console.log('识别图像验证码:', imgElement.src);
    // 将图片转换为 Base64 编码
    getBase64Code(imgElement).then(base64 => {
        // 这里可以调用识别 API 来处理 Base64 编码的图片
        // 发送 Base64 编码给服务器进行识别
        if (base64) sendImageForOCR(imgElement, base64);
    }).catch(error => {
        console.error('图片转换失败:', error);
    });
}

// 获取图片的 Base64 编码
function getBase64Code(element) {
    return new Promise((resolve, reject) => {
        // 确保图片加载完成
        if (element.complete) {
            // 如果图片已经加载完,直接获取宽高
            resolve(getImageBase64(element));
        } else {
            element.onload = function () {
                resolve(getImageBase64(element));
            };
            element.onerror = function () {
                reject('图片加载失败');
            };
        }
    });
}


// 用于判断图像是否跨域的函数
function isCrossOrigin(src) {
    const link = document.createElement('a');
    link.href = src;

    // 如果源与当前页面的主机不同,则认为是跨域资源
    return link.origin !== window.location.origin;
}
function isValidBase64(base64) {
    // 判断 base64 长度是否小于 20,或者不包含 data:image/ 前缀
    if (base64.length < 20 || !base64.startsWith('data:image/')) {
        return false;  // 无效的 base64 数据
    }
    return true;  // 基本有效的图片数据
}

// 获取图片的 Base64 编码
function getImageBase64(element) {
    return new Promise((resolve, reject) => {
        const src = element.src;
        // 静态,单个图片 0 英文+数字  1 算数
        if (src.includes("type=0") || src.includes("type=1")) {
            try {
                // 判断图像是否为跨域资源(简单的判断方法:如果资源是外部域名,可能是跨域)
                if (isCrossOrigin(src)) {
                    element.crossOrigin = 'Anonymous'; // 仅在跨域时设置
                }
                // 创建一个 canvas 元素
                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d');
                // 确保 canvas 的尺寸与图片相同
                canvas.width = element.naturalWidth;
                canvas.height = element.naturalHeight;
                console.log("验证码宽高:", element.naturalWidth, element.naturalHeight);
                // 将图片绘制到 canvas 上
                context.drawImage(element, 0, 0, element.naturalWidth, element.naturalHeight);

                // 尝试直接转换,如果失败,可能存在跨域
                let base64 = canvas.toDataURL("image/png");
                console.log(isValidBase64(base64))
                if (isValidBase64(base64)) {
                    resolve(base64);
                } else {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: src,
                        responseType: 'arraybuffer',
                        onload: function (response) {
                            if (response.status === 200) {
                                // 获取图像的二进制数据
                                const binary = new Uint8Array(response.response);
                                // 将二进制数据转成 base64 编码
                                base64 = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, binary));

                                // 保存原始的事件处理器
                                const clickEvent = element.onclick;

                                // 替换 img 元素的 src 并触发图像加载
                                element.src = base64;

                                // 确保图片加载完成后执行其他操作
                                element.onload = function() {
                                    console.log("图片已更新并加载完毕");

                                    // 重新绑定原始的点击事件
                                    if (clickEvent) {
                                        element.onclick = clickEvent;
                                    }

                                    // 你可以在这里添加其他的事件处理逻辑
                                    element.addEventListener('click', () => {
                                        console.log("图片被点击了");
                                        // 你可以在这里放置自定义的点击事件处理逻辑
                                    });
                                };

                                // 返回 base64 编码的图片数据
                                console.log("替换后的 base64:", base64);
                                resolve(base64);
                            } else {
                                reject("无法加载图片: " + src);
                            }
                        },
                        onerror: function () {
                            reject("请求图片失败: " + src);
                        }
                    });
                }
            } catch (e) {
                reject(e);
            }
        } else {
            console.log("当前是 gif 验证码,图片地址:", src);
            // gif 2 中文 3 英文+ 数字
            GM_xmlhttpRequest({
                method: 'GET',
                url: src,
                responseType: 'arraybuffer',
                onload: function (response) {
                    if (response.status === 200) {
                        // 获取图像的二进制数据
                        const binary = new Uint8Array(response.response);
                        // 将二进制数据转成 base64 编码
                        const base64 = "data:image/gif;base64," + btoa(String.fromCharCode.apply(null, binary));

                        // 保存原始的事件处理器
                        const clickEvent = element.onclick;

                        // 替换 img 元素的 src 并触发图像加载
                        element.src = base64;

                        // 确保图片加载完成后执行其他操作
                        element.onload = function() {
                            console.log("图片已更新并加载完毕");

                            // 重新绑定原始的点击事件
                            if (clickEvent) {
                                element.onclick = clickEvent;
                            }

                            // 你可以在这里添加其他的事件处理逻辑
                            element.addEventListener('click', () => {
                                console.log("图片被点击了");
                                // 你可以在这里放置自定义的点击事件处理逻辑
                            });
                        };

                        // 返回 base64 编码的图片数据
                        console.log("替换后的 base64:", base64);
                        resolve(base64);
                    } else {
                        reject("无法加载图片: " + src);
                    }
                },
                onerror: function () {
                    reject("请求图片失败: " + src);
                }
            });
        }
    });
}

// 发送图片到服务器进行 OCR 识别
function sendImageForOCR(imgElement, base64) {
    console.log("准备发送图片进行识别,图片 URL:", imgElement.src);
    console.log("准备发送图片进行识别,图片 base64:", base64);

    try {

        // 创建 JSON 对象
        const requestData = {
            image: base64
        };
        GM_xmlhttpRequest({
            method: "POST",
            url: "https:/api.xiaojingjing.top:8443/ocr",
            headers: {
                "accept": "application/json",
                "Content-Type": "application/json",
                "User-Agent": navigator.userAgent
            },
            data: JSON.stringify(requestData),
            onload: function (response) {
                console.log('识别结果:', response.responseText);
                const jsonResponse = JSON.parse(response.responseText);
                // 检查 code 是否为 200
                if (response.status === 200) {
                    const inputElement = findClosestInput(imgElement);
                    if (inputElement) {
                        // 关闭输入框的验证
                        disableInputValidation(inputElement);
                        // 获取识别结果
                        const captchaText = jsonResponse.result;
                        console.log('识别结果:', captchaText);

                        // 判断是否包含计算表达式
                        if (imgElement.src.includes("type=1")) {
                            // 如果包含数学运算,进行计算
                            const result = evaluateMathExpression(captchaText);
                            console.log('计算结果:', result);
                            inputElement.value = result;  // 填充计算结果
                            // 在页面上展示识别结果的弹框
                            showCaptchaResultPopup(captchaText.replace('=', '') + '=' + result);
                        } else {
                            // 否则,直接填充识别结果
                            inputElement.value = captchaText;

                            if(imgElement.src.includes("type=3")||!imgElement.src.includes("type")){
                                // 在页面上展示识别结果的弹框
                                showCaptchaResultPopup(captchaText+"\n动态验证码识别率较低,如有错误,请点击图片重试!");
                            }else{
                                // 在页面上展示识别结果的弹框
                                showCaptchaResultPopup(captchaText);
                            }

                        }
                        // 手动触发 input 事件,以便页面检测到输入的变化
                        const event = new Event('input', {bubbles: true});
                        inputElement.dispatchEvent(event);
                        // 如果还有其他事件检测输入,可以一并触发
                        const changeEvent = new Event('change', {bubbles: true});
                        inputElement.dispatchEvent(changeEvent);
                        console.log('验证码已填充至输入框:', inputElement.value);
                    } else {
                        console.log('未查询到输入框!');
                    }
                } else {
                    console.error('识别错误!:', jsonResponse);
                }
            }, onerror: function (error) {
                console.error('识别错误:', error);
            }
        });
    } catch (error) {
        console.error("识别异常:", error);
    }
}


// 计算数学表达式
function evaluateMathExpression(expression) {
    try {
        // 使用 JavaScript 的 eval 函数计算表达式
        // 移除等号并计算
        return eval(expression.replace('=', '').replace('x', '*').replace('X', '*').replace('÷', '/').trim());
    } catch (e) {
        console.error('计算错误:', e);
        return '';
    }
}

// 禁用输入框的验证
function disableInputValidation(inputElement) {
    inputElement.removeAttribute('required');
    inputElement.removeAttribute('minlength');
    inputElement.removeAttribute('maxlength');
    inputElement.removeAttribute('pattern');
    // 移除错误提示标题
    inputElement.removeAttribute('title');
    // 如果输入框是属于一个表单,禁用整个表单的验证
    const form = inputElement.closest('form');
    if (form) {
        // 禁用表单验证
        form.setAttribute('novalidate', 'true');
    }
}

// 给图片添加左键点击事件
function addImageClickListener(imgElement) {
    if (!imgElement) return;
    // 给图片添加左键点击事件
    imgElement.addEventListener('click', function (e) {
        // 确保是左键点击
        if (e.button === 0) {
            // 左键点击
            console.log('点击了图片:', imgElement.src);
            processImageCaptcha(imgElement);
        }
    });
}

// 定义从 img 元素开始逐级向上查找最近的 input 元素的函数
function findClosestInput(element) {
    while (element) {
        element = element.parentElement;
        if (!element) break;
        const inputs = element.getElementsByTagName('input');
        if (inputs.length > 0) {
            // 返回找到的最后一个 input 元素
            return inputs[inputs.length - 1];
        }
    }
    return null;
}

// 检查页面上的验证码图像,并触发识别
function checkForCaptchaImages() {
    // 选择所有图片验证码元素
    $("canvas,img,input[type='image']").each(function () {
        // 确保是可见的图片,并且没有处理过
        const imgElement = this;
        if ($(imgElement).is(":visible") && !$(imgElement).data('processed')) {
            const imgSrc = imgElement.src;
            // 只处理包含 'api-uaa/v1/validate/code' 的图片
            if (imgSrc && imgSrc.includes('api-uaa/v1/validate/code')) {
                // 标记为已处理
                $(imgElement).data('processed', true);
                // 给图片添加左键点击事件
                addImageClickListener(imgElement);
                // 进行识别
                processImageCaptcha(imgElement);
            }
        }
    });
}


// 监听图片的变化,确保图像更新后重新触发识别
function observeImageChanges() {
    const observer = new MutationObserver(() => {
        checkForCaptchaImages();
    });
    // 监听 img 和 canvas 元素的变化
    observer.observe(document.body, {
        childList: true, subtree: true,
    });
}

// 开始监听图像变化
observeImageChanges();