NopeCHA - Automated reCAPTCHA Solver

AI for Automatic reCAPTCHA Recognition

当前为 2025-04-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NopeCHA - Automated reCAPTCHA Solver
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  AI for Automatic reCAPTCHA Recognition
// @author       You
// @require      https://update.greasyfork.org/scripts/534380/1580029/UserscriptSettings.js
// @match        https://www.google.com/recaptcha/api2/bframe*
// @match        https://www.google.com/recaptcha/api2/anchor*
// @icon         https://nopecha.com/apple-icon-72x72.png
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      api.nopecha.com
// @license      MIT
// ==/UserScript==
const API_ENDPOINT = "https://api.nopecha.com";
const GRID_SIZES = { 1: 1, 0: 3, 2: 4 };

UserscriptSettings.define({
    key: {
        name: "Enter your key",
        default: "",
        title: "",
    },
    // disabled_hosts: {
    //     name: "Disabled Hosts",
    //     default: "",
    //     title: "",
    //     formatter: (val) => val.split(",").map(s => s.trim()).filter(host => /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(host)),
    // },
    solve_delay_time: {
        name: "Delay Solving",
        default: 2000,
        title: "Milliseconds to delay solving.",
    },
    auto_open: {
        name: "Auto-Open",
        default: true,
        title: "Automatically opens CAPTCHA challenges.",
    },
    auto_solve: {
        name: "Auto-Solve",
        default: true,
        title: "Automatically solves CAPTCHA challenges.",
    },
    solve_delay: {
        name: "Delay Solving",
        default: true,
        title: "Adds a delay to avoid detection.",
    },
})
const settings = UserscriptSettings;
const POLL_TIMEOUT = 60000;
const MAX_ATTEMPTS = 30;

const eventQueue = [], eventHandlers = [];

let checkboxObserver, intersectionObserver, captchaObserver,
    isRecaptchaActive = false, isCaptchaActive = false;

async function solveCaptcha(params) {
    for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
        try {
            const response = await apiRequest(API_ENDPOINT, {
                method: 'POST',
                data: { ...params, type: 'recaptcha' }
            });
            if (!response.error) return pollCaptchaResult(response.data);
            if ([10, 11, 12, 15, 16, 17].includes(response.error)) {
                await delay(1000);
                continue;
            }
            throw new Error(response.message || `Error ${response.error}`);
        } catch (error) {
            if (attempt === MAX_ATTEMPTS-1) throw error;
            await delay(1000);
        }
    }
}

async function pollCaptchaResult(recognitionId) {
    const startTime = Date.now();
    while (Date.now() - startTime < POLL_TIMEOUT) {
        const response = await apiRequest(`${API_ENDPOINT}/?id=${recognitionId}`);
        if (!response.error) return response;
        await delay(1000);
    }
    throw new Error('Polling timeout');
}

async function apiRequest(url, options = {}) {
    const headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    };
    if (settings.get("key")) {
        headers["Authorization"] = `Bearer ${settings.get("key")}`;
    }
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            url,
            method: options.method || 'GET',
            headers,
            data: options.data ? JSON.stringify(options.data) : null,
            responseType: 'json',
            onload: response => resolve(response.response),
            onerror: reject,
            ontimeout: reject,
            onabort: reject
        });
    });
}

async function loadImage(image, target, timeout = 10000) {
    if (!target && !image.complete && !await new Promise(resolve => {
        const timer = setTimeout(() => resolve(false), timeout);
        image.addEventListener("load", () => {
            clearTimeout(timer);
            resolve(true);
        });
    })) return;

    const canvas = createCanvas(
        image.naturalWidth || target?.clientWidth,
        image.naturalHeight || target?.clientHeight
    );
    canvas.getContext("2d").drawImage(image, 0, 0);
    return !isCanvasEmpty(canvas) && canvas;
}

function getPixelColor(imageData, t, n, o) {
    let index = (o * t + n) * 4;
    return [imageData[index], imageData[index + 1], imageData[index + 2]]
}

