1514 lines
47 KiB
JavaScript
1514 lines
47 KiB
JavaScript
/**
|
||
* ============================================================================
|
||
* 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;
|
||
}
|
||
|
||
|
||
|