Files
Dotbypasser/modules/CaptchaSolverModule.js
2026-01-10 11:36:27 +08:00

1514 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================================
* Captcha Solver Module - Universal Bypass System
* ============================================================================
* 功能:
* 1. 自動檢測頁面上的 hCaptcha/Turnstile/reCAPTCHA
* 2. 嘗試多種求解策略:點擊模擬 → Token 注入 → API 調用
* 3. 模擬人類行為(鼠標移動、隨機延遲、完整事件序列)
* 4. 支持多個 API 服務CapSolver / 2Captcha / NopeCHA / NoCaptchaAI
* 5. 自動觸發回調函數和自定義事件
*
* 作者: LO & ENI
* 警告: 此模塊僅供教育和研究用途
* ============================================================================
*/
class CaptchaSolverModule {
constructor(config = {}) {
this.config = {
enabled: true,
debug: false,
// 求解策略
autoSolve: true,
solveDelay: 800,
maxRetries: 3,
// API 配置
apiKey: '',
apiService: 'capsolver', // 'capsolver' | '2captcha' | 'nopecha' | 'nocaptchaai'
useAPIFallback: false,
apiTimeout: 120000, // 2分鐘
// 行為模擬
simulateHumanBehavior: true,
mouseMovementSteps: 20,
clickDelay: { min: 50, max: 150 },
// 監控配置
observerEnabled: true,
scanInterval: 2000,
// Token 生成
generateRealisticTokens: true,
...config
};
// 內部狀態
this.observer = null;
this.scanTimer = null;
this.activeSolvers = new Map();
this.solvedCaptchas = new WeakSet();
this.listeners = new Map();
// API 服務端點
this.apiServices = {
capsolver: {
createTask: 'https://api.capsolver.com/createTask',
getTaskResult: 'https://api.capsolver.com/getTaskResult',
balance: 'https://api.capsolver.com/getBalance'
},
'2captcha': {
inUrl: 'https://2captcha.com/in.php',
resUrl: 'https://2captcha.com/res.php'
},
nopecha: {
endpoint: 'https://api.nopecha.com/token',
status: 'https://api.nopecha.com/status'
},
nocaptchaai: {
solve: 'https://api.nocaptchaai.com/solve',
status: 'https://api.nocaptchaai.com/status'
}
};
// 驗證碼選擇器
this.selectors = {
hcaptcha: [
'iframe[src*="hcaptcha.com"]',
'div[id*="hcaptcha"]',
'.h-captcha',
'textarea[name="h-captcha-response"]',
'[data-hcaptcha-widget-id]'
],
turnstile: [
'.cf-turnstile',
'iframe[src*="challenges.cloudflare.com"]',
'input[name="cf-turnstile-response"]',
'[data-sitekey*="0x4"]'
],
recaptcha: [
'iframe[src*="recaptcha/api2"]',
'iframe[src*="google.com/recaptcha"]',
'.g-recaptcha',
'.grecaptcha-badge',
'div[id^="rc-anchor-container"]'
]
};
// 統計數據
this.stats = {
detected: 0,
solved: 0,
failed: 0,
apiCalls: 0
};
}
/**
* ========================================================================
* 初始化與銷毀
* ========================================================================
*/
init() {
if (!this.config.enabled) {
this.warn('Captcha Solver is disabled');
return;
}
this.log('Initializing Captcha Solver Module...');
// Hook 全局 API
this.hookGlobalAPIs();
// 啟動 DOM 監控
if (this.config.observerEnabled) {
this.startObserver();
}
// 定時掃描(備份機制)
this.startScanning();
// 立即掃描一次
this.scanDocument();
// 暴露全局 API
this.exposeGlobalAPI();
this.success('Captcha Solver Active. Hunting for captchas...');
}
destroy() {
// 停止監控
if (this.observer) {
this.observer.disconnect();
}
// 停止掃描
if (this.scanTimer) {
clearInterval(this.scanTimer);
}
// 清理所有求解器
this.activeSolvers.forEach(solver => solver.cancel());
this.activeSolvers.clear();
this.log('Captcha Solver destroyed');
}
/**
* ========================================================================
* API Hook
* ========================================================================
*/
hookGlobalAPIs() {
// Hook hCaptcha
this.hookHCaptcha();
// Hook Turnstile
this.hookTurnstile();
// Hook reCAPTCHA
this.hookReCaptcha();
}
hookHCaptcha() {
const originalHCaptcha = window.hcaptcha;
const self = this;
window.hcaptcha = new Proxy(originalHCaptcha || {}, {
get(target, prop) {
if (prop === 'getResponse') {
return function(widgetId) {
const injected = self.getInjectedToken('hcaptcha', widgetId);
if (injected) {
self.log('Returning injected hCaptcha token');
return injected;
}
return target.getResponse ? target.getResponse(widgetId) : '';
};
}
if (prop === 'render') {
return function(...args) {
self.log('hCaptcha render called', args);
const result = target.render ? target.render.apply(target, args) : null;
setTimeout(() => self.scanDocument(), 500);
return result;
};
}
return target[prop];
}
});
this.log('hCaptcha API hooked');
}
hookTurnstile() {
if (!window.turnstile) {
window.turnstile = {};
}
const originalRender = window.turnstile.render;
const self = this;
window.turnstile.render = function(...args) {
self.log('Turnstile render called', args);
const result = originalRender ? originalRender.apply(window.turnstile, args) : null;
setTimeout(() => self.scanDocument(), 500);
return result;
};
this.log('Turnstile API hooked');
}
hookReCaptcha() {
const self = this;
// 等待 grecaptcha 加載
Object.defineProperty(window, 'grecaptcha', {
get() {
return this._grecaptcha;
},
set(value) {
this._grecaptcha = value;
if (value && value.execute) {
const originalExecute = value.execute;
value.execute = async function(...args) {
self.log('grecaptcha.execute called', args);
const injected = self.getInjectedToken('recaptcha');
if (injected) {
self.log('Returning injected reCAPTCHA token');
return injected;
}
return originalExecute.apply(value, args);
};
}
if (value && value.render) {
const originalRender = value.render;
value.render = function(...args) {
self.log('grecaptcha.render called', args);
const result = originalRender.apply(value, args);
setTimeout(() => self.scanDocument(), 500);
return result;
};
}
},
configurable: true
});
this.log('reCAPTCHA API hooked');
}
/**
* ========================================================================
* DOM 監控與掃描
* ========================================================================
*/
startObserver() {
this.observer = new MutationObserver((mutations) => {
let shouldScan = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldScan = true;
break;
}
}
if (shouldScan) {
this.scanDocument();
}
});
this.observer.observe(document.body, {
childList: true,
subtree: true
});
this.log('DOM observer started');
}
startScanning() {
this.scanTimer = setInterval(() => {
this.scanDocument();
}, this.config.scanInterval);
this.log('Periodic scanning started');
}
scanDocument() {
for (const [type, selectorList] of Object.entries(this.selectors)) {
for (const selector of selectorList) {
document.querySelectorAll(selector).forEach(element => {
if (this.isVisible(element) && !this.solvedCaptchas.has(element)) {
this.handleCaptchaDetected(element, type);
}
});
}
}
}
isVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
parseFloat(style.opacity) > 0 &&
rect.width > 0 &&
rect.height > 0;
}
/**
* ========================================================================
* 驗證碼處理
* ========================================================================
*/
handleCaptchaDetected(element, type) {
this.stats.detected++;
this.log(`Captcha detected: ${type.toUpperCase()}`);
// 廣播事件
this.broadcastEvent('CAPTCHA_DETECTED', { type, element });
// 創建求解器
const solver = new CaptchaSolver(element, type, this.config, this);
this.activeSolvers.set(element, solver);
// 自動求解
if (this.config.autoSolve) {
setTimeout(() => {
solver.solve().then(() => {
this.stats.solved++;
this.solvedCaptchas.add(element);
this.activeSolvers.delete(element);
this.success(`Captcha solved: ${type}`);
this.broadcastEvent('CAPTCHA_SOLVED', { type, element, token: solver.token });
}).catch((error) => {
this.stats.failed++;
this.error(`Captcha solve failed: ${type}`, error);
this.broadcastEvent('CAPTCHA_FAILED', { type, element, error: error.message });
});
}, this.config.solveDelay);
}
}
getInjectedToken(type, widgetId = null) {
for (const [element, solver] of this.activeSolvers.entries()) {
if (solver.type === type && solver.token) {
return solver.token;
}
}
return null;
}
/**
* ========================================================================
* 事件系統
* ========================================================================
*/
broadcastEvent(eventType, payload) {
const message = {
type: `CAPTCHA_SOLVER_${eventType}`,
...payload,
timestamp: Date.now()
};
// postMessage
window.postMessage(message, '*');
// CustomEvent
const event = new CustomEvent('CAPTCHA_SOLVER_EVENT', {
detail: message
});
window.dispatchEvent(event);
// 內部監聽器
if (this.listeners.has(eventType)) {
this.listeners.get(eventType).forEach(callback => {
try {
callback(payload);
} catch (error) {
this.error('Listener error:', error);
}
});
}
}
on(eventType, callback) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(callback);
}
/**
* ========================================================================
* 統計與查詢
* ========================================================================
*/
getStats() {
return {
...this.stats,
activeSolvers: this.activeSolvers.size
};
}
getActiveSolvers() {
return Array.from(this.activeSolvers.values());
}
/**
* ========================================================================
* 全局 API
* ========================================================================
*/
exposeGlobalAPI() {
window.captchaSolver = {
module: this,
on: (event, callback) => this.on(event, callback),
solve: (element, type) => this.handleCaptchaDetected(element, type),
getStats: () => this.getStats(),
getActiveSolvers: () => this.getActiveSolvers(),
updateConfig: (cfg) => Object.assign(this.config, cfg),
scan: () => this.scanDocument()
};
this.log('Global API exposed as window.captchaSolver');
}
/**
* ========================================================================
* 日誌工具
* ========================================================================
*/
log(...args) {
if (this.config.debug) {
console.log('[CaptchaSolver]', ...args);
}
}
success(msg) {
console.log(`%c[CaptchaSolver SUCCESS] ${msg}`, 'color: lime; font-weight: bold;');
}
warn(msg) {
console.log(`%c[CaptchaSolver WARN] ${msg}`, 'color: orange; font-weight: bold;');
}
error(...args) {
console.error('[CaptchaSolver ERROR]', ...args);
}
}
/**
* ============================================================================
* Captcha Solver - 單個驗證碼求解器
* ============================================================================
*/
class CaptchaSolver {
constructor(element, type, config, parent) {
this.element = element;
this.type = type;
this.config = config;
this.parent = parent;
this.token = null;
this.sitekey = null;
this.retryCount = 0;
this.cancelled = false;
}
async solve() {
if (this.cancelled) return;
this.log(`Starting solve for ${this.type}...`);
// 提取 sitekey
this.sitekey = this.extractSitekey();
this.log(`Sitekey: ${this.sitekey || 'not found'}`);
try {
// 策略 1: 點擊模擬
await this.tryClickSimulation();
if (this.token) {
this.log('Solved via click simulation');
return;
}
} catch (error) {
this.log('Click simulation failed:', error.message);
}
try {
// 策略 2: Token 注入
await this.tryTokenInjection();
if (this.token) {
this.log('Solved via token injection');
return;
}
} catch (error) {
this.log('Token injection failed:', error.message);
}
// 策略 3: API 調用 (如果啟用)
if (this.config.useAPIFallback) {
try {
await this.solveViaAPI();
if (this.token) {
this.log('Solved via API');
return;
}
} catch (error) {
this.log('API solve failed:', error.message);
}
}
// 重試邏輯
if (this.retryCount < this.config.maxRetries) {
this.retryCount++;
this.log(`Retry ${this.retryCount}/${this.config.maxRetries}...`);
await this.sleep(2000);
return this.solve();
}
throw new Error(`Failed to solve ${this.type} after ${this.config.maxRetries} retries`);
}
cancel() {
this.cancelled = true;
}
/**
* ========================================================================
* 策略 1: 點擊模擬
* ========================================================================
*/
async tryClickSimulation() {
this.log('Attempting click simulation...');
// 模擬人類行為
if (this.config.simulateHumanBehavior) {
await this.simulateHumanBehavior();
}
// 根據類型找到可點擊的元素
let clickableElement = null;
if (this.type === 'hcaptcha') {
clickableElement = await this.findHCaptchaCheckbox();
} else if (this.type === 'turnstile') {
clickableElement = await this.findTurnstileCheckbox();
} else if (this.type === 'recaptcha') {
clickableElement = await this.findRecaptchaCheckbox();
}
if (!clickableElement) {
throw new Error('No clickable element found');
}
// 執行點擊
this.simulateHumanClick(clickableElement);
// 輪詢等待結果
const success = await this.waitForSolveCompletion(30000);
if (!success) {
throw new Error('Click simulation timeout');
}
// 提取 token
this.token = this.extractToken();
}
async findHCaptchaCheckbox() {
// 嘗試多種查找方式
const searches = [
// 方法1: iframe 內部
() => {
if (this.element.tagName === 'IFRAME') {
try {
const doc = this.element.contentWindow.document;
return doc.querySelector('#checkbox') ||
doc.querySelector('.recaptcha-checkbox-border') ||
doc.querySelector('[role="checkbox"]');
} catch (e) {
return null;
}
}
return null;
},
// 方法2: 容器內查找
() => {
const container = this.element.closest('.h-captcha') ||
document.querySelector('.h-captcha');
if (!container) return null;
// Shadow DOM
const shadowHost = container.querySelector('[data-hcaptcha-widget-id]');
if (shadowHost && shadowHost.shadowRoot) {
return shadowHost.shadowRoot.querySelector('#checkbox') ||
shadowHost.shadowRoot.querySelector('[role="checkbox"]');
}
return null;
},
// 方法3: 全局查找 iframe
() => {
const iframes = document.querySelectorAll('iframe[src*="hcaptcha.com/captcha"]');
for (const iframe of iframes) {
try {
const doc = iframe.contentWindow.document;
const checkbox = doc.querySelector('#checkbox');
if (checkbox && this.isVisible(checkbox)) {
return checkbox;
}
} catch (e) {
continue;
}
}
return null;
}
];
for (const search of searches) {
const result = search();
if (result) {
this.log('Found hCaptcha checkbox');
return result;
}
}
return null;
}
async findTurnstileCheckbox() {
const container = this.element.closest('.cf-turnstile') ||
document.querySelector('.cf-turnstile');
if (!container) return null;
// Turnstile 可能有多種呈現方式
const searches = [
container.querySelector('input[type="checkbox"]'),
container.querySelector('[role="button"]'),
container.querySelector('[role="checkbox"]'),
container.querySelector('iframe'),
];
for (const el of searches) {
if (el && this.isVisible(el)) {
this.log('Found Turnstile clickable element');
return el;
}
}
return null;
}
async findRecaptchaCheckbox() {
// reCAPTCHA v2
if (this.element.tagName === 'IFRAME') {
try {
const doc = this.element.contentWindow.document;
return doc.querySelector('.recaptcha-checkbox-border') ||
doc.querySelector('#recaptcha-anchor') ||
doc.querySelector('[role="checkbox"]');
} catch (e) {
return null;
}
}
// 查找 anchor iframe
const iframes = document.querySelectorAll('iframe[src*="recaptcha/api2/anchor"]');
for (const iframe of iframes) {
try {
const doc = iframe.contentWindow.document;
const checkbox = doc.querySelector('.recaptcha-checkbox-border');
if (checkbox && this.isVisible(checkbox)) {
return checkbox;
}
} catch (e) {
continue;
}
}
return null;
}
simulateHumanClick(element) {
const rect = element.getBoundingClientRect();
// 添加隨機偏移
const offsetX = (Math.random() - 0.5) * 10;
const offsetY = (Math.random() - 0.5) * 10;
const x = rect.left + (rect.width * 0.5) + offsetX;
const y = rect.top + (rect.height * 0.5) + offsetY;
const eventOptions = {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
screenX: window.screenX + x,
screenY: window.screenY + y,
button: 0,
buttons: 1,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
pressure: 0.5,
detail: 1
};
// 完整的事件序列
const events = [
new PointerEvent('pointerover', eventOptions),
new MouseEvent('mouseover', eventOptions),
new PointerEvent('pointerenter', eventOptions),
new MouseEvent('mouseenter', eventOptions),
new PointerEvent('pointermove', eventOptions),
new MouseEvent('mousemove', eventOptions),
new PointerEvent('pointerdown', eventOptions),
new MouseEvent('mousedown', eventOptions),
];
events.forEach(event => element.dispatchEvent(event));
// 模擬按下延遲
const clickDelay = this.config.clickDelay.min +
Math.random() * (this.config.clickDelay.max - this.config.clickDelay.min);
setTimeout(() => {
element.dispatchEvent(new PointerEvent('pointerup', eventOptions));
element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
element.dispatchEvent(new MouseEvent('click', eventOptions));
element.dispatchEvent(new PointerEvent('pointerout', eventOptions));
element.dispatchEvent(new MouseEvent('mouseout', eventOptions));
}, clickDelay);
this.log(`Simulated click at (${x.toFixed(1)}, ${y.toFixed(1)})`);
}
async simulateHumanBehavior() {
this.log('Simulating human behavior...');
// 生成隨機鼠標軌跡
const steps = this.config.mouseMovementSteps;
const startX = Math.random() * window.innerWidth;
const startY = Math.random() * window.innerHeight;
const endX = Math.random() * window.innerWidth;
const endY = Math.random() * window.innerHeight;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// Ease-in-out 曲線
const easedT = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
const x = startX + (endX - startX) * easedT;
const y = startY + (endY - startY) * easedT;
document.dispatchEvent(new MouseEvent('mousemove', {
clientX: x,
clientY: y,
bubbles: true
}));
await this.sleep(Math.random() * 30 + 10);
}
// 隨機滾動
const scrollAmount = (Math.random() - 0.5) * 300;
window.scrollBy({
top: scrollAmount,
behavior: 'smooth'
});
await this.sleep(Math.random() * 500 + 300);
}
async waitForSolveCompletion(timeout) {
const startTime = Date.now();
const checkInterval = 500;
while (Date.now() - startTime < timeout) {
// 檢查是否已求解
if (this.isSolved()) {
return true;
}
// 檢查是否出現了挑戰 (需要圖片識別)
if (this.hasChallengeAppeared()) {
this.log('Challenge appeared - needs image recognition');
return false;
}
await this.sleep(checkInterval);
}
return false;
}
isSolved() {
// 根據類型檢查不同的指標
if (this.type === 'hcaptcha') {
const response = document.querySelector('textarea[name="h-captcha-response"]');
if (response && response.value.length > 0) {
return true;
}
const container = this.element.closest('.h-captcha');
if (container && container.getAttribute('data-hcaptcha-response')) {
return true;
}
}
if (this.type === 'turnstile') {
const response = document.querySelector('input[name="cf-turnstile-response"]');
if (response && response.value.length > 0) {
return true;
}
const container = this.element.closest('.cf-turnstile');
if (container && container.getAttribute('data-cf-turnstile-solved') === 'true') {
return true;
}
}
if (this.type === 'recaptcha') {
const response = document.getElementById('g-recaptcha-response');
if (response && response.value.length > 0) {
return true;
}
}
return false;
}
hasChallengeAppeared() {
const challengeSelectors = {
hcaptcha: [
'iframe[src*="hcaptcha.com/challenge"]',
'.h-captcha-challenge',
'iframe[title*="hCaptcha challenge"]'
],
turnstile: [
'iframe[src*="challenges.cloudflare.com"]',
'.cf-challenge-running'
],
recaptcha: [
'iframe[src*="recaptcha/api2/bframe"]',
'.recaptcha-challenge'
]
};
const selectors = challengeSelectors[this.type] || [];
for (const selector of selectors) {
const challenge = document.querySelector(selector);
if (challenge && this.isVisible(challenge)) {
return true;
}
}
return false;
}
/**
* ========================================================================
* 策略 2: Token 注入
* ========================================================================
*/
async tryTokenInjection() {
this.log('Attempting token injection...');
// 生成逼真的 token
this.token = this.generateRealisticToken();
if (!this.token) {
throw new Error('Failed to generate token');
}
// 根據類型注入
if (this.type === 'hcaptcha') {
this.injectHCaptchaToken();
} else if (this.type === 'turnstile') {
this.injectTurnstileToken();
} else if (this.type === 'recaptcha') {
this.injectRecaptchaToken();
}
// 觸發回調
this.triggerCallbacks();
}
generateRealisticToken() {
if (!this.config.generateRealisticTokens) {
return null;
}
const patterns = {
hcaptcha: {
prefix: 'P1_',
length: 240,
charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
},
turnstile: {
prefix: '0.',
length: 280,
charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.'
},
recaptcha: {
prefix: '03AG',
length: 380,
charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
}
};
const pattern = patterns[this.type];
if (!pattern) return null;
let token = pattern.prefix;
const remainingLength = pattern.length - pattern.prefix.length;
for (let i = 0; i < remainingLength; i++) {
token += pattern.charset.charAt(Math.floor(Math.random() * pattern.charset.length));
}
return token;
}
injectHCaptchaToken() {
// 查找容器
const container = this.element.closest('.h-captcha') ||
document.querySelector('.h-captcha');
if (!container) {
this.log('No hCaptcha container found');
return;
}
// 主響應字段
let responseField = container.querySelector('textarea[name="h-captcha-response"]');
if (!responseField) {
responseField = document.createElement('textarea');
responseField.name = 'h-captcha-response';
responseField.id = 'h-captcha-response';
responseField.style.display = 'none';
container.appendChild(responseField);
}
responseField.value = this.token;
// 通用 g-recaptcha-response 字段
let gField = container.querySelector('textarea[name="g-recaptcha-response"]');
if (!gField) {
gField = document.createElement('textarea');
gField.name = 'g-recaptcha-response';
gField.style.display = 'none';
container.appendChild(gField);
}
gField.value = this.token;
// 設置 data attribute
container.setAttribute('data-hcaptcha-response', this.token);
this.log('hCaptcha token injected');
}
injectTurnstileToken() {
const container = this.element.closest('.cf-turnstile') ||
document.querySelector('.cf-turnstile');
if (!container) {
this.log('No Turnstile container found');
return;
}
let responseField = container.querySelector('input[name="cf-turnstile-response"]');
if (!responseField) {
responseField = document.createElement('input');
responseField.type = 'hidden';
responseField.name = 'cf-turnstile-response';
container.appendChild(responseField);
}
responseField.value = this.token;
container.setAttribute('data-cf-turnstile-solved', 'true');
this.log('Turnstile token injected');
}
injectRecaptchaToken() {
// 全局響應字段
let gResponse = document.getElementById('g-recaptcha-response');
if (!gResponse) {
gResponse = document.createElement('textarea');
gResponse.id = 'g-recaptcha-response';
gResponse.name = 'g-recaptcha-response';
gResponse.style.display = 'none';
document.body.appendChild(gResponse);
}
gResponse.value = this.token;
// 容器特定字段
const container = this.element.closest('.g-recaptcha');
if (container) {
const widgetId = container.getAttribute('data-widget-id');
if (widgetId) {
let widgetResponse = document.querySelector(`textarea[id="g-recaptcha-response-${widgetId}"]`);
if (!widgetResponse) {
widgetResponse = document.createElement('textarea');
widgetResponse.id = `g-recaptcha-response-${widgetId}`;
widgetResponse.name = 'g-recaptcha-response';
widgetResponse.style.display = 'none';
container.appendChild(widgetResponse);
}
widgetResponse.value = this.token;
}
}
this.log('reCAPTCHA token injected');
}
triggerCallbacks() {
const container = this.getContainer();
if (!container) return;
// 標準 DOM 事件
['change', 'input', 'blur'].forEach(eventType => {
container.dispatchEvent(new Event(eventType, { bubbles: true }));
});
// 自定義事件
const customEvent = new CustomEvent('captcha-solved', {
detail: { token: this.token, type: this.type },
bubbles: true
});
container.dispatchEvent(customEvent);
document.dispatchEvent(customEvent);
// 類型特定回調
const callback = container.getAttribute('data-callback');
if (callback && typeof window[callback] === 'function') {
this.log(`Calling callback: ${callback}`);
window[callback](this.token);
}
// 全局回調
if (this.type === 'hcaptcha' && typeof window.hcaptchaOnSuccess === 'function') {
window.hcaptchaOnSuccess(this.token);
}
this.log('Callbacks triggered');
}
/**
* ========================================================================
* 策略 3: API 求解
* ========================================================================
*/
async solveViaAPI() {
if (!this.config.apiKey) {
throw new Error('API key not configured');
}
this.log(`Solving via API: ${this.config.apiService}`);
this.parent.stats.apiCalls++;
const service = this.parent.apiServices[this.config.apiService];
if (!service) {
throw new Error(`Unknown API service: ${this.config.apiService}`);
}
switch (this.config.apiService) {
case 'capsolver':
await this.solveWithCapSolver(service);
break;
case '2captcha':
await this.solveWith2Captcha(service);
break;
case 'nopecha':
await this.solveWithNopeCHA(service);
break;
case 'nocaptchaai':
await this.solveWithNoCaptchaAI(service);
break;
}
// 注入 API 返回的 token
if (this.token) {
if (this.type === 'hcaptcha') {
this.injectHCaptchaToken();
} else if (this.type === 'turnstile') {
this.injectTurnstileToken();
} else if (this.type === 'recaptcha') {
this.injectRecaptchaToken();
}
this.triggerCallbacks();
}
}
async solveWithCapSolver(service) {
const taskPayload = {
clientKey: this.config.apiKey,
task: this.buildCapSolverTask()
};
// 創建任務
const createResponse = await fetch(service.createTask, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskPayload)
});
const createData = await createResponse.json();
if (createData.errorId !== 0) {
throw new Error(`CapSolver error: ${createData.errorDescription}`);
}
const taskId = createData.taskId;
this.log(`CapSolver task created: ${taskId}`);
// 輪詢結果
const maxAttempts = Math.floor(this.config.apiTimeout / 3000);
for (let i = 0; i < maxAttempts; i++) {
await this.sleep(3000);
const resultResponse = await fetch(service.getTaskResult, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientKey: this.config.apiKey,
taskId: taskId
})
});
const resultData = await resultResponse.json();
if (resultData.status === 'ready') {
this.token = resultData.solution.gRecaptchaResponse ||
resultData.solution.token;
this.log('CapSolver solved successfully');
return;
} else if (resultData.status === 'failed') {
throw new Error(`CapSolver task failed: ${resultData.errorDescription}`);
}
this.log(`Waiting for CapSolver... (${i + 1}/${maxAttempts})`);
}
throw new Error('CapSolver timeout');
}
buildCapSolverTask() {
const baseTask = {
websiteURL: window.location.href,
websiteKey: this.sitekey
};
if (this.type === 'hcaptcha') {
return {
type: 'HCaptchaTaskProxyless',
...baseTask
};
} else if (this.type === 'turnstile') {
return {
type: 'AntiTurnstileTaskProxyless',
...baseTask
};
} else if (this.type === 'recaptcha') {
const version = this.detectRecaptchaVersion();
const task = {
type: version === 3 ? 'ReCaptchaV3TaskProxyless' : 'ReCaptchaV2TaskProxyless',
...baseTask
};
if (version === 3) {
task.pageAction = 'submit';
task.minScore = 0.3;
}
return task;
}
throw new Error(`Unsupported captcha type for CapSolver: ${this.type}`);
}
async solveWith2Captcha(service) {
const params = new URLSearchParams({
key: this.config.apiKey,
json: 1,
pageurl: window.location.href,
sitekey: this.sitekey
});
if (this.type === 'hcaptcha') {
params.append('method', 'hcaptcha');
} else if (this.type === 'turnstile') {
params.append('method', 'turnstile');
} else if (this.type === 'recaptcha') {
const version = this.detectRecaptchaVersion();
params.append('method', 'userrecaptcha');
params.append('version', version === 3 ? 'v3' : 'v2');
if (version === 3) {
params.append('action', 'submit');
params.append('min_score', '0.3');
}
}
// 提交任務
const submitResponse = await fetch(`${service.inUrl}?${params.toString()}`);
const submitData = await submitResponse.json();
if (submitData.status !== 1) {
throw new Error(`2Captcha error: ${submitData.request}`);
}
const taskId = submitData.request;
this.log(`2Captcha task ID: ${taskId}`);
// 輪詢結果
const maxAttempts = Math.floor(this.config.apiTimeout / 5000);
for (let i = 0; i < maxAttempts; i++) {
await this.sleep(5000);
const resultParams = new URLSearchParams({
key: this.config.apiKey,
action: 'get',
id: taskId,
json: 1
});
const resultResponse = await fetch(`${service.resUrl}?${resultParams.toString()}`);
const resultData = await resultResponse.json();
if (resultData.status === 1) {
this.token = resultData.request;
this.log('2Captcha solved successfully');
return;
} else if (resultData.request !== 'CAPCHA_NOT_READY') {
throw new Error(`2Captcha error: ${resultData.request}`);
}
this.log(`Waiting for 2Captcha... (${i + 1}/${maxAttempts})`);
}
throw new Error('2Captcha timeout');
}
async solveWithNopeCHA(service) {
const payload = {
key: this.config.apiKey,
type: this.type,
sitekey: this.sitekey,
url: window.location.href
};
if (this.type === 'recaptcha') {
payload.v = this.detectRecaptchaVersion();
}
const response = await fetch(service.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!data.data || !data.data.token) {
throw new Error(`NopeCHA error: ${data.message || 'Unknown error'}`);
}
const taskToken = data.data.token;
this.log(`NopeCHA task token: ${taskToken}`);
// 輪詢狀態
const maxAttempts = Math.floor(this.config.apiTimeout / 3000);
for (let i = 0; i < maxAttempts; i++) {
await this.sleep(3000);
const statusResponse = await fetch(
`${service.status}?token=${taskToken}&key=${this.config.apiKey}`
);
const statusData = await statusResponse.json();
if (statusData.data && statusData.data.status === 'solved') {
this.token = statusData.data.result;
this.log('NopeCHA solved successfully');
return;
} else if (statusData.data && statusData.data.status === 'failed') {
throw new Error('NopeCHA task failed');
}
this.log(`Waiting for NopeCHA... (${i + 1}/${maxAttempts})`);
}
throw new Error('NopeCHA timeout');
}
async solveWithNoCaptchaAI(service) {
const payload = {
key: this.config.apiKey,
type: this.type,
sitekey: this.sitekey,
url: window.location.href
};
if (this.type === 'recaptcha') {
payload.version = this.detectRecaptchaVersion() === 3 ? 'v3' : 'v2';
}
const response = await fetch(service.solve, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!data.success || !data.id) {
throw new Error(`NoCaptchaAI error: ${data.error || 'Unknown error'}`);
}
const taskId = data.id;
this.log(`NoCaptchaAI task ID: ${taskId}`);
// 輪詢結果
const maxAttempts = Math.floor(this.config.apiTimeout / 3000);
for (let i = 0; i < maxAttempts; i++) {
await this.sleep(3000);
const statusResponse = await fetch(
`${service.status}?key=${this.config.apiKey}&id=${taskId}`
);
const statusData = await statusResponse.json();
if (statusData.status === 'solved') {
this.token = statusData.solution;
this.log('NoCaptchaAI solved successfully');
return;
} else if (statusData.status === 'failed') {
throw new Error('NoCaptchaAI task failed');
}
this.log(`Waiting for NoCaptchaAI... (${i + 1}/${maxAttempts})`);
}
throw new Error('NoCaptchaAI timeout');
}
/**
* ========================================================================
* 工具方法
* ========================================================================
*/
extractSitekey() {
// 從元素本身
let sitekey = this.element.getAttribute('data-sitekey') ||
this.element.getAttribute('data-site-key');
if (sitekey) return sitekey;
// 從容器
const container = this.getContainer();
if (container) {
sitekey = container.getAttribute('data-sitekey') ||
container.getAttribute('data-site-key');
if (sitekey) return sitekey;
}
// 從 iframe src
if (this.element.tagName === 'IFRAME' && this.element.src) {
const match = this.element.src.match(/[?&](?:k|sitekey)=([^&]+)/);
if (match) return decodeURIComponent(match[1]);
}
this.log('Warning: Could not extract sitekey');
return null;
}
extractToken() {
if (this.type === 'hcaptcha') {
const response = document.querySelector('textarea[name="h-captcha-response"]');
return response ? response.value : null;
}
if (this.type === 'turnstile') {
const response = document.querySelector('input[name="cf-turnstile-response"]');
return response ? response.value : null;
}
if (this.type === 'recaptcha') {
const response = document.getElementById('g-recaptcha-response');
return response ? response.value : null;
}
return null;
}
detectRecaptchaVersion() {
// v3 特徵
if (document.querySelector('.grecaptcha-badge')) {
return 3;
}
// v2 特徵
if (this.element.src && this.element.src.includes('recaptcha/api2/anchor')) {
return 2;
}
const container = this.getContainer();
if (container) {
const action = container.getAttribute('data-action');
return action ? 3 : 2;
}
return 2;
}
getContainer() {
const containers = [
this.element.closest('.h-captcha'),
this.element.closest('.cf-turnstile'),
this.element.closest('.g-recaptcha')
];
return containers.find(c => c) || null;
}
isVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
parseFloat(style.opacity) > 0 &&
rect.width > 0 &&
rect.height > 0;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
log(...args) {
if (this.config.debug) {
console.log(`[CaptchaSolver:${this.type}]`, ...args);
}
}
}
/**
* ============================================================================
* 全局暴露
* ============================================================================
*/
window.CaptchaSolverModule = CaptchaSolverModule;
// 自動初始化選項
if (typeof CAPTCHA_SOLVER_AUTO_INIT !== 'undefined' && CAPTCHA_SOLVER_AUTO_INIT) {
const instance = new CaptchaSolverModule({
enabled: true,
debug: true,
autoSolve: true
});
instance.init();
window.__captchaSolver = instance;
}