function isImageEmpty(canvas, minThreshold = 0, maxThreshold = 230, emptyRatio = 0.99) {
    const context = canvas.getContext("2d");
    const width = context.canvas.width;
    const height = context.canvas.height;
    if (width === 0 || height === 0) return true;

    const imageData = context.getImageData(0, 0, width, height).data;
    let emptyPixels = 0;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const color = getPixelColor(imageData, width, x, y, 1);
            const isColorBelowThreshold = color.every(value => value <= minThreshold);
            const isColorAboveThreshold = color.every(value => value >= maxThreshold);
            if (isColorBelowThreshold || isColorAboveThreshold) emptyPixels++;
        }
    }

    return emptyPixels / (width * height) > emptyRatio;
}

let isSolving = false;

async function startCaptchaSolving() {
    if(isSolving) return;

    isSolving = true;
    while (isCaptchaActive && (getCaptchaHeader() || isVerifyButtonDisabled())) {
        await delay(1000);
    }

    while(isCaptchaActive) {
        let { task, type, cells, images, waitAfterSolve } = await getCaptchaInfo();
        let startTime = new Date().valueOf(), c = [...cells];
        type !== 1 && (images = [images[0]]);
        let processedImages = await Promise.all(images.map(s => loadImage(s)));
        if(type === 1) {
            let s = [],
                x = [];
            for(let [index, img] of processedImages.entries()) img.width !== 100 || img.height !== 100 || (s.push(c[index]), x.push(img));
            c = s, processedImages = x
        }
        if(processedImages.length === 0) {
            clickElement("#recaptcha-verify-button");
            await delay(3e3);
            continue;
        }

        if(processedImages.some(isImageEmpty)) {
            await delay(3000);
            continue;
        }

        const gridSize = GRID_SIZES[type];
        const response = await solveCaptcha({
            task,
            grid: `${gridSize}x${gridSize}`,
            image_data: processedImages.map(canvasToBase64),
        })
        if(!response || "error" in response) {
            console.warn("api error", response), await delay(2e3);
            continue
        }
        const endTime = new Date().valueOf();
        if(settings.get("solve_delay")) {
            const delayTime = settings.get("solve_delay_time") - endTime + startTime;
            delayTime > 0 && await delay(delayTime)
        }
        const gridWidth = type === 2 ? 4 : 3;

        for(c.forEach((s, x) => {
            let B = s.classList.contains("rc-imageselect-tileselected"),
                h = cells.indexOf(s);
            response.data[x] !== B && clickElement(`tr:nth-child(${Math.floor(h/gridWidth)+1}) td:nth-child(${h%gridWidth+1})`)
        }), (!waitAfterSolve || !response.data.some(s => s)) && (await delay(200), clickElement("#recaptcha-verify-button")), await waitForEvent(eventQueue); document.querySelectorAll(".rc-imageselect-dynamic-selected").length > 0;) await delay(1e3)
    }
}

let e = document.referrer;
e = e ? e.split("/")[2] : location.origin

if (location.pathname.endsWith("/anchor")) {
    settings.createMenu();
    registerEventHandler({
        name: "auto-open",
        condition: () => settings.get("auto_open"), // && !settings.get("disabled_hosts").includes(e),
        ready: () => document.contains(document.querySelector(".recaptcha-checkbox")),
        start: initializeRecaptcha,
        quit: () => {
            checkboxObserver.disconnect();
            intersectionObserver.disconnect();
            isRecaptchaActive = false;
        },
        running: () => isRecaptchaActive
    })
} else {
    registerEventHandler({
        name: "auto-solve",
        condition: () => settings.get("auto_solve"), // && !settings.get("disabled_hosts").includes(e),
        ready: () => document.contains(document.querySelector(".rc-imageselect, .rc-imageselect-target")),
        start: initializeCaptcha,
        quit: () => {
            captchaObserver.disconnect();
            isCaptchaActive = false;
            processEvents(eventQueue)
        },
        running: () => isCaptchaActive
    })
}

