NopeCHA - Automated reCAPTCHA Solver

AI for Automatic reCAPTCHA Recognition

目前為 2025-04-29 提交的版本,檢視 最新版本

// ==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);
    });
}