brave-real-browser-mcp-server 2.41.0 → 2.41.2
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 +3 -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 +2 -2
- package/scripts/version-bump.js +79 -14
- 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.41.
|
|
3
|
+
"version": "2.41.2",
|
|
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",
|
|
@@ -75,10 +75,11 @@
|
|
|
75
75
|
"license": "ISC",
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
78
|
-
"brave-real-puppeteer-core": "^24.
|
|
78
|
+
"brave-real-puppeteer-core": "^24.36.1-brave.2",
|
|
79
79
|
"ghost-cursor": "^1.4.2",
|
|
80
80
|
"puppeteer-extra": "^3.3.6",
|
|
81
81
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
82
|
+
"tesseract.js": "^5.1.1",
|
|
82
83
|
"tree-kill": "^1.2.2",
|
|
83
84
|
"vscode-languageserver": "^9.0.1",
|
|
84
85
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-blocker",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/adm-zip": "^0.5.5",
|
|
65
65
|
"@types/fs-extra": "^11.0.4",
|
|
66
66
|
"@types/node": "^20.0.0",
|
|
67
|
-
"brave-real-puppeteer-core": "^24.
|
|
67
|
+
"brave-real-puppeteer-core": "^24.36.1-brave.2",
|
|
68
68
|
"mocha": "^10.4.0",
|
|
69
69
|
"puppeteer-core": ">=24.0.0",
|
|
70
70
|
"sinon": "^17.0.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-launcher",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.2",
|
|
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.17.
|
|
57
|
+
"brave-real-blocker": "^1.17.2",
|
|
58
58
|
"escape-string-regexp": "^5.0.0",
|
|
59
59
|
"is-wsl": "^3.1.0",
|
|
60
60
|
"which": "^6.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-puppeteer-core",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.36.1-brave.2",
|
|
4
4
|
"description": "🦁 Brave Real-World Optimized Puppeteer & Playwright Core with 1-5ms ultra-fast timing, 50+ professional stealth features, intelligent browser auto-detection, and 100% bot detection bypass. Features cross-platform Brave browser integration, comprehensive anti-detection, and breakthrough performance improvements.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"test-version": "node ./scripts/test-version-management.js"
|
|
135
135
|
},
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"brave-real-launcher": "^1.23.
|
|
137
|
+
"brave-real-launcher": "^1.23.2",
|
|
138
138
|
"get-east-asian-width": "^1.4.0",
|
|
139
139
|
"yargs": "^18.0.0"
|
|
140
140
|
},
|
package/scripts/version-bump.js
CHANGED
|
@@ -31,6 +31,9 @@ const PACKAGES = [
|
|
|
31
31
|
];
|
|
32
32
|
|
|
33
33
|
function incrementVersion(version, type) {
|
|
34
|
+
// Clean the version string first
|
|
35
|
+
version = String(version).trim();
|
|
36
|
+
|
|
34
37
|
// Handle versions with -patch suffix (e.g., "1.57.0-patch.15")
|
|
35
38
|
const patchSuffixMatch = version.match(/^(.+)-patch\.(\d+)$/);
|
|
36
39
|
if (patchSuffixMatch) {
|
|
@@ -39,26 +42,49 @@ function incrementVersion(version, type) {
|
|
|
39
42
|
return `${baseVersion}-patch.${patchNum + 1}`;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
45
|
+
// Handle versions with -brave suffix (e.g., "24.36.1-brave.1")
|
|
46
|
+
const braveSuffixMatch = version.match(/^(.+)-brave\.(\d+)$/);
|
|
47
|
+
if (braveSuffixMatch) {
|
|
48
|
+
const baseVersion = braveSuffixMatch[1];
|
|
49
|
+
const braveNum = parseInt(braveSuffixMatch[2], 10);
|
|
50
|
+
return `${baseVersion}-brave.${braveNum + 1}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Extract only the semver part (ignore any prerelease/build metadata)
|
|
54
|
+
const semverMatch = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
55
|
+
if (!semverMatch) {
|
|
56
|
+
console.warn(` ⚠️ Invalid version format: "${version}", defaulting to 1.0.0`);
|
|
57
|
+
return '1.0.1';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse version parts, ensuring valid numbers
|
|
61
|
+
let major = parseInt(semverMatch[1], 10) || 0;
|
|
62
|
+
let minor = parseInt(semverMatch[2], 10) || 0;
|
|
63
|
+
let patch = parseInt(semverMatch[3], 10) || 0;
|
|
64
|
+
|
|
65
|
+
// Validate - must be valid numbers
|
|
66
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
67
|
+
console.warn(` ⚠️ Version contains NaN: "${version}", defaulting to 1.0.1`);
|
|
68
|
+
return '1.0.1';
|
|
69
|
+
}
|
|
44
70
|
|
|
45
71
|
switch (type) {
|
|
46
72
|
case 'major':
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
major++;
|
|
74
|
+
minor = 0;
|
|
75
|
+
patch = 0;
|
|
50
76
|
break;
|
|
51
77
|
case 'minor':
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
minor++;
|
|
79
|
+
patch = 0;
|
|
54
80
|
break;
|
|
55
81
|
case 'patch':
|
|
56
82
|
default:
|
|
57
|
-
|
|
83
|
+
patch++;
|
|
58
84
|
break;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
return
|
|
87
|
+
return `${major}.${minor}.${patch}`;
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
function getNpmVersion(packageName) {
|
|
@@ -91,12 +117,48 @@ function writePackageJson(pkgPath, data) {
|
|
|
91
117
|
function compareVersions(v1, v2) {
|
|
92
118
|
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
93
119
|
const parse = (v) => {
|
|
120
|
+
v = String(v).trim();
|
|
121
|
+
|
|
122
|
+
// Handle -patch suffix
|
|
94
123
|
const patchMatch = v.match(/^(.+)-patch\.(\d+)$/);
|
|
95
124
|
if (patchMatch) {
|
|
96
|
-
const
|
|
97
|
-
|
|
125
|
+
const semverMatch = patchMatch[1].match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
126
|
+
if (semverMatch) {
|
|
127
|
+
return [
|
|
128
|
+
parseInt(semverMatch[1], 10) || 0,
|
|
129
|
+
parseInt(semverMatch[2], 10) || 0,
|
|
130
|
+
parseInt(semverMatch[3], 10) || 0,
|
|
131
|
+
parseInt(patchMatch[2], 10) || 0
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle -brave suffix
|
|
137
|
+
const braveMatch = v.match(/^(.+)-brave\.(\d+)$/);
|
|
138
|
+
if (braveMatch) {
|
|
139
|
+
const semverMatch = braveMatch[1].match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
140
|
+
if (semverMatch) {
|
|
141
|
+
return [
|
|
142
|
+
parseInt(semverMatch[1], 10) || 0,
|
|
143
|
+
parseInt(semverMatch[2], 10) || 0,
|
|
144
|
+
parseInt(semverMatch[3], 10) || 0,
|
|
145
|
+
parseInt(braveMatch[2], 10) || 0
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Standard semver parsing - extract only digits
|
|
151
|
+
const semverMatch = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
152
|
+
if (semverMatch) {
|
|
153
|
+
return [
|
|
154
|
+
parseInt(semverMatch[1], 10) || 0,
|
|
155
|
+
parseInt(semverMatch[2], 10) || 0,
|
|
156
|
+
parseInt(semverMatch[3], 10) || 0
|
|
157
|
+
];
|
|
98
158
|
}
|
|
99
|
-
|
|
159
|
+
|
|
160
|
+
// Fallback - return zeros
|
|
161
|
+
return [0, 0, 0];
|
|
100
162
|
};
|
|
101
163
|
|
|
102
164
|
const parts1 = parse(v1);
|
|
@@ -105,8 +167,11 @@ function compareVersions(v1, v2) {
|
|
|
105
167
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
106
168
|
const a = parts1[i] || 0;
|
|
107
169
|
const b = parts2[i] || 0;
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
// Extra safety - ensure numbers
|
|
171
|
+
const numA = isNaN(a) ? 0 : a;
|
|
172
|
+
const numB = isNaN(b) ? 0 : b;
|
|
173
|
+
if (numA > numB) return 1;
|
|
174
|
+
if (numA < numB) return -1;
|
|
110
175
|
}
|
|
111
176
|
return 0;
|
|
112
177
|
}
|
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
|
},
|