brave-real-browser-mcp-server 2.40.2 → 2.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eng.traineddata +0 -0
- package/lib/ocr-captcha-solver.js +525 -0
- package/package.json +6 -2
- package/packages/brave-real-blocker/package.json +2 -2
- package/packages/brave-real-launcher/package.json +2 -2
- package/packages/brave-real-playwright-core/package.json +1 -1
- package/packages/brave-real-puppeteer-core/package.json +3 -3
- package/scripts/auto-update-deps.js +193 -0
- package/src/mcp/handlers.js +94 -2
- package/src/shared/tools.js +18 -5
package/eng.traineddata
ADDED
|
Binary file
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCR Text Captcha Solver (Enhanced Version)
|
|
3
|
+
*
|
|
4
|
+
* Simple text-based captcha solver using Tesseract.js OCR
|
|
5
|
+
* Works with captchas like: CCA23E, actukd, hf4kvf (eCourts India style)
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Advanced image preprocessing (grayscale, threshold, denoise, remove lines)
|
|
9
|
+
* - Multiple OCR attempts with different settings
|
|
10
|
+
* - Auto-retry with captcha refresh
|
|
11
|
+
* - Hindi + English language support
|
|
12
|
+
* - Works offline (no API needed)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const Tesseract = require('tesseract.js');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
// Colors for console
|
|
20
|
+
const colors = {
|
|
21
|
+
green: '\x1b[32m',
|
|
22
|
+
yellow: '\x1b[33m',
|
|
23
|
+
blue: '\x1b[34m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
reset: '\x1b[0m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const log = {
|
|
30
|
+
info: (msg) => console.log(`${colors.blue}[ocr-captcha]${colors.reset} ${msg}`),
|
|
31
|
+
success: (msg) => console.log(`${colors.green}[ocr-captcha]${colors.reset} ✅ ${msg}`),
|
|
32
|
+
warn: (msg) => console.log(`${colors.yellow}[ocr-captcha]${colors.reset} ⚠️ ${msg}`),
|
|
33
|
+
error: (msg) => console.log(`${colors.red}[ocr-captcha]${colors.reset} ❌ ${msg}`),
|
|
34
|
+
debug: (msg) => console.log(`${colors.cyan}[ocr-captcha]${colors.reset} 🔍 ${msg}`)
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Common captcha character substitutions for correction
|
|
38
|
+
const CHAR_SUBSTITUTIONS = {
|
|
39
|
+
'0': ['O', 'o', 'Q', 'D'],
|
|
40
|
+
'O': ['0', 'o', 'Q', 'D'],
|
|
41
|
+
'1': ['l', 'I', 'i', '|', '!'],
|
|
42
|
+
'l': ['1', 'I', 'i', '|'],
|
|
43
|
+
'I': ['1', 'l', 'i', '|'],
|
|
44
|
+
'5': ['S', 's', '$'],
|
|
45
|
+
'S': ['5', 's', '$'],
|
|
46
|
+
'8': ['B', '&'],
|
|
47
|
+
'B': ['8', '&'],
|
|
48
|
+
'2': ['Z', 'z'],
|
|
49
|
+
'Z': ['2', 'z'],
|
|
50
|
+
'6': ['G', 'b'],
|
|
51
|
+
'G': ['6', 'C'],
|
|
52
|
+
'9': ['g', 'q'],
|
|
53
|
+
'g': ['9', 'q'],
|
|
54
|
+
'C': ['G', 'c', '('],
|
|
55
|
+
'E': ['3', 'e'],
|
|
56
|
+
'3': ['E', 'e'],
|
|
57
|
+
'A': ['4', 'a'],
|
|
58
|
+
'4': ['A', 'a'],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Default OCR settings optimized for captchas
|
|
62
|
+
const DEFAULT_OCR_CONFIG = {
|
|
63
|
+
lang: 'eng',
|
|
64
|
+
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
65
|
+
tessedit_pageseg_mode: '7', // Single line
|
|
66
|
+
preserve_interword_spaces: '0',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Preprocessing configurations for different captcha types
|
|
70
|
+
const PREPROCESS_CONFIGS = [
|
|
71
|
+
{ name: 'standard', threshold: 128, invert: false, removeLines: true },
|
|
72
|
+
{ name: 'high-contrast', threshold: 100, invert: false, removeLines: true },
|
|
73
|
+
{ name: 'low-contrast', threshold: 160, invert: false, removeLines: true },
|
|
74
|
+
{ name: 'inverted', threshold: 128, invert: true, removeLines: true },
|
|
75
|
+
{ name: 'no-line-removal', threshold: 128, invert: false, removeLines: false },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Advanced image preprocessing in browser
|
|
80
|
+
* Removes noise, lines, and enhances text
|
|
81
|
+
*/
|
|
82
|
+
async function preprocessImageAdvanced(page, selector, config = {}) {
|
|
83
|
+
const { threshold = 128, invert = false, removeLines = true } = config;
|
|
84
|
+
|
|
85
|
+
return await page.evaluate(({ sel, threshold, invert, removeLines }) => {
|
|
86
|
+
const img = document.querySelector(sel);
|
|
87
|
+
if (!img) return null;
|
|
88
|
+
|
|
89
|
+
const canvas = document.createElement('canvas');
|
|
90
|
+
const ctx = canvas.getContext('2d');
|
|
91
|
+
|
|
92
|
+
// Use natural dimensions for better quality
|
|
93
|
+
const width = img.naturalWidth || img.width || 200;
|
|
94
|
+
const height = img.naturalHeight || img.height || 50;
|
|
95
|
+
|
|
96
|
+
canvas.width = width;
|
|
97
|
+
canvas.height = height;
|
|
98
|
+
|
|
99
|
+
// Draw original image
|
|
100
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
101
|
+
|
|
102
|
+
// Get image data
|
|
103
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
104
|
+
const data = imageData.data;
|
|
105
|
+
|
|
106
|
+
// Step 1: Convert to grayscale
|
|
107
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
108
|
+
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
109
|
+
data[i] = gray;
|
|
110
|
+
data[i + 1] = gray;
|
|
111
|
+
data[i + 2] = gray;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Step 2: Remove diagonal lines (common in captchas)
|
|
115
|
+
if (removeLines) {
|
|
116
|
+
// Detect and remove thin lines
|
|
117
|
+
for (let y = 1; y < height - 1; y++) {
|
|
118
|
+
for (let x = 1; x < width - 1; x++) {
|
|
119
|
+
const idx = (y * width + x) * 4;
|
|
120
|
+
const pixel = data[idx];
|
|
121
|
+
|
|
122
|
+
// Check if this is a dark pixel
|
|
123
|
+
if (pixel < threshold) {
|
|
124
|
+
// Get surrounding pixels
|
|
125
|
+
const top = data[((y - 1) * width + x) * 4];
|
|
126
|
+
const bottom = data[((y + 1) * width + x) * 4];
|
|
127
|
+
const left = data[(y * width + (x - 1)) * 4];
|
|
128
|
+
const right = data[(y * width + (x + 1)) * 4];
|
|
129
|
+
|
|
130
|
+
// Count dark neighbors
|
|
131
|
+
let darkNeighbors = 0;
|
|
132
|
+
if (top < threshold) darkNeighbors++;
|
|
133
|
+
if (bottom < threshold) darkNeighbors++;
|
|
134
|
+
if (left < threshold) darkNeighbors++;
|
|
135
|
+
if (right < threshold) darkNeighbors++;
|
|
136
|
+
|
|
137
|
+
// If isolated or thin line (≤2 dark neighbors), might be noise
|
|
138
|
+
if (darkNeighbors <= 1) {
|
|
139
|
+
// Make it white (remove noise)
|
|
140
|
+
data[idx] = 255;
|
|
141
|
+
data[idx + 1] = 255;
|
|
142
|
+
data[idx + 2] = 255;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step 3: Apply threshold (binarization)
|
|
150
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
151
|
+
const gray = data[i];
|
|
152
|
+
let bw = gray > threshold ? 255 : 0;
|
|
153
|
+
|
|
154
|
+
// Invert if needed
|
|
155
|
+
if (invert) bw = 255 - bw;
|
|
156
|
+
|
|
157
|
+
data[i] = bw;
|
|
158
|
+
data[i + 1] = bw;
|
|
159
|
+
data[i + 2] = bw;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ctx.putImageData(imageData, 0, 0);
|
|
163
|
+
|
|
164
|
+
return canvas.toDataURL('image/png');
|
|
165
|
+
}, { sel: selector, threshold, invert, removeLines });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get captcha image with optional preprocessing
|
|
170
|
+
*/
|
|
171
|
+
async function getCaptchaImage(page, selector, preprocess = true, preprocessConfig = {}) {
|
|
172
|
+
try {
|
|
173
|
+
if (preprocess) {
|
|
174
|
+
// Try preprocessing first
|
|
175
|
+
const processed = await preprocessImageAdvanced(page, selector, preprocessConfig);
|
|
176
|
+
if (processed) {
|
|
177
|
+
log.debug('Image preprocessed successfully');
|
|
178
|
+
return processed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback: Screenshot of element
|
|
183
|
+
const element = await page.$(selector);
|
|
184
|
+
if (element) {
|
|
185
|
+
const screenshot = await element.screenshot({ encoding: 'base64' });
|
|
186
|
+
return `data:image/png;base64,${screenshot}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Fallback 2: Get image src
|
|
190
|
+
const imgSrc = await page.$eval(selector, (img) => {
|
|
191
|
+
if (img.tagName === 'IMG') {
|
|
192
|
+
const canvas = document.createElement('canvas');
|
|
193
|
+
canvas.width = img.naturalWidth || img.width;
|
|
194
|
+
canvas.height = img.naturalHeight || img.height;
|
|
195
|
+
const ctx = canvas.getContext('2d');
|
|
196
|
+
ctx.drawImage(img, 0, 0);
|
|
197
|
+
return canvas.toDataURL('image/png');
|
|
198
|
+
}
|
|
199
|
+
return img.src || null;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return imgSrc;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
log.warn(`Image capture error: ${error.message}`);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Recognize text using Tesseract with multiple attempts
|
|
211
|
+
*/
|
|
212
|
+
async function recognizeText(imageData, config = {}) {
|
|
213
|
+
const worker = await Tesseract.createWorker(config.lang || 'eng');
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await worker.setParameters({
|
|
217
|
+
tessedit_char_whitelist: config.tessedit_char_whitelist || DEFAULT_OCR_CONFIG.tessedit_char_whitelist,
|
|
218
|
+
tessedit_pageseg_mode: config.tessedit_pageseg_mode || '7',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const { data } = await worker.recognize(imageData);
|
|
222
|
+
|
|
223
|
+
// Clean up recognized text
|
|
224
|
+
let text = data.text
|
|
225
|
+
.replace(/\s+/g, '') // Remove whitespace
|
|
226
|
+
.replace(/[^a-zA-Z0-9]/g, ''); // Keep only alphanumeric
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
text,
|
|
230
|
+
confidence: data.confidence,
|
|
231
|
+
rawText: data.text,
|
|
232
|
+
};
|
|
233
|
+
} finally {
|
|
234
|
+
await worker.terminate();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Solve text captcha with multiple preprocessing attempts
|
|
240
|
+
*/
|
|
241
|
+
async function solveTextCaptcha(page, selector, options = {}) {
|
|
242
|
+
const {
|
|
243
|
+
lang = 'eng',
|
|
244
|
+
retries = 3,
|
|
245
|
+
confidence = 50, // Lowered for better success rate
|
|
246
|
+
allowedChars = null,
|
|
247
|
+
expectedLength = null,
|
|
248
|
+
tryAllPreprocess = true, // Try all preprocessing configs
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
log.info(`Solving text captcha: ${selector}`);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
let bestResult = null;
|
|
255
|
+
let allAttempts = [];
|
|
256
|
+
|
|
257
|
+
// Determine which preprocessing configs to try
|
|
258
|
+
const configsToTry = tryAllPreprocess ? PREPROCESS_CONFIGS : [PREPROCESS_CONFIGS[0]];
|
|
259
|
+
|
|
260
|
+
for (const preprocessConfig of configsToTry) {
|
|
261
|
+
log.debug(`Trying preprocess config: ${preprocessConfig.name}`);
|
|
262
|
+
|
|
263
|
+
// Get preprocessed image
|
|
264
|
+
const imageData = await getCaptchaImage(page, selector, true, preprocessConfig);
|
|
265
|
+
|
|
266
|
+
if (!imageData) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Try different PSM modes
|
|
271
|
+
const psmModes = ['7', '8', '13', '6']; // 7=single line, 8=word, 13=raw, 6=block
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < Math.min(retries, psmModes.length); i++) {
|
|
274
|
+
const config = {
|
|
275
|
+
...DEFAULT_OCR_CONFIG,
|
|
276
|
+
lang,
|
|
277
|
+
tessedit_pageseg_mode: psmModes[i],
|
|
278
|
+
...(allowedChars && { tessedit_char_whitelist: allowedChars }),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const result = await recognizeText(imageData, config);
|
|
283
|
+
result.preprocessConfig = preprocessConfig.name;
|
|
284
|
+
result.psmMode = psmModes[i];
|
|
285
|
+
allAttempts.push(result);
|
|
286
|
+
|
|
287
|
+
log.debug(` PSM ${psmModes[i]}: "${result.text}" (${result.confidence.toFixed(1)}%)`);
|
|
288
|
+
|
|
289
|
+
// Check if result meets criteria
|
|
290
|
+
const meetsConfidence = result.confidence >= confidence;
|
|
291
|
+
const meetsLength = !expectedLength || result.text.length === expectedLength;
|
|
292
|
+
const hasText = result.text.length > 0;
|
|
293
|
+
|
|
294
|
+
if (meetsConfidence && meetsLength && hasText) {
|
|
295
|
+
bestResult = result;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Keep best result so far (prioritize correct length)
|
|
300
|
+
if (hasText) {
|
|
301
|
+
if (!bestResult) {
|
|
302
|
+
bestResult = result;
|
|
303
|
+
} else if (expectedLength) {
|
|
304
|
+
// Prefer correct length
|
|
305
|
+
if (result.text.length === expectedLength && bestResult.text.length !== expectedLength) {
|
|
306
|
+
bestResult = result;
|
|
307
|
+
} else if (result.confidence > bestResult.confidence) {
|
|
308
|
+
bestResult = result;
|
|
309
|
+
}
|
|
310
|
+
} else if (result.confidence > bestResult.confidence) {
|
|
311
|
+
bestResult = result;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
log.debug(` OCR error: ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Stop if we found a good result
|
|
320
|
+
if (bestResult && bestResult.confidence >= confidence) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (bestResult && bestResult.text) {
|
|
326
|
+
log.success(`Solved: "${bestResult.text}" (confidence: ${bestResult.confidence.toFixed(1)}%, config: ${bestResult.preprocessConfig})`);
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
text: bestResult.text,
|
|
330
|
+
confidence: bestResult.confidence,
|
|
331
|
+
attempts: allAttempts.length,
|
|
332
|
+
allAttempts,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
log.warn('OCR could not recognize text');
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
text: bestResult?.text || '',
|
|
340
|
+
confidence: bestResult?.confidence || 0,
|
|
341
|
+
attempts: allAttempts.length,
|
|
342
|
+
allAttempts,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
} catch (error) {
|
|
346
|
+
log.error(`Failed to solve captcha: ${error.message}`);
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
text: '',
|
|
350
|
+
confidence: 0,
|
|
351
|
+
error: error.message,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Solve captcha and fill input field with auto-retry
|
|
358
|
+
*/
|
|
359
|
+
async function solveCaptchaAndFill(page, captchaSelector, inputSelector, options = {}) {
|
|
360
|
+
const {
|
|
361
|
+
humanLike = true,
|
|
362
|
+
submitAfter = false,
|
|
363
|
+
submitSelector = 'button[type="submit"], input[type="submit"], button.btn-primary, input.btn',
|
|
364
|
+
refreshSelector = null,
|
|
365
|
+
maxRefreshAttempts = 5, // Increased retries
|
|
366
|
+
minConfidence = 40, // Minimum confidence to try
|
|
367
|
+
expectedLength = null,
|
|
368
|
+
lang = 'eng',
|
|
369
|
+
allowedChars = null,
|
|
370
|
+
waitAfterRefresh = 1500, // Wait after refresh
|
|
371
|
+
waitBeforeType = 500, // Wait before typing
|
|
372
|
+
} = options;
|
|
373
|
+
|
|
374
|
+
let attempts = 0;
|
|
375
|
+
let lastResult = null;
|
|
376
|
+
|
|
377
|
+
log.info(`Starting captcha solve with ${maxRefreshAttempts} max attempts`);
|
|
378
|
+
|
|
379
|
+
while (attempts < maxRefreshAttempts) {
|
|
380
|
+
attempts++;
|
|
381
|
+
log.info(`Attempt ${attempts}/${maxRefreshAttempts}`);
|
|
382
|
+
|
|
383
|
+
// Wait for image to load properly
|
|
384
|
+
await new Promise(r => setTimeout(r, waitBeforeType));
|
|
385
|
+
|
|
386
|
+
// Solve captcha
|
|
387
|
+
const result = await solveTextCaptcha(page, captchaSelector, {
|
|
388
|
+
lang,
|
|
389
|
+
expectedLength,
|
|
390
|
+
allowedChars,
|
|
391
|
+
confidence: minConfidence,
|
|
392
|
+
tryAllPreprocess: attempts <= 2, // Only try all configs on first 2 attempts
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
lastResult = result;
|
|
396
|
+
|
|
397
|
+
// Check if we got a usable result
|
|
398
|
+
if (!result.text || result.text.length === 0) {
|
|
399
|
+
log.warn('No text recognized');
|
|
400
|
+
if (refreshSelector) {
|
|
401
|
+
log.info('Refreshing captcha...');
|
|
402
|
+
try {
|
|
403
|
+
await page.click(refreshSelector);
|
|
404
|
+
await new Promise(r => setTimeout(r, waitAfterRefresh));
|
|
405
|
+
} catch (e) {
|
|
406
|
+
log.warn(`Refresh failed: ${e.message}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// If expected length specified and doesn't match, refresh
|
|
413
|
+
if (expectedLength && result.text.length !== expectedLength) {
|
|
414
|
+
log.warn(`Length mismatch: got ${result.text.length}, expected ${expectedLength}`);
|
|
415
|
+
if (refreshSelector && attempts < maxRefreshAttempts) {
|
|
416
|
+
log.info('Refreshing captcha for better result...');
|
|
417
|
+
await page.click(refreshSelector);
|
|
418
|
+
await new Promise(r => setTimeout(r, waitAfterRefresh));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Fill the input
|
|
424
|
+
const input = await page.$(inputSelector);
|
|
425
|
+
if (!input) {
|
|
426
|
+
return { success: false, error: 'Input field not found', attempts };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Clear field
|
|
430
|
+
await page.click(inputSelector, { clickCount: 3 });
|
|
431
|
+
await page.keyboard.press('Backspace');
|
|
432
|
+
await new Promise(r => setTimeout(r, 100));
|
|
433
|
+
|
|
434
|
+
// Type the captcha
|
|
435
|
+
if (humanLike) {
|
|
436
|
+
for (const char of result.text) {
|
|
437
|
+
await page.keyboard.type(char);
|
|
438
|
+
await new Promise(r => setTimeout(r, 30 + Math.random() * 80));
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
await page.type(inputSelector, result.text);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
log.success(`Filled captcha: "${result.text}"`);
|
|
445
|
+
|
|
446
|
+
// Submit if requested
|
|
447
|
+
if (submitAfter) {
|
|
448
|
+
try {
|
|
449
|
+
await page.click(submitSelector);
|
|
450
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
451
|
+
|
|
452
|
+
// Check if captcha was wrong
|
|
453
|
+
const stillOnPage = await page.$(captchaSelector);
|
|
454
|
+
const errorVisible = await page.evaluate(() => {
|
|
455
|
+
const errorSelectors = ['.error', '.alert-danger', '.captcha-error', '#captchaError'];
|
|
456
|
+
return errorSelectors.some(sel => {
|
|
457
|
+
const el = document.querySelector(sel);
|
|
458
|
+
return el && el.offsetParent !== null;
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if ((stillOnPage || errorVisible) && refreshSelector) {
|
|
463
|
+
log.warn('Captcha might be wrong, retrying...');
|
|
464
|
+
await page.click(refreshSelector);
|
|
465
|
+
await new Promise(r => setTimeout(r, waitAfterRefresh));
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
469
|
+
log.warn(`Submit error: ${e.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
success: true,
|
|
475
|
+
text: result.text,
|
|
476
|
+
confidence: result.confidence,
|
|
477
|
+
attempts,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
success: false,
|
|
483
|
+
error: 'Max attempts reached',
|
|
484
|
+
attempts,
|
|
485
|
+
lastResult,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Solve captcha from URL directly
|
|
491
|
+
*/
|
|
492
|
+
async function solveCaptchaFromUrl(imageUrl, options = {}) {
|
|
493
|
+
log.info(`Solving captcha from URL: ${imageUrl}`);
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const result = await recognizeText(imageUrl, {
|
|
497
|
+
...DEFAULT_OCR_CONFIG,
|
|
498
|
+
...options,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
success: result.confidence > 40 && result.text.length > 0,
|
|
503
|
+
text: result.text,
|
|
504
|
+
confidence: result.confidence,
|
|
505
|
+
};
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return {
|
|
508
|
+
success: false,
|
|
509
|
+
error: error.message,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Export functions
|
|
515
|
+
module.exports = {
|
|
516
|
+
solveTextCaptcha,
|
|
517
|
+
solveCaptchaAndFill,
|
|
518
|
+
solveCaptchaFromUrl,
|
|
519
|
+
getCaptchaImage,
|
|
520
|
+
recognizeText,
|
|
521
|
+
preprocessImageAdvanced,
|
|
522
|
+
CHAR_SUBSTITUTIONS,
|
|
523
|
+
DEFAULT_OCR_CONFIG,
|
|
524
|
+
PREPROCESS_CONFIGS,
|
|
525
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-browser-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.41.1",
|
|
4
4
|
"description": "MCP Server for Brave Real Browser - Puppeteer with Brave Browser, Stealth Mode, Ad Blocker, and Turnstile Auto-Solver for undetectable web automation.",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"module": "lib/esm/index.mjs",
|
|
@@ -43,7 +43,10 @@
|
|
|
43
43
|
"lint": "echo 'No linting configured'",
|
|
44
44
|
"build:all": "npm run build --workspaces --if-present",
|
|
45
45
|
"clean:all": "npm run clean --workspaces --if-present",
|
|
46
|
-
"test:all": "npm run test --workspaces --if-present"
|
|
46
|
+
"test:all": "npm run test --workspaces --if-present",
|
|
47
|
+
"postinstall": "node scripts/auto-update-deps.js || true",
|
|
48
|
+
"update-deps": "node scripts/auto-update-deps.js",
|
|
49
|
+
"upstream-patch": "node scripts/upstream-patcher.js"
|
|
47
50
|
},
|
|
48
51
|
"keywords": [
|
|
49
52
|
"mcp-server",
|
|
@@ -76,6 +79,7 @@
|
|
|
76
79
|
"ghost-cursor": "^1.4.2",
|
|
77
80
|
"puppeteer-extra": "^3.3.6",
|
|
78
81
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
82
|
+
"tesseract.js": "^5.1.1",
|
|
79
83
|
"tree-kill": "^1.2.2",
|
|
80
84
|
"vscode-languageserver": "^9.0.1",
|
|
81
85
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-blocker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"description": "Advanced uBlock Origin management and stealth features for Brave Real Browser",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/node": "^20.0.0",
|
|
67
67
|
"brave-real-puppeteer-core": "^24.36.NaN.1",
|
|
68
68
|
"mocha": "^10.4.0",
|
|
69
|
-
"puppeteer-core": "
|
|
69
|
+
"puppeteer-core": ">=24.0.0",
|
|
70
70
|
"sinon": "^17.0.1",
|
|
71
71
|
"ts-node": "^10.9.2",
|
|
72
72
|
"tsup": "^8.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-launcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "Launch Brave Browser with ease from node. Based on chrome-launcher with Brave-specific support.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"typescript": "^5.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"brave-real-blocker": "^1.
|
|
57
|
+
"brave-real-blocker": "^1.17.1",
|
|
58
58
|
"escape-string-regexp": "^5.0.0",
|
|
59
59
|
"is-wsl": "^3.1.0",
|
|
60
60
|
"which": "^6.0.0"
|
|
@@ -134,13 +134,13 @@
|
|
|
134
134
|
"test-version": "node ./scripts/test-version-management.js"
|
|
135
135
|
},
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"brave-real-launcher": "^1.
|
|
137
|
+
"brave-real-launcher": "^1.23.1",
|
|
138
138
|
"get-east-asian-width": "^1.4.0",
|
|
139
139
|
"yargs": "^18.0.0"
|
|
140
140
|
},
|
|
141
141
|
"optionalDependencies": {
|
|
142
|
-
"playwright-core": "
|
|
143
|
-
"puppeteer-core": "
|
|
142
|
+
"playwright-core": ">=1.40.0",
|
|
143
|
+
"puppeteer-core": ">=24.0.0"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
146
|
"test": "^3.3.0"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Auto-Update Dependencies Script
|
|
4
|
+
*
|
|
5
|
+
* यह script npm install के बाद automatically चलता है और:
|
|
6
|
+
* 1. सभी external dependencies को latest version पर update करता है
|
|
7
|
+
* 2. Internal workspace packages को sync रखता है
|
|
8
|
+
* 3. puppeteer-core और playwright-core को latest पर रखता है
|
|
9
|
+
*
|
|
10
|
+
* Usage: Automatically runs via postinstall hook
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// Colors for console
|
|
18
|
+
const colors = {
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
blue: '\x1b[34m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bold: '\x1b[1m'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const log = {
|
|
28
|
+
info: (msg) => console.log(`${colors.blue}[auto-update]${colors.reset} ${msg}`),
|
|
29
|
+
success: (msg) => console.log(`${colors.green}[auto-update]${colors.reset} ${msg}`),
|
|
30
|
+
warn: (msg) => console.log(`${colors.yellow}[auto-update]${colors.reset} ${msg}`),
|
|
31
|
+
error: (msg) => console.log(`${colors.red}[auto-update]${colors.reset} ${msg}`)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Skip if CI environment and SKIP_AUTO_UPDATE is set
|
|
35
|
+
if (process.env.SKIP_AUTO_UPDATE === 'true') {
|
|
36
|
+
log.info('Skipping auto-update (SKIP_AUTO_UPDATE=true)');
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Skip during npm publish
|
|
41
|
+
if (process.env.npm_command === 'publish') {
|
|
42
|
+
log.info('Skipping auto-update during publish');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Critical dependencies to always keep updated
|
|
47
|
+
const CRITICAL_DEPS = [
|
|
48
|
+
'puppeteer-core',
|
|
49
|
+
'playwright-core',
|
|
50
|
+
'@modelcontextprotocol/sdk'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Packages to update (external dependencies)
|
|
54
|
+
const PACKAGES_TO_UPDATE = [
|
|
55
|
+
'ghost-cursor',
|
|
56
|
+
'puppeteer-extra',
|
|
57
|
+
'puppeteer-extra-plugin-stealth',
|
|
58
|
+
'puppeteer-extra-plugin-adblocker',
|
|
59
|
+
'@ghostery/adblocker-puppeteer'
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
async function getLatestVersion(packageName) {
|
|
63
|
+
try {
|
|
64
|
+
const result = execSync(`npm view ${packageName} version`, {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
67
|
+
}).trim();
|
|
68
|
+
return result;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getCurrentVersion(packageName) {
|
|
75
|
+
try {
|
|
76
|
+
const result = execSync(`npm list ${packageName} --depth=0 --json`, {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
79
|
+
});
|
|
80
|
+
const data = JSON.parse(result);
|
|
81
|
+
return data.dependencies?.[packageName]?.version || null;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function updateDependencies() {
|
|
88
|
+
log.info('Checking for dependency updates...');
|
|
89
|
+
|
|
90
|
+
const updates = [];
|
|
91
|
+
|
|
92
|
+
// Check critical dependencies
|
|
93
|
+
for (const pkg of CRITICAL_DEPS) {
|
|
94
|
+
const latest = await getLatestVersion(pkg);
|
|
95
|
+
const current = await getCurrentVersion(pkg);
|
|
96
|
+
|
|
97
|
+
if (latest && current && latest !== current) {
|
|
98
|
+
updates.push({ name: pkg, current, latest, critical: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check other packages
|
|
103
|
+
for (const pkg of PACKAGES_TO_UPDATE) {
|
|
104
|
+
const latest = await getLatestVersion(pkg);
|
|
105
|
+
const current = await getCurrentVersion(pkg);
|
|
106
|
+
|
|
107
|
+
if (latest && current && latest !== current) {
|
|
108
|
+
updates.push({ name: pkg, current, latest, critical: false });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (updates.length === 0) {
|
|
113
|
+
log.success('All dependencies are up to date!');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Show what will be updated
|
|
118
|
+
log.info(`Found ${updates.length} packages to update:`);
|
|
119
|
+
updates.forEach(u => {
|
|
120
|
+
const icon = u.critical ? '🔴' : '🔵';
|
|
121
|
+
console.log(` ${icon} ${u.name}: ${u.current} → ${u.latest}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
log.info('Run "npm update" to update these packages.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function updateBasedOnField() {
|
|
128
|
+
log.info('Checking basedOn field updates...');
|
|
129
|
+
|
|
130
|
+
const packagesDir = path.join(__dirname, '..', 'packages');
|
|
131
|
+
const updates = [];
|
|
132
|
+
|
|
133
|
+
// Check brave-real-puppeteer-core
|
|
134
|
+
const puppeteerPkgPath = path.join(packagesDir, 'brave-real-puppeteer-core', 'package.json');
|
|
135
|
+
if (fs.existsSync(puppeteerPkgPath)) {
|
|
136
|
+
const pkg = JSON.parse(fs.readFileSync(puppeteerPkgPath, 'utf8'));
|
|
137
|
+
const basedOn = pkg.brave?.basedOn?.['puppeteer-core'];
|
|
138
|
+
const latestPuppeteer = await getLatestVersion('puppeteer-core');
|
|
139
|
+
|
|
140
|
+
if (basedOn && latestPuppeteer && basedOn !== latestPuppeteer) {
|
|
141
|
+
updates.push({
|
|
142
|
+
package: 'brave-real-puppeteer-core',
|
|
143
|
+
basedOn,
|
|
144
|
+
latest: latestPuppeteer,
|
|
145
|
+
upstream: 'puppeteer-core'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check brave-real-playwright-core
|
|
151
|
+
const playwrightPkgPath = path.join(packagesDir, 'brave-real-playwright-core', 'package.json');
|
|
152
|
+
if (fs.existsSync(playwrightPkgPath)) {
|
|
153
|
+
const pkg = JSON.parse(fs.readFileSync(playwrightPkgPath, 'utf8'));
|
|
154
|
+
const basedOn = pkg.brave?.basedOn?.['playwright-core'];
|
|
155
|
+
const latestPlaywright = await getLatestVersion('playwright-core');
|
|
156
|
+
|
|
157
|
+
if (basedOn && latestPlaywright && basedOn !== latestPlaywright) {
|
|
158
|
+
updates.push({
|
|
159
|
+
package: 'brave-real-playwright-core',
|
|
160
|
+
basedOn,
|
|
161
|
+
latest: latestPlaywright,
|
|
162
|
+
upstream: 'playwright-core'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (updates.length > 0) {
|
|
168
|
+
log.warn('Upstream updates available:');
|
|
169
|
+
updates.forEach(u => {
|
|
170
|
+
console.log(` ⬆️ ${u.package}: ${u.upstream} ${u.basedOn} → ${u.latest}`);
|
|
171
|
+
});
|
|
172
|
+
log.info('Run "npm run upstream-patch" to update to latest upstream');
|
|
173
|
+
} else {
|
|
174
|
+
log.success('All packages are based on latest upstream versions!');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Main execution
|
|
179
|
+
async function main() {
|
|
180
|
+
console.log(`\n${colors.bold}🔄 Auto-Update Dependencies${colors.reset}\n`);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await updateDependencies();
|
|
184
|
+
await updateBasedOnField();
|
|
185
|
+
console.log('');
|
|
186
|
+
} catch (e) {
|
|
187
|
+
log.error('Auto-update failed: ' + e.message);
|
|
188
|
+
// Don't fail the install
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main();
|
package/src/mcp/handlers.js
CHANGED
|
@@ -506,13 +506,105 @@ const handlers = {
|
|
|
506
506
|
return { success: true, message: 'Browser closed' };
|
|
507
507
|
},
|
|
508
508
|
|
|
509
|
-
|
|
509
|
+
// 8. Solve Captcha (Enhanced with OCR for text captchas)
|
|
510
510
|
async solve_captcha(params = {}) {
|
|
511
511
|
const { page } = requireBrowser();
|
|
512
|
-
const {
|
|
512
|
+
const {
|
|
513
|
+
type = 'auto',
|
|
514
|
+
timeout = 30000,
|
|
515
|
+
// OCR-specific options
|
|
516
|
+
captchaSelector,
|
|
517
|
+
inputSelector,
|
|
518
|
+
refreshSelector,
|
|
519
|
+
lang = 'eng',
|
|
520
|
+
expectedLength,
|
|
521
|
+
allowedChars,
|
|
522
|
+
maxRetries = 3
|
|
523
|
+
} = params;
|
|
513
524
|
|
|
514
525
|
notifyProgress('solve_captcha', 'started', `Solving ${type} captcha...`);
|
|
515
526
|
|
|
527
|
+
// Handle text/image captcha with OCR
|
|
528
|
+
if (type === 'text' || type === 'image') {
|
|
529
|
+
if (!captchaSelector) {
|
|
530
|
+
return { success: false, error: 'captchaSelector is required for text/image captcha' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
// Dynamic import for OCR solver
|
|
535
|
+
const ocrSolver = require('../../lib/ocr-captcha-solver');
|
|
536
|
+
|
|
537
|
+
if (inputSelector) {
|
|
538
|
+
// Solve and fill
|
|
539
|
+
const result = await ocrSolver.solveCaptchaAndFill(page, captchaSelector, inputSelector, {
|
|
540
|
+
lang,
|
|
541
|
+
expectedLength,
|
|
542
|
+
allowedChars,
|
|
543
|
+
refreshSelector,
|
|
544
|
+
maxRefreshAttempts: maxRetries,
|
|
545
|
+
humanLike: true
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
notifyProgress('solve_captcha', result.success ? 'completed' : 'error',
|
|
549
|
+
result.success ? `OCR solved: "${result.text}"` : `OCR failed: ${result.error}`);
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
success: result.success,
|
|
553
|
+
type: 'ocr',
|
|
554
|
+
text: result.text,
|
|
555
|
+
confidence: result.confidence,
|
|
556
|
+
attempts: result.attempts,
|
|
557
|
+
filled: true
|
|
558
|
+
};
|
|
559
|
+
} else {
|
|
560
|
+
// Just solve (don't fill)
|
|
561
|
+
const result = await ocrSolver.solveTextCaptcha(page, captchaSelector, {
|
|
562
|
+
lang,
|
|
563
|
+
expectedLength,
|
|
564
|
+
allowedChars,
|
|
565
|
+
retries: maxRetries
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
notifyProgress('solve_captcha', result.success ? 'completed' : 'error',
|
|
569
|
+
result.success ? `OCR result: "${result.text}"` : 'OCR failed');
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
success: result.success,
|
|
573
|
+
type: 'ocr',
|
|
574
|
+
text: result.text,
|
|
575
|
+
confidence: result.confidence,
|
|
576
|
+
attempts: result.attempts,
|
|
577
|
+
filled: false
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
notifyProgress('solve_captcha', 'error', `OCR error: ${err.message}`);
|
|
582
|
+
return { success: false, error: err.message, type: 'ocr' };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Auto-detect captcha type
|
|
587
|
+
if (type === 'auto') {
|
|
588
|
+
// Check for text/image captcha first
|
|
589
|
+
const hasTextCaptcha = await page.evaluate(() => {
|
|
590
|
+
const captchaIndicators = [
|
|
591
|
+
'img[src*="captcha"]',
|
|
592
|
+
'img[alt*="captcha"]',
|
|
593
|
+
'img[id*="captcha"]',
|
|
594
|
+
'.captcha-image',
|
|
595
|
+
'#captcha-image',
|
|
596
|
+
'img[src*="Captcha"]'
|
|
597
|
+
];
|
|
598
|
+
return captchaIndicators.some(sel => document.querySelector(sel));
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (hasTextCaptcha && captchaSelector) {
|
|
602
|
+
// Recursively call with text type
|
|
603
|
+
return this.solve_captcha({ ...params, type: 'text' });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Original Turnstile/reCAPTCHA/hCaptcha handling
|
|
516
608
|
const start = Date.now();
|
|
517
609
|
let attempts = 0;
|
|
518
610
|
|
package/src/shared/tools.js
CHANGED
|
@@ -173,21 +173,34 @@ const TOOLS = [
|
|
|
173
173
|
}
|
|
174
174
|
},
|
|
175
175
|
|
|
176
|
-
// 8. Solve Captcha
|
|
176
|
+
// 8. Solve Captcha (Enhanced with OCR for text captchas)
|
|
177
177
|
{
|
|
178
178
|
name: 'solve_captcha',
|
|
179
179
|
emoji: '🔓',
|
|
180
|
-
description: 'Auto-solve CAPTCHA with AI (Turnstile, reCAPTCHA, hCaptcha)',
|
|
181
|
-
descriptionHindi: 'CAPTCHA हल करना (AI
|
|
180
|
+
description: 'Auto-solve CAPTCHA with AI (Turnstile, reCAPTCHA, hCaptcha, Text/Image OCR)',
|
|
181
|
+
descriptionHindi: 'CAPTCHA हल करना (AI + OCR powered)',
|
|
182
182
|
category: 'interaction',
|
|
183
183
|
requiresBrowser: true,
|
|
184
184
|
requiresPage: true,
|
|
185
185
|
inputSchema: {
|
|
186
186
|
type: 'object',
|
|
187
187
|
properties: {
|
|
188
|
-
type: {
|
|
188
|
+
type: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
enum: ['turnstile', 'recaptcha', 'hcaptcha', 'text', 'image', 'auto'],
|
|
191
|
+
default: 'auto',
|
|
192
|
+
description: 'Captcha type: turnstile/recaptcha/hcaptcha (JS-based), text/image (OCR-based), auto (detect)'
|
|
193
|
+
},
|
|
189
194
|
timeout: { type: 'number', default: 30000 },
|
|
190
|
-
aiMode: { type: 'boolean', default: true, description: 'Use AI vision for complex CAPTCHAs' }
|
|
195
|
+
aiMode: { type: 'boolean', default: true, description: 'Use AI vision for complex CAPTCHAs' },
|
|
196
|
+
// OCR-specific options for text/image captchas
|
|
197
|
+
captchaSelector: { type: 'string', description: 'CSS selector for captcha image (required for text/image type)' },
|
|
198
|
+
inputSelector: { type: 'string', description: 'CSS selector for input field to fill result' },
|
|
199
|
+
refreshSelector: { type: 'string', description: 'CSS selector for captcha refresh button' },
|
|
200
|
+
lang: { type: 'string', default: 'eng', description: 'OCR language: eng, hin, eng+hin' },
|
|
201
|
+
expectedLength: { type: 'number', description: 'Expected captcha text length' },
|
|
202
|
+
allowedChars: { type: 'string', description: 'Allowed characters in captcha' },
|
|
203
|
+
maxRetries: { type: 'number', default: 3, description: 'Max refresh attempts for OCR' }
|
|
191
204
|
}
|
|
192
205
|
}
|
|
193
206
|
},
|