Greasy Fork is available in English.

网页通用验证码

解放眼睛和双手,自动识别并填入数字,字母验证码。新版本支持识别滑动验证码。

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name        网页通用验证码
// @namespace    http://tampermonkey.net/
// @version 3.1.3
// @description  解放眼睛和双手,自动识别并填入数字,字母验证码。新版本支持识别滑动验证码。
// @author       哈士奇

// @include        http://*
// @include        https://*
// @license        MIT

// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_listValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_log
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// @grant        GM_notification
// @grant        GM_setClipboard
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// @connect      *
// @require      https://unpkg.com/[email protected]/dist/vue.js
// @require      https://unpkg.com/element-ui/lib/index.js
// @resource elementUIcss https://unpkg.com/element-ui/lib/theme-chalk/index.css

// @run-at document-end
// ==/UserScript==

(function () {
    // GM_setValue('tipsConfig',"")
    var elementUIcss = GM_getResourceText('elementUIcss');
    GM_addStyle(elementUIcss);

    function getStyle(el) {
        // 获取元素样式
        if (window.getComputedStyle) {
            return window.getComputedStyle(el, null);
        } else {
            return el.currentStyle;
        }
    }

    function init() {
        //简化各种api和初始化全局变量
        CUR_URL = window.location.href;
        DOMAIN = CUR_URL.split('//')[1].split('/')[0];
        SLIDE_STORE_KEY = 'husky_' + 'slidePath' + location.host;
        NORMAL_STORE_KEY = 'husky_' + 'normalPath' + location.host;
        selector = document.querySelector.bind(document);
        selectorAll = document.querySelectorAll.bind(document);
        getItem = localStorage.getItem.bind(localStorage);
        setItem = localStorage.setItem.bind(localStorage);
    }

    function getNumber(str) {
        return Number(str.split('.')[0].replace(/[^0-9]/gi, ''));
    }

    function isNumber(value) {
        if (!value && value !== 0) {
            return false;
        }
        value = Number(value);
        return typeof value === 'number' && !isNaN(value);
    }

    function getEleTransform(el) {
        const style = window.getComputedStyle(el, null);
        var transform =
            style.getPropertyValue('-webkit-transform') ||
            style.getPropertyValue('-moz-transform') ||
            style.getPropertyValue('-ms-transform') ||
            style.getPropertyValue('-o-transform') ||
            style.getPropertyValue('transform') ||
            'null';
        return transform && transform.split(',')[4];
    }

    class Captcha {
        // 识别网页中的验证码
        constructor() {
            this.imgCache = [];
            this.inputTags = [];
            this.recommendPath = {};
            this.checkTimer = null;
            this.listenLoadSuccess = false;

            window.addEventListener('load', async () => {
                this.listenLoadSuccess = true;
                this.init();
            });
            setTimeout(() => {
                if (!this.listenLoadSuccess) {
                    this.listenLoadSuccess = true;
                    this.init();
                }
            }, 5000);
        }

        doCheckTask() {
            this.findCaptcha();
            this.checkSlideCaptcha();
        }
        init() {
            if (blackListCheck()) {
                return;
            }
            this.manualLocateCaptcha();
            this.doCheckTask();

            const MutationObserver =
                window.MutationObserver ||
                window.WebKitMutationObserver ||
                window.MozMutationObserver;
            const body = document.body;

            const Observer = new MutationObserver((mutations, instance) => {
                if (blackListCheck()) {
                    return;
                }
                for (let i = 0; i < mutations.length; i++) {
                    const el = mutations[i].target;
                    const tagName = mutations[i].target.tagName.toLowerCase();
                    let checkList = [];
                    checkList.push(el.getAttribute('id'));
                    checkList.push(el.className);
                    checkList.push(el.getAttribute('alt'));
                    checkList.push(el.getAttribute('src'));
                    checkList.push(el.getAttribute('name'));
                    checkList = checkList.filter((item) => item);

                    for (let x = 0; x < checkList.length; x++) {
                        if (
                            /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma|滑块|拖动|拼图|yidun|slide).*/im.test(
                                checkList[x].toString().toLowerCase()
                            ) ||
                            tagName === 'img' ||
                            tagName === 'iframe'
                        ) {
                            if (!this.checkTimer) {
                                this.checkTimer = setTimeout(() => {
                                    this.doCheckTask();
                                }, 0);
                            } else {
                                window.clearTimeout(this.checkTimer);
                                this.checkTimer = setTimeout(() => {
                                    this.doCheckTask();
                                }, 2000);
                            }
                            return;
                        }
                    }
                }
            });
            Observer.observe(body, {
                childList: true,
                subtree: true,
                attributes: true,
            });
        }
        dataURLtoFile(dataURL, filename = 'captcha.jpg') {
            //  base64转图片文件
            var arr = dataURL.split(','),
                mime =
                    (arr[0].match(/:(.*?);/) && arr[0].match(/:(.*?);/)[1]) ||
                    'image/png',
                bstr = atob(arr[1]),
                n = bstr.length,
                u8arr = new Uint8Array(n);
            while (n--) {
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, { type: mime });
        }
        async getRecommendPath() {
            let requestUrl =
                'http://101.43.206.185:7000/cssPath?href=' +
                location.href.split('?')[0];
            try {
                GM_xmlhttpRequest({
                    method: 'get',
                    url: requestUrl,
                    onload: async (res) => {
                        if (res.status === 200 && res.response) {
                            let data =
                                (res.response && JSON.parse(res.response)) ||
                                {};
                            const { path, recommendTimes = 0 } = data;
                            if (path && recommendTimes) {
                                let inputSelector = path.split('$$')[0];
                                let imgSelector = path.split('$$')[1];
                                if (
                                    selector(inputSelector) &&
                                    selector(imgSelector) &&
                                    selector(imgSelector).getAttribute('src') &&
                                    selector(inputSelector).getAttribute(
                                        'type'
                                    ) === 'text'
                                ) {
                                    let dataURL = await this.handleImg(
                                        selector(imgSelector)
                                    );
                                    try {
                                        if (
                                            !this.hasRequest(dataURL, {
                                                record: true,
                                            })
                                        ) {
                                            let code = await this.request(
                                                this.dataURLtoFile(dataURL),
                                                this.cssPath(
                                                    selector(inputSelector)
                                                ) +
                                                    '$$' +
                                                    this.cssPath(
                                                        selector(imgSelector)
                                                    ),
                                                selector(
                                                    imgSelector
                                                ).getAttribute('src')
                                            );
                                            if (code) {
                                                selector(inputSelector).value =
                                                    code;
                                                if (
                                                    typeof Vue !== 'undefined'
                                                ) {
                                                    new Vue().$message.success(
                                                        '获取验证码成功'
                                                    );
                                                }
                                                console.log(
                                                    '正在使用共享验证码功能获取验证码'
                                                );
                                            } else {
                                                console.error(
                                                    '验证码为空,请检查图片是否正确'
                                                );
                                            }
                                        }
                                    } catch (error) {
                                        console.log(error);
                                        // if (typeof Vue !== "undefined") {
                                        //     new Vue().$message.error("获取验证码失败");
                                        // }
                                    }
                                }
                            }
                        }
                    },
                    onerror: function (err) {
                        console.log('推荐路径请求失败:' + err);
                    },
                });
            } catch (error) {
                console.log(error);
            }
        }
        getCaptchaFeature(el) {
            // 获取验证码特征
            let checkList = [];
            checkList.push(el.getAttribute('id'));
            checkList.push(el.className);
            checkList.push(el.getAttribute('alt'));
            checkList.push(el.getAttribute('src'));
            checkList.push(el.getAttribute('name'));

            return checkList;
        }
        cssPath = (el) => {
            // 获取元素css path
            if (!(el instanceof Element)) return;
            var path = [];
            while (el.nodeType === Node.ELEMENT_NODE) {
                var selector = el.nodeName.toLowerCase();
                if (el.id) {
                    selector += '#' + el.id;
                    path.unshift(selector);
                    break;
                } else {
                    var sib = el,
                        nth = 1;
                    while ((sib = sib.previousElementSibling)) {
                        if (sib.nodeName.toLowerCase() == selector) nth++;
                    }
                    if (nth != 1) selector += ':nth-of-type(' + nth + ')';
                }
                path.unshift(selector);
                el = el.parentNode;
            }
            return path.join(' > ');
        };

        manualLocateCaptcha() {
            let imgs = [];
            let inputTags = [];
            let cssPathStore = {};
            let finish = false;
            this.vue = new Vue();
            this.isIframe = top !== self;
            var onTagClick = (e) => {
                let el = e.target;
                let tagName = el.tagName;
                if (tagName.toLowerCase() === 'input') {
                    let type = el.getAttribute('type');
                    if (type && type !== 'text') {
                        this.vue.$message.error(
                            '提醒:当前点击输入框type=' +
                                type +
                                ',请选择文本输入框'
                        );
                    } else {
                        cssPathStore.input = this.cssPath(el);
                        this.vue.$message.success('您已成功选择输入框');
                    }
                } else {
                    cssPathStore.img = this.cssPath(el);
                    this.vue.$message.success('您已成功选择验证码图片');
                }
                if (cssPathStore.input && cssPathStore.img) {
                    GM_setValue(NORMAL_STORE_KEY, JSON.stringify(cssPathStore));
                    imgs.forEach((img) => {
                        img && img.removeEventListener('click', onTagClick);
                    }, false);
                    inputTags.forEach((input) => {
                        input.removeEventListener('click', onTagClick);
                    }, false);
                    setTimeout(() => {
                        this.vue.$message.success('选择完毕,赶快试试吧');
                        captchaInstance.doCheckTask();
                    }, 3000);
                    finish = true;
                }
            };
            var onMenuClick = (e) => {
                if (this.isIframe) {
                    alert(
                        '当前脚本处于iframe中,暂不支持该操作,快让作者优化吧'
                    );
                    return;
                }
                finish = false;
                cssPathStore = {};
                GM_deleteValue(NORMAL_STORE_KEY);
                this.vue.$alert('接下来请点击验证码图片和输入框', '操作提示', {
                    confirmButtonText: '确定',
                    callback: () => {
                        setTimeout(() => {
                            imgs.forEach((img) => {
                                img &&
                                    img.removeEventListener(
                                        'click',
                                        onTagClick
                                    );
                            }, false);
                            inputTags.forEach((input) => {
                                input.removeEventListener('click', onTagClick);
                            }, false);
                            if (!finish) {
                                this.vue.$notify.success({
                                    title: '提示',
                                    message: '已退出手动选择验证码模式。',
                                    offset: 100,
                                });
                            }
                        }, 20000);
                    },
                });

                // alert("请点击验证码和输入框各一次。");
                imgs = [...selectorAll('img')];
                inputTags = [...selectorAll('input')];
                imgs.forEach((img) => {
                    img.addEventListener('click', onTagClick);
                }, false);
                inputTags.forEach((input) => {
                    input.addEventListener('click', onTagClick);
                }, false);
            };
            GM_registerMenuCommand('手动选择验证码和输入框', onMenuClick);
        }
        handleImg(img) {
            return new Promise((resolve, reject) => {
                try {
                    // 图片没设置跨域,可采用图片转canvas转base64的方式
                    let dataURL = null;

                    const action = () => {
                        let canvas = document.createElement('canvas');
                        canvas.width = img.naturalWidth;
                        canvas.height = img.naturalHeight;
                        let ctx = canvas.getContext('2d');
                        ctx.drawImage(
                            img,
                            0,
                            0,
                            img.naturalWidth,
                            img.naturalHeight
                        );
                        dataURL = canvas.toDataURL('image/png');
                        resolve(dataURL);
                    };
                    if (!img.src.includes(';base64,')) {
                        img.onload = function () {
                            action();
                        };
                        if (img.complete) {
                            action();
                        } else {
                            img.onload = function () {
                                action();
                            };
                        }
                    } else {
                        dataURL = img.src;
                        resolve(dataURL);
                    }
                } catch (error) {
                    console.error('error:' + error);
                    // 这块处理比较复杂,待优化
                    // 图片设置跨域,重新请求图片内容后转base64,相当于替用户点击了“换一张图片”
                    // if (this.times >= 1) {
                    //   return;
                    // }
                    // if (typeof Vue !== "undefined") {
                    //     new Vue().$notify.success({
                    //         title: "温馨提示",
                    //         message: "当前验证码结果可能和图片显示不一致,请放心提交。",
                    //         offset: 100,
                    //     });
                    // }

                    // this.times++;
                    // GM_xmlhttpRequest({
                    //     method: "get",
                    //     url: img.src,
                    //     responseType: "blob",
                    //     onload: (res) => {
                    //         if (res.status === 200) {
                    //             let blob = res.response;
                    //             let fileReader = new FileReader();
                    //             fileReader.onloadend = (e) => {
                    //                 let base64 = e.target.result;
                    //                 resolve(base64);
                    //             };
                    //             fileReader.readAsDataURL(blob);
                    //         } else {
                    //             console.log("图片转换blob失败");
                    //             console.log(res);
                    //             reject();
                    //         }
                    //     },
                    //     onerror: function(err) {
                    //         console.log("图片请求失败:" + err);
                    //         reject();
                    //     },
                    // });
                }
            });
        }
        hasRequest(dataURL, config = {}) {
            let startIndex = config.type === 'url' ? 0 : dataURL.length - 100;
            let imgClips = dataURL.slice(startIndex, dataURL.length);
            if (this.imgCache.includes(imgClips)) {
                return true;
            }
            if (config.record) {
                this.imgCache.push(imgClips);
            }
            return false;
        }
        request(file, path, src) {
            try {
                if (!file) {
                    console.error('缺少file参数');
                    return Promise.reject();
                }

                return new Promise((resolve, reject) => {
                    let host = location.href;
                    let href = location.href.split('?')[0].split('#')[0];
                    if (self === top) {
                        host = location.host;
                    }
                    let formData = new FormData();
                    let detail = {
                        path,
                        src,
                        host,
                        href,
                    };
                    formData.append('img', file);
                    formData.append('detail', JSON.stringify(detail));
                    // let requestUrl = "http://192.168.31.184:7000/captcha";
                    let requestUrl = 'http://101.43.206.185:7000/captcha';
                    GM_xmlhttpRequest({
                        method: 'post',
                        url: requestUrl,
                        data: formData,
                        onload: function (response) {
                            if (response.status === -1) {
                                console.error('获取验证码失败:' + response);
                                reject();
                            } else {
                                let data = response.response;
                                if (data.length < 50) {
                                    data = JSON.parse(data);
                                    if (data.code) {
                                        resolve(data.code);
                                    } else {
                                        let date = new Date().getDate();
                                        let tipsConfig = {
                                            date,
                                            times: 1,
                                        };
                                        let cache =
                                            GM_getValue('tipsConfig') &&
                                            JSON.parse(
                                                GM_getValue('tipsConfig')
                                            );
                                        if (cache && cache.times > 3) {
                                        } else {
                                            if (!cache) {
                                                GM_setValue(
                                                    'tipsConfig',
                                                    JSON.stringify(tipsConfig)
                                                );
                                            } else {
                                                cache.times = cache.times + 1;
                                                GM_setValue(
                                                    'tipsConfig',
                                                    JSON.stringify(cache)
                                                );
                                            }
                                            if (typeof Vue !== 'undefined') {
                                                new Vue().$message.error(
                                                    data.msg
                                                );
                                            }
                                        }

                                        console.error('获取验证码失败:', data);
                                        reject();
                                    }
                                } else {
                                    console.error('获取验证码失败:', response);
                                    console.dir(data);
                                    reject();
                                }
                            }
                        },
                        onerror: function (err) {
                            console.error(err);
                            reject();
                        },
                    });
                });
            } catch (error) {
                console.log(error);
            }
        }
        async findCaptcha() {
            // 先读取用户手动设置的验证码配置
            let cache = GM_getValue(NORMAL_STORE_KEY);
            let captchaPath = cache && JSON.parse(cache);
            if (
                captchaPath &&
                captchaPath.input &&
                captchaPath.img &&
                selector(captchaPath.input) &&
                selector(captchaPath.img)
            ) {
                let dataURL = await this.handleImg(selector(captchaPath.img));
                try {
                    if (!this.hasRequest(dataURL, { record: true })) {
                        let code = await this.request(
                            this.dataURLtoFile(dataURL),
                            this.cssPath(selector(captchaPath.input)) +
                                '$$' +
                                this.cssPath(selector(captchaPath.img)),
                            selector(captchaPath.img).getAttribute('src')
                        );
                        if (code) {
                            let input = selector(captchaPath.input);
                            input.value = code.trim();

                            // 触发 input 和 change 事件
                            let inputEvent = new Event('input', {
                                bubbles: true,
                            });
                            let changeEvent = new Event('change', {
                                bubbles: true,
                            });
                            input.dispatchEvent(inputEvent);
                            input.dispatchEvent(changeEvent);

                            console.log(
                                '正在使用用户自定义验证码位置数据获取验证码'
                            );
                            return;
                        } else {
                            console.error('验证码为空,请检查图片是否正确');
                        }
                    }
                } catch (error) {
                    console.log(error);
                }
                return;
            }

            // 自动寻找验证码和输入框
            let captchaMap = [];
            let imgs = [...selectorAll('img')];
            imgs.forEach((img) => {
                let checkList = [
                    ...this.getCaptchaFeature(img),
                    ...this.getCaptchaFeature(img.parentNode),
                ];
                checkList = checkList.filter((item) => item);
                let isInvalid =
                    ['#', 'about:blank'].includes(img.getAttribute('src')) ||
                    !img.getAttribute('src');

                for (let i = 0; i < checkList.length; i++) {
                    if (
                        /.*(code|captcha|验证码|login|点击|verify|yzm|yanzhengma).*/im.test(
                            checkList[i].toLowerCase()
                        ) &&
                        img.width > 30 &&
                        img.width < 150 &&
                        img.height < 80 &&
                        !isInvalid
                    ) {
                        captchaMap.push({ img: img, input: null });
                        break;
                    }
                }
            });
            captchaMap.forEach((item) => {
                let imgEle = item.img;
                let parentNode = imgEle.parentNode;
                for (let i = 0; i < 4; i++) {
                    // 以当前可能是验证码的图片为基点,向上遍历四层查找可能的Input输入框
                    if (!parentNode) {
                        return;
                    }
                    let inputTags = [...parentNode.querySelectorAll('input')];
                    if (inputTags.length) {
                        let input = inputTags.pop();
                        let type = input.getAttribute('type');
                        while (type !== 'text' && inputTags.length) {
                            if (type === 'password') {
                                break;
                            }
                            input = inputTags.pop();
                            type = input.getAttribute('type');
                        }
                        let inputWidth = getStyle(input).width.replace(
                            /[^0-9]/gi,
                            ''
                        );
                        // let inputHeight = getStyle(input).height.replace(/[^0-9]/gi, "");
                        if (!type || (type === 'text' && inputWidth > 50)) {
                            // 兼容各种奇葩情况
                            item.input = input;
                            break;
                        }
                        if (type === 'password') {
                            // 验证码一般在密码框后面,遍历到密码框了就大概率说明没有验证码
                            break;
                        }
                    }
                    parentNode = parentNode.parentNode;
                }
            });

            if (!captchaMap.length) {
                const { path, recommendTimes } = this.recommendPath;
                if (path) {
                    let inputSelector = path.split('$$')[0];
                    let imgSelector = path.split('$$')[1];
                    if (selector(inputSelector) && selector(imgSelector)) {
                        let dataURL = await this.handleImg(
                            selector(imgSelector)
                        );
                        try {
                            if (!this.hasRequest(dataURL, { record: true })) {
                                let input = selector(inputSelector);
                                input.value = await this.request(
                                    this.dataURLtoFile(dataURL),
                                    path,
                                    item.img.getAttribute('src')
                                );

                                // 触发 input 和 change 事件
                                let inputEvent = new Event('input', {
                                    bubbles: true,
                                });
                                let changeEvent = new Event('change', {
                                    bubbles: true,
                                });
                                input.dispatchEvent(inputEvent);
                                input.dispatchEvent(changeEvent);

                                if (typeof Vue !== 'undefined') {
                                    new Vue().$message.success(
                                        '获取验证码成功'
                                    );
                                }
                            }
                        } catch (error) {
                            console.log(error);
                        }
                    }
                }
            }

            captchaMap = captchaMap.filter((item) => item.input);
            captchaMap.forEach(async (item, index) => {
                let dataURL = await this.handleImg(item.img);
                try {
                    if (!this.hasRequest(dataURL, { record: true })) {
                        let code = await this.request(
                            this.dataURLtoFile(dataURL),
                            this.cssPath(item.input) +
                                '$$' +
                                this.cssPath(item.img),
                            item.img.getAttribute('src')
                        );
                        if (code) {
                            let input = item.input;
                            input.value = code;

                            // 触发 input 和 change 事件
                            let inputEvent = new Event('input', {
                                bubbles: true,
                            });
                            let changeEvent = new Event('change', {
                                bubbles: true,
                            });
                            input.dispatchEvent(inputEvent);
                            input.dispatchEvent(changeEvent);

                            if (typeof Vue !== 'undefined') {
                                new Vue().$message.success('获取验证码成功');
                            }
                            console.log('正在使用自动寻找验证码功能获取验证码');
                        } else {
                            if (index === captchaMap.length - 1) {
                                this.getRecommendPath();
                            }
                            console.error('验证码为空,请检查图片是否正确');
                        }
                    }
                } catch (error) {
                    if (index === captchaMap.length - 1) {
                        this.getRecommendPath();
                    }
                    console.log(error);
                }
            });
        }
        getImgViaBlob(url) {
            return new Promise((resolve, reject) => {
                try {
                    GM_xmlhttpRequest({
                        method: 'get',
                        url,
                        responseType: 'blob',
                        onload: (res) => {
                            if (res.status === 200) {
                                let blob = res.response;
                                let fileReader = new FileReader();
                                fileReader.onloadend = (e) => {
                                    let base64 = e.target.result;
                                    if (base64.length > 20) {
                                        resolve(base64);
                                    } else {
                                        alert(
                                            '验证码助手:当前网站验证码图片禁止跨域访问,待作者优化。'
                                        );
                                        handleClearMenuClick();
                                        reject('base64图片长度不够');
                                        throw 'getImgViaBlob: base64图片长度不够';
                                    }
                                };
                                fileReader.readAsDataURL(blob);
                            } else {
                                console.log('图片转换blob失败');
                                console.log(res);
                                reject();
                            }
                        },
                        onerror: function (err) {
                            console.log('图片请求失败:' + err);
                            reject();
                        },
                    });
                } catch (error) {
                    console.log(error);
                    reject();
                }
            });
        }
        elDisplay(el) {
            if (!el) {
                return false;
            }

            while (el) {
                if (!(el instanceof Element)) {
                    return true;
                }
                if (getStyle(el).display === 'none') {
                    return false;
                }
                el = el.parentNode;
            }
            return true;
        }
        checkSlideCaptcha() {
            const check = async () => {
                const slideCache =
                    (GM_getValue(SLIDE_STORE_KEY) &&
                        JSON.parse(GM_getValue(SLIDE_STORE_KEY))) ||
                    {};
                const { bgImg, targetImg, moveItem } = slideCache;
                if (
                    bgImg &&
                    targetImg &&
                    moveItem &&
                    selector(targetImg) &&
                    selector(bgImg) &&
                    selector(moveItem) &&
                    this.elDisplay(selector(targetImg)) &&
                    this.elDisplay(selector(bgImg)) &&
                    this.elDisplay(selector(moveItem))
                ) {
                    const target_url =
                        selector(targetImg).getAttribute('src') ||
                        getStyle(selector(targetImg))['background-image'].split(
                            '"'
                        )[1];
                    const bg_url =
                        selector(bgImg).getAttribute('src') ||
                        getStyle(selector(bgImg))['background-image'].split(
                            '"'
                        )[1];
                    if (
                        !this.hasRequest(target_url, {
                            record: true,
                            type: 'url',
                        })
                    ) {
                        const target_base64 = await this.getImgViaBlob(
                            target_url
                        );
                        const bg_base64 = await this.getImgViaBlob(bg_url);
                        return new Promise(async (resolve, reject) => {
                            let host = location.href;
                            let href = location.href
                                .split('?')[0]
                                .split('#')[0];
                            if (self === top) {
                                host = location.host;
                            }
                            let detail = {
                                path: slideCache,
                                host,
                                href,
                            };
                            let formData = new FormData();
                            let requestUrl =
                                'http://101.43.206.185:7000/slideCaptcha';
                            let targetWidth = getNumber(
                                getStyle(selector(targetImg)).width
                            );
                            let bgWidth = getNumber(
                                getStyle(selector(bgImg)).width
                            );
                            formData.append(
                                'target_img',
                                this.dataURLtoFile(target_base64)
                            );
                            formData.append(
                                'bg_img',
                                this.dataURLtoFile(bg_base64)
                            );
                            formData.append('targetWidth', targetWidth);
                            formData.append('bgWidth', bgWidth);
                            formData.append('detail', JSON.stringify(detail));
                            GM_xmlhttpRequest({
                                method: 'post',
                                url: requestUrl,
                                data: formData,
                                onload: (response) => {
                                    const data = JSON.parse(response.response);
                                    this.moveSideCaptcha(
                                        selector(targetImg),
                                        selector(moveItem),
                                        data.result.target[0]
                                    );
                                    // resolve()
                                },
                                onerror: function (err) {
                                    console.error(err);
                                    reject();
                                },
                            });
                        });
                    }
                }
            };
            check();
            // const interval = 3000;
            // simulateInterval(check, interval);
        }
        moveSideCaptcha(targetImg, moveItem, distance) {
            if (distance === 0) {
                console.log('distance', distance);
                return;
            }
            var btn = moveItem;
            let target = targetImg;

            let varible = null;
            let targetLeft =
                Number(getStyle(target).left.replace('px', '')) || 0;
            let targetParentLeft =
                Number(getStyle(target.parentNode).left.replace('px', '')) || 0;
            let targetTransform = Number(getEleTransform(target)) || 0;
            let targetParentTransform =
                Number(getEleTransform(target.parentNode)) || 0;

            var mousedown = document.createEvent('MouseEvents');
            var rect = btn.getBoundingClientRect();
            var x = rect.x;
            var y = rect.y;
            mousedown.initMouseEvent(
                'mousedown',
                true,
                true,
                document.defaultView,
                0,
                x,
                y,
                x,
                y,
                false,
                false,
                false,
                false,
                0,
                null
            );
            btn.dispatchEvent(mousedown);

            var dx = 0;
            var dy = 0;
            var interval = setInterval(function () {
                var mousemove = document.createEvent('MouseEvents');
                var _x = x + dx;
                var _y = y + dy;
                mousemove.initMouseEvent(
                    'mousemove',
                    true,
                    true,
                    document.defaultView,
                    0,
                    _x,
                    _y,
                    _x,
                    _y,
                    false,
                    false,
                    false,
                    false,
                    0,
                    null
                );
                btn.dispatchEvent(mousemove);
                btn.dispatchEvent(mousemove);

                let newTargetLeft =
                    Number(getStyle(target).left.replace('px', '')) || 0;
                let newTargetParentLeft =
                    Number(
                        getStyle(target.parentNode).left.replace('px', '')
                    ) || 0;
                let newTargetTransform = Number(getEleTransform(target)) || 0;
                let newTargetParentTransform =
                    Number(getEleTransform(target.parentNode)) || 0;

                if (newTargetLeft !== targetLeft) {
                    varible = newTargetLeft;
                } else if (newTargetParentLeft !== targetParentLeft) {
                    varible = newTargetParentLeft;
                } else if (newTargetTransform !== targetTransform) {
                    varible = newTargetTransform;
                } else if (newTargetParentTransform != targetParentTransform) {
                    varible = newTargetParentTransform;
                }
                if (varible >= distance) {
                    clearInterval(interval);
                    var mouseup = document.createEvent('MouseEvents');
                    mouseup.initMouseEvent(
                        'mouseup',
                        true,
                        true,
                        document.defaultView,
                        0,
                        _x,
                        _y,
                        _x,
                        _y,
                        false,
                        false,
                        false,
                        false,
                        0,
                        null
                    );
                    setTimeout(() => {
                        btn.dispatchEvent(mouseup);
                    }, Math.ceil(Math.random() * 2000));
                } else {
                    if (dx >= distance - 20) {
                        dx += Math.ceil(Math.random() * 2);
                    } else {
                        dx += Math.ceil(Math.random() * 10);
                    }
                    let sign = Math.random() > 0.5 ? -1 : 1;
                    dy += Math.ceil(Math.random() * 3 * sign);
                }
            }, 10);
            setTimeout(() => {
                clearInterval(interval);
            }, 10000);
        }
    }

    function getEleCssPath(el) {
        // 获取元素css path
        if (!(el instanceof Element)) return;
        var path = [];
        while (el.nodeType === Node.ELEMENT_NODE) {
            var selector = el.nodeName.toLowerCase();
            if (el.id) {
                selector += '#' + el.id;
                path.unshift(selector);
                break;
            } else {
                var sib = el,
                    nth = 1;
                while ((sib = sib.previousElementSibling)) {
                    if (sib.nodeName.toLowerCase() == selector) nth++;
                }
                if (nth != 1) selector += ':nth-of-type(' + nth + ')';
            }
            path.unshift(selector);
            el = el.parentNode;
        }
        return path.join(' > ');
    }

    function handleSlideMenuClick({ isPostmessage } = {}) {
        if (top === self) {
            alert('请点击滑动验证码的大图片,小图片,滑块。');
        }
        this.vue = new Vue();
        this.isIframe = top !== self;
        GM_deleteValue(SLIDE_STORE_KEY);

        let imgs = [...selectorAll('img')];
        let divTags = [...selectorAll('div')];
        imgs.forEach((img) => {
            img.addEventListener('click', onSlideTagClick);
        }, false);
        divTags.forEach((input) => {
            input.addEventListener('click', onSlideTagClick);
        }, false);

        setTimeout(() => {
            imgs.forEach((img) => {
                img && img.removeEventListener('click', onSlideTagClick);
            }, false);
            divTags.forEach((input) => {
                input.removeEventListener('click', onSlideTagClick);
            }, false);
        }, 30000);

        if (!isPostmessage) {
            if (self === top) {
                const iframes = [...selectorAll('iframe')];
                iframes.forEach((iframe) => {
                    iframe.contentWindow.postMessage(
                        {
                            sign: 'husky',
                            action: 'handleSlideMenuClick',
                        },
                        '*'
                    );
                });
            } else {
                window.postMessage(
                    {
                        sign: 'husky',
                        action: 'handleSlideMenuClick',
                    },
                    '*'
                );
            }
        }
    }

    let noticeTimer = 0;

    function notice(msg) {
        if (noticeTimer) {
            clearTimeout(noticeTimer);
        } else {
            setTimeout(() => new Vue().$message.success(msg));
        }
        noticeTimer = setTimeout(() => new Vue().$message.success(msg), 1000);
    }

    var onSlideTagClick = (e) => {
        let el = e.target;
        let tagName = el.tagName.toLowerCase();
        let width = Number(getNumber(getStyle(el).width)) || 0;
        const vue = new Vue();
        let height = Number(getNumber(getStyle(el).height)) || 0;
        let position = getStyle(el).position;
        let pathCache =
            (GM_getValue(SLIDE_STORE_KEY) &&
                JSON.parse(GM_getValue(SLIDE_STORE_KEY))) ||
            {};
        if (tagName === 'img') {
            if (width >= height && width > 150) {
                let newValue = { ...pathCache, bgImg: getEleCssPath(el) };
                GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
                pathCache = newValue;
                notice('您已成功选择大图片');
            } else if (width < 100 && height >= width - 5) {
                let newValue = { ...pathCache, targetImg: getEleCssPath(el) };
                GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
                pathCache = newValue;
                notice('您已成功选择小图片');
            }
        } else {
            let curEl = el;
            for (let i = 0; i < 3; i++) {
                if (!curEl || curEl === Window) {
                    break;
                }
                position = getStyle(curEl).position;
                let bgUrl = getStyle(curEl)['backgroundImage'];
                width = Number(getNumber(getStyle(curEl).width)) || 0;
                height = Number(getNumber(getStyle(curEl).height)) || 0;

                if (position === 'absolute' && width < 100 && height < 100) {
                    let newValue = {
                        ...pathCache,
                        moveItem: getEleCssPath(curEl),
                    };
                    GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
                    pathCache = newValue;
                    notice('您已成功选择滑块');
                    break;
                }
                let reg = /url\("(.+)"\)/im;

                if (bgUrl && bgUrl.match(reg)) {
                    if (width >= height && width > 150) {
                        let newValue = {
                            ...pathCache,
                            bgImg: getEleCssPath(curEl),
                        };
                        GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
                        pathCache = newValue;
                        notice('您已成功选择大图片');
                        break;
                    } else if (width < 100 && height >= width - 5) {
                        let newValue = {
                            ...pathCache,
                            targetImg: getEleCssPath(curEl),
                        };
                        GM_setValue(SLIDE_STORE_KEY, JSON.stringify(newValue));
                        pathCache = newValue;
                        notice('您已成功选择小图片');
                        break;
                    }
                }
                curEl = curEl.parentNode;
            }

            curEl = el;
            const firstImg = curEl.querySelector('img');
            firstImg && onSlideTagClick({ target: firstImg });
        }

        const finish =
            Object.keys(pathCache).filter((item) => item).length == 3;
        if (finish) {
            let imgs = [...selectorAll('img')];
            let divTags = [...selectorAll('div')];
            imgs.forEach((img) => {
                img && img.removeEventListener('click', onSlideTagClick);
            }, false);
            divTags.forEach((div) => {
                div.removeEventListener('click', onSlideTagClick);
            }, false);
            setTimeout(() => {
                vue.$message.success('选择完毕,赶快试试吧');
                captchaInstance.doCheckTask();
            }, 3000);
        }
    };

    GM_registerMenuCommand('手动定位滑动验证码', handleSlideMenuClick);

    function handleClearMenuClick() {
        GM_listValues().forEach((name) => {
            if (name.includes('husky')) {
                GM_deleteValue(name);
            }
        });
    }

    GM_registerMenuCommand('清空所有验证码配置', handleClearMenuClick);

    function cleanCurrentPage() {
        GM_deleteValue(SLIDE_STORE_KEY);
        GM_deleteValue(NORMAL_STORE_KEY);
    }
    GM_registerMenuCommand('清空当前页面验证码配置', cleanCurrentPage);

    let blackListMenuId = null;

    function blackListCheck() {
        let key = location.host + location.pathname + '_black';
        let data = GM_getValue(key) && JSON.parse(GM_getValue(key));
        if (blackListMenuId) {
            GM_unregisterMenuCommand(blackListMenuId);
        }
        if (data) {
            blackListMenuId = GM_registerMenuCommand(
                '标记当前网站有验证码',
                labelWebsite
            );
        } else {
            blackListMenuId = GM_registerMenuCommand(
                '标记当前网站没有验证码',
                labelWebsite
            );
        }
        return data;
    }

    function labelWebsite() {
        let key = location.host + location.pathname + '_black';
        let data = GM_getValue(key) && JSON.parse(GM_getValue(key));
        if (data) {
            GM_setValue(key, 'false');
        } else {
            GM_setValue(key, 'true');
        }
        notice(
            '操作成功,' +
                (data ? '已标记网站有验证码' : '已标记网站没有验证码')
        );
        if (data) {
            captchaInstance = captchaInstance || new Captcha();
            captchaInstance.init();
        }
        blackListCheck();
    }
    blackListCheck();

    var captchaInstance = null;

    function main() {
        window.addEventListener('DOMContentLoaded', function () {
            init();
            captchaInstance = new Captcha();
        });
    }

    const actions = {
        handleSlideMenuClick: handleSlideMenuClick,
    };

    window.addEventListener(
        'message',
        (event) => {
            const { data = {} } = event || {};
            const { sign, action } = data;
            if (sign === 'husky') {
                if (action && actions[action]) {
                    actions[action]({ isPostmessage: true });
                }
            }
        },
        false
    );
    main();
})();