async function checkEventHandler(handler) {
    if (handler.timedout) return false;
    const condition = handler.condition();
    if (condition === handler.running()) return false;
    if (!condition && handler.running()) {
        handler.quit();
        return false;
    }
    if (condition && !handler.running()) {
        while (!handler.ready()) await delay(200);
        handler.start();
        return false;
    }
}

function createCanvas(width, height) {
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    return canvas;
}

function canvasToBase64(canvas) {
    return canvas.toDataURL("image/jpeg").replace(/data:image\/[a-z]+;base64,/g, "");
}

function isCanvasEmpty(canvas) {
    try {
        canvas.getContext("2d").getImageData(0, 0, 1, 1);
    } catch {
        return true;
    }
    return false;
}


function delay(milliseconds) {
    return new Promise(resolve => setTimeout(resolve, milliseconds));
}

function initializeCaptcha() {
    isCaptchaActive = true;
    processEvents(eventQueue);
    let captchaTimeout;
    captchaObserver = new MutationObserver(() => {
        clearTimeout(captchaTimeout);
        captchaTimeout = setTimeout(() => processEvents(eventQueue), 200);
    });
    captchaObserver.observe(document.body, { childList: true, subtree: true });
    startCaptchaSolving();
}

function processEvents(queue) {
    queue.forEach(callback => callback());
    queue.splice(0);
}

function registerEventHandler(handler, timeoutDuration) {
    handler.timedout = false;
    eventHandlers.push(handler);
    let timeout, interval = setInterval(async () => {
        await checkEventHandler(handler) || (clearTimeout(timeout), clearInterval(interval));
    }, 400);
    timeoutDuration && (timeout = setTimeout(() => clearInterval(interval), timeoutDuration), handler.timedout = true);
}

function waitForEvent(queue) {
    return new Promise(resolve => queue.push(resolve));
}

function initializeRecaptcha() {
    isRecaptchaActive = true;
    checkboxObserver = new MutationObserver(changes => {
        if (changes.length === 2) {
            handleCheckboxChange();
        }
        if (changes.length && changes[0].target.classList.contains("recaptcha-checkbox-expired")) {
            location.reload();
        }
    });
    checkboxObserver.observe(document.querySelector(".recaptcha-checkbox"), {
        attributes: true
    });
    let isIntersected = false;
    intersectionObserver = new IntersectionObserver(() => {
        if (!isIntersected) {
            isIntersected = true;
            handleCheckboxChange();
        }
    }, {
        threshold: 0
    });
    intersectionObserver.observe(document.body);
}

function isVerifyButtonDisabled() {
    return document.querySelector("#recaptcha-verify-button")?.getAttribute("disabled");
}

async function handleCheckboxChange() {
    await delay(400);
    clickElement(".recaptcha-checkbox");
}

function clickElement(selector) {
    document.querySelector(selector)?.click();
}

function getCaptchaHeader() {
    return document.querySelector(".rc-doscaptcha-header");
}

function getCaptchaInfo() {
    return new Promise(resolve => {
        const interval = setInterval(() => {
            const instructions = document.querySelector(".rc-imageselect-instructions");
            const cells = [...document.querySelectorAll("table tr td")];
            const images = cells.map(cell => cell.querySelector("img")); //.filter(c => c).filter(c => c.src.trim());
            if (!instructions || cells.concat(images).length < 18) return;
            clearInterval(interval);
            const lines = instructions.innerText.split("\n");
            const task = lines.slice(0, 2).join(" ").replace(/\s+/g, " ").trim();
            const type = cells.length === 16 ? 2 : images.some(img => img.classList.contains("rc-image-tile-11")) ? 1 : 0;
            const waitAfterSolve = lines.length === 3 && type !== 2;
            resolve({ task, type, cells, images, waitAfterSolve });
        }, 1000);
    });
}