real-browser-mcp-server 1.1.7 → 1.1.8
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/dist/lib/cjs/index.js +384 -0
- package/{lib → dist/lib}/cjs/module/pageController.js +27 -29
- package/{lib → dist/lib}/cjs/module/turnstile.js +23 -12
- package/dist/src/ai/action-parser.js +229 -0
- package/dist/src/ai/core.js +367 -0
- package/dist/src/ai/element-finder.js +409 -0
- package/{src → dist/src}/ai/index.js +35 -50
- package/dist/src/ai/page-analyzer.js +264 -0
- package/dist/src/ai/selector-healer.js +215 -0
- package/dist/src/index.js +116 -0
- package/dist/src/mcp/handlers/browser.js +230 -0
- package/dist/src/mcp/handlers/dom.js +550 -0
- package/dist/src/mcp/handlers/extract.js +451 -0
- package/dist/src/mcp/handlers/helpers.js +514 -0
- package/dist/src/mcp/handlers/index.js +63 -0
- package/dist/src/mcp/handlers/misc.js +1224 -0
- package/dist/src/mcp/handlers/network.js +1134 -0
- package/dist/src/mcp/handlers/state.js +215 -0
- package/dist/src/mcp/handlers/vision.js +475 -0
- package/dist/src/mcp/index.js +166 -0
- package/dist/src/mcp/server.js +117 -0
- package/{src → dist/src}/mcp/tools.js +12 -11
- package/dist/src/shared/tools.js +598 -0
- package/{test → dist/test}/cjs/test.js +119 -169
- package/dist/test/mcp/smoke-test.js +131 -0
- package/lib/esm/module/pageController.mjs +21 -18
- package/lib/esm/module/turnstile.mjs +7 -0
- package/package.json +22 -11
- package/.github/ISSUE_TEMPLATE/general_issue.yaml +0 -58
- package/.github/SETUP.md +0 -111
- package/.github/workflows/publish.yml +0 -162
- package/Dockerfile +0 -78
- package/lib/cjs/adblocker.bin +0 -0
- package/lib/cjs/index.js +0 -396
- package/src/ai/action-parser.js +0 -269
- package/src/ai/core.js +0 -379
- package/src/ai/element-finder.js +0 -466
- package/src/ai/page-analyzer.js +0 -295
- package/src/ai/selector-healer.js +0 -236
- package/src/index.js +0 -128
- package/src/mcp/handlers.js +0 -5306
- package/src/mcp/index.js +0 -190
- package/src/mcp/server.js +0 -141
- package/src/shared/tools.js +0 -625
- package/test/esm/test.mjs +0 -299
- package/test/mcp/smoke-test.js +0 -141
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Auto-generated helpers handlers
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.helpersHandlers = void 0;
|
|
6
|
+
exports.helpersHandlers = {
|
|
7
|
+
async _handleBlockingModals(page) {
|
|
8
|
+
try {
|
|
9
|
+
const closed = await page.evaluate(() => {
|
|
10
|
+
// Selectors for common modal close buttons
|
|
11
|
+
const closeSelectors = [
|
|
12
|
+
// Bootstrap/Standard Modals
|
|
13
|
+
'.modal.show .btn-close',
|
|
14
|
+
'.modal.show .close',
|
|
15
|
+
'.modal.in .close',
|
|
16
|
+
'.modal-footer .btn-primary', // "OK" button usually
|
|
17
|
+
'.modal-footer .btn-secondary', // "Close" button
|
|
18
|
+
// Custom Overlays
|
|
19
|
+
'#modal-close',
|
|
20
|
+
'.popup-close',
|
|
21
|
+
'.overlay-close',
|
|
22
|
+
// Generic "X" buttons in overlays
|
|
23
|
+
'div[role="dialog"] button[aria-label="Close"]',
|
|
24
|
+
'div[role="dialog"] .close',
|
|
25
|
+
// SweetAlert / specific libraries
|
|
26
|
+
'.swal2-confirm',
|
|
27
|
+
'.swal2-cancel',
|
|
28
|
+
'.ui-dialog-titlebar-close',
|
|
29
|
+
// eCourts specific if known (generic fallback)
|
|
30
|
+
'.modal-header .close',
|
|
31
|
+
'button[data-dismiss="modal"]'
|
|
32
|
+
];
|
|
33
|
+
let clicked = false;
|
|
34
|
+
// Check if any modal is visible (display block/flex and opacity > 0)
|
|
35
|
+
const modals = document.querySelectorAll('.modal, .popup, .overlay, .dialog, [role="dialog"]');
|
|
36
|
+
for (const modal of modals) {
|
|
37
|
+
const style = window.getComputedStyle(modal);
|
|
38
|
+
if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0') {
|
|
39
|
+
// Modal is visible, find a close button inside
|
|
40
|
+
for (const selector of closeSelectors) {
|
|
41
|
+
const btn = modal.querySelector(selector);
|
|
42
|
+
if (btn && btn.offsetParent !== null) { // Visible button
|
|
43
|
+
btn.click();
|
|
44
|
+
clicked = true;
|
|
45
|
+
break; // Clicked one, break inner loop
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (clicked)
|
|
49
|
+
break; // Handled one modal, break outer loop
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return clicked;
|
|
53
|
+
});
|
|
54
|
+
if (closed) {
|
|
55
|
+
// notifyProgress('helper', 'progress', '🧹 Auto-closed a blocking modal/popup');
|
|
56
|
+
await new Promise(r => setTimeout(r, 500)); // Wait for animation
|
|
57
|
+
}
|
|
58
|
+
return closed;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async _analyzeFullPage(page) {
|
|
65
|
+
return await page.evaluate(() => {
|
|
66
|
+
const inputs = [];
|
|
67
|
+
const allInputs = document.querySelectorAll('input, textarea, select');
|
|
68
|
+
allInputs.forEach((el, index) => {
|
|
69
|
+
if (el.type === 'hidden' || el.offsetParent === null)
|
|
70
|
+
return;
|
|
71
|
+
// Find associated label
|
|
72
|
+
let label = '';
|
|
73
|
+
if (el.id) {
|
|
74
|
+
const labelEl = document.querySelector(`label[for="${el.id}"]`);
|
|
75
|
+
if (labelEl)
|
|
76
|
+
label = labelEl.textContent.trim();
|
|
77
|
+
}
|
|
78
|
+
if (!label) {
|
|
79
|
+
const parent = el.closest('label, .form-group, .field');
|
|
80
|
+
if (parent)
|
|
81
|
+
label = parent.textContent?.split('\n')[0]?.trim() || '';
|
|
82
|
+
}
|
|
83
|
+
inputs.push({
|
|
84
|
+
index,
|
|
85
|
+
tag: el.tagName.toLowerCase(),
|
|
86
|
+
type: el.type || 'text',
|
|
87
|
+
name: el.name || '',
|
|
88
|
+
id: el.id || '',
|
|
89
|
+
placeholder: el.placeholder || '',
|
|
90
|
+
label: label,
|
|
91
|
+
required: el.required,
|
|
92
|
+
value: el.value || '',
|
|
93
|
+
selector: el.id ? `#${el.id}` : (el.name ? `[name="${el.name}"]` : `input[type="${el.type}"]:nth-of-type(${index + 1})`)
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// Detect captcha elements
|
|
97
|
+
const captcha = {
|
|
98
|
+
image: document.querySelector('img[src*="captcha"], img[id*="captcha"], .captcha-image')?.src || null,
|
|
99
|
+
input: document.querySelector('input[name*="captcha"], input[id*="captcha"]')?.id || null
|
|
100
|
+
};
|
|
101
|
+
// Detect submit button
|
|
102
|
+
const submitBtn = document.querySelector('button[type="submit"], input[type="submit"], button.submit');
|
|
103
|
+
return {
|
|
104
|
+
inputs,
|
|
105
|
+
captcha,
|
|
106
|
+
submitButton: submitBtn ? (submitBtn.id ? `#${submitBtn.id}` : 'button[type="submit"]') : null,
|
|
107
|
+
totalInputs: inputs.length
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async _fillFormFields(page, formData, formSelector, humanLike = true, aiMatch = true) {
|
|
112
|
+
const targetForm = formSelector || 'form';
|
|
113
|
+
const fields = Object.keys(formData || {});
|
|
114
|
+
let filledCount = 0;
|
|
115
|
+
const filledFields = [];
|
|
116
|
+
const unfilledFields = [];
|
|
117
|
+
// First, analyze the full page
|
|
118
|
+
const pageInfo = await handlers._analyzeFullPage(page);
|
|
119
|
+
notifyProgress('solve_captcha', 'progress', `🔍 Page analyzed: ${pageInfo.totalInputs} inputs found`);
|
|
120
|
+
for (const [field, value] of Object.entries(formData || {})) {
|
|
121
|
+
// Enhanced AI Field Matching - uses pageInfo for better matching
|
|
122
|
+
let bestMatch = null;
|
|
123
|
+
let bestScore = 0;
|
|
124
|
+
for (const input of pageInfo.inputs) {
|
|
125
|
+
let score = 0;
|
|
126
|
+
const fieldLower = field.toLowerCase();
|
|
127
|
+
// Exact matches
|
|
128
|
+
if (input.name.toLowerCase() === fieldLower)
|
|
129
|
+
score = 100;
|
|
130
|
+
else if (input.id.toLowerCase() === fieldLower)
|
|
131
|
+
score = 95;
|
|
132
|
+
// Partial matches
|
|
133
|
+
else if (input.name.toLowerCase().includes(fieldLower))
|
|
134
|
+
score = 80;
|
|
135
|
+
else if (input.id.toLowerCase().includes(fieldLower))
|
|
136
|
+
score = 75;
|
|
137
|
+
else if (input.placeholder.toLowerCase().includes(fieldLower))
|
|
138
|
+
score = 70;
|
|
139
|
+
else if (input.label.toLowerCase().includes(fieldLower))
|
|
140
|
+
score = 65;
|
|
141
|
+
// Type-based matching
|
|
142
|
+
else if (fieldLower.includes('email') && input.type === 'email')
|
|
143
|
+
score = 60;
|
|
144
|
+
else if (fieldLower.includes('pass') && input.type === 'password')
|
|
145
|
+
score = 60;
|
|
146
|
+
else if (fieldLower.includes('phone') && input.type === 'tel')
|
|
147
|
+
score = 60;
|
|
148
|
+
if (score > bestScore) {
|
|
149
|
+
bestScore = score;
|
|
150
|
+
bestMatch = input;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!bestMatch || bestScore < 50) {
|
|
154
|
+
unfilledFields.push(field);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const element = await page.$(bestMatch.selector);
|
|
159
|
+
if (!element) {
|
|
160
|
+
unfilledFields.push(field);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Focus element first (human-like)
|
|
164
|
+
await element.focus();
|
|
165
|
+
await new Promise(r => setTimeout(r, 100 + Math.random() * 150));
|
|
166
|
+
if (bestMatch.tag === 'select') {
|
|
167
|
+
// Smart Select
|
|
168
|
+
await page.evaluate((sel, val) => {
|
|
169
|
+
const el = document.querySelector(sel);
|
|
170
|
+
if (!el)
|
|
171
|
+
return;
|
|
172
|
+
el.value = val;
|
|
173
|
+
if (el.value !== val) {
|
|
174
|
+
for (const opt of el.options) {
|
|
175
|
+
if (opt.text.toLowerCase().includes(val.toLowerCase())) {
|
|
176
|
+
el.value = opt.value;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
182
|
+
}, bestMatch.selector, String(value));
|
|
183
|
+
}
|
|
184
|
+
else if (bestMatch.type === 'checkbox' || bestMatch.type === 'radio') {
|
|
185
|
+
if (value)
|
|
186
|
+
await element.click();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Text input - clear and type with human behavior
|
|
190
|
+
await element.click({ clickCount: 3 });
|
|
191
|
+
await page.keyboard.press('Backspace');
|
|
192
|
+
await new Promise(r => setTimeout(r, 50));
|
|
193
|
+
if (humanLike) {
|
|
194
|
+
// Human-like typing with variable delays
|
|
195
|
+
for (let i = 0; i < String(value).length; i++) {
|
|
196
|
+
const char = String(value)[i];
|
|
197
|
+
await page.keyboard.type(char);
|
|
198
|
+
// Variable delay based on character type
|
|
199
|
+
const delay = char === ' ' ? 80 : (30 + Math.random() * 70);
|
|
200
|
+
await new Promise(r => setTimeout(r, delay));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
await page.type(bestMatch.selector, String(value));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Tab to next field (human-like navigation)
|
|
208
|
+
if (humanLike) {
|
|
209
|
+
await new Promise(r => setTimeout(r, 100 + Math.random() * 200));
|
|
210
|
+
await page.keyboard.press('Tab');
|
|
211
|
+
await new Promise(r => setTimeout(r, 50));
|
|
212
|
+
}
|
|
213
|
+
filledCount++;
|
|
214
|
+
filledFields.push({ field, selector: bestMatch.selector, matchScore: bestScore });
|
|
215
|
+
notifyProgress('solve_captcha', 'progress', `📝 Filled: ${field} (score: ${bestScore})`, { field, filledCount });
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
unfilledFields.push(field);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
success: filledCount > 0,
|
|
223
|
+
filledCount,
|
|
224
|
+
filledFields,
|
|
225
|
+
unfilledFields,
|
|
226
|
+
totalFields: fields.length,
|
|
227
|
+
pageInfo
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
async _validateBeforeSubmit(page) {
|
|
231
|
+
return await page.evaluate(() => {
|
|
232
|
+
const errors = [];
|
|
233
|
+
const requiredFields = document.querySelectorAll('[required], .required input');
|
|
234
|
+
requiredFields.forEach(field => {
|
|
235
|
+
if (!field.value || field.value.trim() === '') {
|
|
236
|
+
const label = field.name || field.id || field.placeholder || 'Unknown';
|
|
237
|
+
errors.push({ field: label, error: 'Required field is empty' });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// Check for visible error messages
|
|
241
|
+
const errorMsgs = document.querySelectorAll('.error, .error-message, .invalid-feedback, [class*="error"]');
|
|
242
|
+
errorMsgs.forEach(el => {
|
|
243
|
+
if (el.offsetParent !== null && el.textContent.trim()) {
|
|
244
|
+
errors.push({ field: 'form', error: el.textContent.trim() });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return { valid: errors.length === 0, errors };
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
async _detectPostSubmitErrors(page) {
|
|
251
|
+
await new Promise(r => setTimeout(r, 1500)); // Wait for page response
|
|
252
|
+
return await page.evaluate(() => {
|
|
253
|
+
const errors = [];
|
|
254
|
+
// Check for error messages
|
|
255
|
+
const errorSelectors = [
|
|
256
|
+
'.error', '.error-message', '.alert-danger', '.invalid',
|
|
257
|
+
'[class*="error"]', '[class*="invalid"]', '.captcha-error'
|
|
258
|
+
];
|
|
259
|
+
for (const sel of errorSelectors) {
|
|
260
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
261
|
+
if (el.offsetParent !== null && el.textContent.trim()) {
|
|
262
|
+
errors.push(el.textContent.trim());
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Check if captcha input is still visible (might indicate wrong captcha)
|
|
267
|
+
const captchaInput = document.querySelector('input[name*="captcha"], input[id*="captcha"]');
|
|
268
|
+
if (captchaInput && captchaInput.offsetParent !== null && !captchaInput.value) {
|
|
269
|
+
errors.push('Captcha may have failed - input is empty');
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
hasErrors: errors.length > 0,
|
|
273
|
+
errors: [...new Set(errors)].slice(0, 5) // Unique errors, max 5
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
async _solveWithVisionAPI(imageBase64, langHint = '') {
|
|
278
|
+
if (process.env.NVIDIA_API_KEY) {
|
|
279
|
+
try {
|
|
280
|
+
return await handlers._solveWithNvidia(imageBase64, langHint);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
notifyProgress('solve_captcha', 'progress', `⚠️ NVIDIA API error: ${e.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (process.env.OPENROUTER_API_KEY) {
|
|
287
|
+
try {
|
|
288
|
+
return await handlers._solveWithOpenRouter(imageBase64, langHint);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
notifyProgress('solve_captcha', 'progress', `⚠️ OpenRouter API error: ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null; // No API configured — fallback to host LLM
|
|
295
|
+
},
|
|
296
|
+
async _solveWithNvidia(imageBase64, langHint = '') {
|
|
297
|
+
const https = require('https');
|
|
298
|
+
const apiKey = process.env.NVIDIA_API_KEY;
|
|
299
|
+
// NVIDIA vision models sorted by speed & reliability
|
|
300
|
+
const models = [
|
|
301
|
+
'z-ai/glm-4.7',
|
|
302
|
+
'deepseek-ai/deepseek-v4-pro',
|
|
303
|
+
'meta/llama-3.2-11b-vision-instruct',
|
|
304
|
+
'meta/llama-4-maverick-17b-128e-instruct',
|
|
305
|
+
'microsoft/phi-4-multimodal-instruct'
|
|
306
|
+
];
|
|
307
|
+
for (const model of models) {
|
|
308
|
+
notifyProgress('solve_captcha', 'progress', `🟢 NVIDIA: Trying ${model}...`);
|
|
309
|
+
const requestBody = JSON.stringify({
|
|
310
|
+
model,
|
|
311
|
+
messages: [{
|
|
312
|
+
role: 'user',
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text: 'This is a CAPTCHA image. Read ONLY the text/characters shown. Return ONLY the exact characters, nothing else.' + langHint
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: 'image_url',
|
|
320
|
+
image_url: { url: `data:image/png;base64,${imageBase64}` }
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
}],
|
|
324
|
+
max_tokens: 30,
|
|
325
|
+
temperature: 0.1
|
|
326
|
+
});
|
|
327
|
+
try {
|
|
328
|
+
const result = await new Promise((resolve, reject) => {
|
|
329
|
+
const options = {
|
|
330
|
+
hostname: 'integrate.api.nvidia.com',
|
|
331
|
+
path: '/v1/chat/completions',
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: {
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
336
|
+
'Content-Length': Buffer.byteLength(requestBody)
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const req = https.request(options, (res) => {
|
|
340
|
+
let data = '';
|
|
341
|
+
res.on('data', chunk => data += chunk);
|
|
342
|
+
res.on('end', () => {
|
|
343
|
+
try {
|
|
344
|
+
const json = JSON.parse(data);
|
|
345
|
+
if (json.error) {
|
|
346
|
+
notifyProgress('solve_captcha', 'progress', `⏭️ NVIDIA ${model}: ${(json.error.message || '').substring(0, 60)}`);
|
|
347
|
+
return resolve(null); // Try next model
|
|
348
|
+
}
|
|
349
|
+
const text = json?.choices?.[0]?.message?.content?.trim();
|
|
350
|
+
if (!text)
|
|
351
|
+
return resolve(null);
|
|
352
|
+
const cleaned = text.replace(/[\s"'\n\r`]/g, '');
|
|
353
|
+
notifyProgress('solve_captcha', 'progress', `✨ NVIDIA [${model.split('/')[1]}] extracted: "${cleaned}"`);
|
|
354
|
+
resolve(cleaned || null);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
resolve(null);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
req.on('error', () => resolve(null));
|
|
362
|
+
req.setTimeout(20000, () => { req.destroy(); resolve(null); });
|
|
363
|
+
req.write(requestBody);
|
|
364
|
+
req.end();
|
|
365
|
+
});
|
|
366
|
+
if (result)
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
// Try next model
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw new Error('All NVIDIA models failed');
|
|
374
|
+
},
|
|
375
|
+
async _solveWithOpenRouter(imageBase64, langHint = '') {
|
|
376
|
+
const https = require('https');
|
|
377
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
378
|
+
// Best free vision models on OpenRouter (auto-fallback)
|
|
379
|
+
const models = [
|
|
380
|
+
'google/gemini-2.5-pro-free', // Insanely smart & free
|
|
381
|
+
'meta-llama/llama-3.2-90b-vision-instruct:free',
|
|
382
|
+
'qwen/qwen-vl-plus:free'
|
|
383
|
+
];
|
|
384
|
+
for (const model of models) {
|
|
385
|
+
notifyProgress('solve_captcha', 'progress', `🟢 OpenRouter: Trying ${model}...`);
|
|
386
|
+
const requestBody = JSON.stringify({
|
|
387
|
+
model,
|
|
388
|
+
messages: [{
|
|
389
|
+
role: 'user',
|
|
390
|
+
content: [
|
|
391
|
+
{ type: 'text', text: 'This is a CAPTCHA image. Read ONLY the text/characters shown. Return ONLY the exact characters, nothing else.' + langHint },
|
|
392
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } }
|
|
393
|
+
]
|
|
394
|
+
}],
|
|
395
|
+
max_tokens: 30,
|
|
396
|
+
temperature: 0.1
|
|
397
|
+
});
|
|
398
|
+
try {
|
|
399
|
+
const result = await new Promise((resolve, reject) => {
|
|
400
|
+
const options = {
|
|
401
|
+
hostname: 'openrouter.ai',
|
|
402
|
+
path: '/api/v1/chat/completions',
|
|
403
|
+
method: 'POST',
|
|
404
|
+
headers: {
|
|
405
|
+
'Content-Type': 'application/json',
|
|
406
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
407
|
+
'HTTP-Referer': 'https://github.com/brave-browser',
|
|
408
|
+
'X-Title': 'Brave MCP',
|
|
409
|
+
'Content-Length': Buffer.byteLength(requestBody)
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const req = https.request(options, (res) => {
|
|
413
|
+
let data = '';
|
|
414
|
+
res.on('data', chunk => data += chunk);
|
|
415
|
+
res.on('end', () => {
|
|
416
|
+
try {
|
|
417
|
+
const json = JSON.parse(data);
|
|
418
|
+
if (json.error) {
|
|
419
|
+
notifyProgress('solve_captcha', 'progress', `⏭️ OpenRouter ${model}: ${(json.error.message || '').substring(0, 60)}`);
|
|
420
|
+
return resolve(null); // Try next model
|
|
421
|
+
}
|
|
422
|
+
const text = json?.choices?.[0]?.message?.content?.trim();
|
|
423
|
+
if (!text)
|
|
424
|
+
return resolve(null);
|
|
425
|
+
const cleaned = text.replace(/[\s"'\n\r`]/g, '');
|
|
426
|
+
notifyProgress('solve_captcha', 'progress', `✨ OpenRouter [${model.split('/')[1]}] extracted: "${cleaned}"`);
|
|
427
|
+
resolve(cleaned || null);
|
|
428
|
+
}
|
|
429
|
+
catch (e) {
|
|
430
|
+
resolve(null);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
req.on('error', () => resolve(null));
|
|
435
|
+
req.setTimeout(20000, () => { req.destroy(); resolve(null); });
|
|
436
|
+
req.write(requestBody);
|
|
437
|
+
req.end();
|
|
438
|
+
});
|
|
439
|
+
if (result)
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
// Try next model
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
throw new Error('All OpenRouter models failed');
|
|
447
|
+
},
|
|
448
|
+
async _submitForm(page, validateFirst = true, maxRetries = 1) {
|
|
449
|
+
try {
|
|
450
|
+
// Pre-submit validation
|
|
451
|
+
if (validateFirst) {
|
|
452
|
+
const validation = await handlers._validateBeforeSubmit(page);
|
|
453
|
+
if (!validation.valid) {
|
|
454
|
+
notifyProgress('solve_captcha', 'warn', `⚠️ Validation failed: ${validation.errors.length} issue(s)`);
|
|
455
|
+
return { success: false, message: 'Pre-submit validation failed', errors: validation.errors };
|
|
456
|
+
}
|
|
457
|
+
notifyProgress('solve_captcha', 'progress', '✅ Pre-submit validation passed');
|
|
458
|
+
}
|
|
459
|
+
const submitSelector = await page.evaluate(() => {
|
|
460
|
+
const buttons = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], a.btn'));
|
|
461
|
+
const candidates = buttons.filter(b => {
|
|
462
|
+
const text = (b.innerText || b.value || '').toLowerCase();
|
|
463
|
+
return text.includes('submit') || text.includes('go') || text.includes('search') ||
|
|
464
|
+
text.includes('view') || text.includes('login') || text.includes('sign in') ||
|
|
465
|
+
text.includes('register') || text.includes('send');
|
|
466
|
+
});
|
|
467
|
+
const best = candidates.find(b => b.offsetParent !== null);
|
|
468
|
+
if (best) {
|
|
469
|
+
return best.id ? `#${best.id}` : (best.name ? `[name="${best.name}"]` : 'button[type="submit"]');
|
|
470
|
+
}
|
|
471
|
+
// Fallback to any submit button
|
|
472
|
+
const fallback = document.querySelector('button[type="submit"], input[type="submit"]');
|
|
473
|
+
return fallback ? (fallback.id ? `#${fallback.id}` : 'button[type="submit"]') : null;
|
|
474
|
+
});
|
|
475
|
+
if (!submitSelector) {
|
|
476
|
+
notifyProgress('solve_captcha', 'warn', '⚠️ Could not auto-detect submit button');
|
|
477
|
+
return { success: false, message: 'Could not auto-detect submit button' };
|
|
478
|
+
}
|
|
479
|
+
// Click submit button with human-like behavior
|
|
480
|
+
try {
|
|
481
|
+
const { createCursor } = require('ghost-cursor-patchright');
|
|
482
|
+
const cursor = createCursor(page);
|
|
483
|
+
await cursor.click(submitSelector);
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
await page.click(submitSelector);
|
|
487
|
+
}
|
|
488
|
+
// Wait for response
|
|
489
|
+
try {
|
|
490
|
+
await page.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' });
|
|
491
|
+
notifyProgress('solve_captcha', 'completed', '✅ Form submitted and navigation complete');
|
|
492
|
+
return { success: true, message: 'Form submitted and navigation complete', navigated: true };
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
// No navigation - check for errors on same page
|
|
496
|
+
const postErrors = await handlers._detectPostSubmitErrors(page);
|
|
497
|
+
if (postErrors.hasErrors) {
|
|
498
|
+
notifyProgress('solve_captcha', 'warn', `⚠️ Submit detected errors: ${postErrors.errors[0]}`);
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
message: 'Form submitted but errors detected',
|
|
502
|
+
errors: postErrors.errors,
|
|
503
|
+
needsRetry: postErrors.errors.some(e => e.toLowerCase().includes('captcha'))
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
notifyProgress('solve_captcha', 'completed', '✅ Form submitted (no navigation detected)');
|
|
507
|
+
return { success: true, message: 'Form submitted (no navigation detected)', navigated: false };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
return { success: false, message: error.message };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aiEnhancedSelector = exports.getAICore = exports.getHeadlessFromEnv = exports.notifyProgress = exports.setProgressCallback = exports.requireBrowser = exports.getState = exports.handlers = void 0;
|
|
4
|
+
exports.executeTool = executeTool;
|
|
5
|
+
exports.cleanup = cleanup;
|
|
6
|
+
// @ts-nocheck
|
|
7
|
+
const browser_1 = require("./browser");
|
|
8
|
+
const dom_1 = require("./dom");
|
|
9
|
+
const network_1 = require("./network");
|
|
10
|
+
const vision_1 = require("./vision");
|
|
11
|
+
const extract_1 = require("./extract");
|
|
12
|
+
const helpers_1 = require("./helpers");
|
|
13
|
+
const misc_1 = require("./misc");
|
|
14
|
+
const state_1 = require("./state");
|
|
15
|
+
Object.defineProperty(exports, "setProgressCallback", { enumerable: true, get: function () { return state_1.setProgressCallback; } });
|
|
16
|
+
Object.defineProperty(exports, "notifyProgress", { enumerable: true, get: function () { return state_1.notifyProgress; } });
|
|
17
|
+
Object.defineProperty(exports, "getHeadlessFromEnv", { enumerable: true, get: function () { return state_1.getHeadlessFromEnv; } });
|
|
18
|
+
Object.defineProperty(exports, "getState", { enumerable: true, get: function () { return state_1.getState; } });
|
|
19
|
+
Object.defineProperty(exports, "requireBrowser", { enumerable: true, get: function () { return state_1.requireBrowser; } });
|
|
20
|
+
const core_1 = require("../../ai/core");
|
|
21
|
+
Object.defineProperty(exports, "getAICore", { enumerable: true, get: function () { return core_1.getAICore; } });
|
|
22
|
+
Object.defineProperty(exports, "aiEnhancedSelector", { enumerable: true, get: function () { return core_1.aiEnhancedSelector; } });
|
|
23
|
+
exports.handlers = {
|
|
24
|
+
...browser_1.browserHandlers,
|
|
25
|
+
...dom_1.domHandlers,
|
|
26
|
+
...network_1.networkHandlers,
|
|
27
|
+
...vision_1.visionHandlers,
|
|
28
|
+
...extract_1.extractHandlers,
|
|
29
|
+
...helpers_1.helpersHandlers,
|
|
30
|
+
...misc_1.miscHandlers
|
|
31
|
+
};
|
|
32
|
+
async function executeTool(name, args = {}) {
|
|
33
|
+
if (exports.handlers[name]) {
|
|
34
|
+
try {
|
|
35
|
+
const aiCore = core_1.getAICore ? (0, core_1.getAICore)() : null;
|
|
36
|
+
let wrappedHandler = exports.handlers[name];
|
|
37
|
+
if (aiCore && typeof aiCore.wrapHandler === 'function') {
|
|
38
|
+
wrappedHandler = aiCore.wrapHandler(exports.handlers[name].bind(exports.handlers), name);
|
|
39
|
+
}
|
|
40
|
+
return await wrappedHandler(args);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { success: false, error: error.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { success: false, error: `Tool ${name} not implemented` };
|
|
47
|
+
}
|
|
48
|
+
async function cleanup() {
|
|
49
|
+
if (state_1.state.browserInstance) {
|
|
50
|
+
try {
|
|
51
|
+
await state_1.state.browserInstance.close();
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (typeof state_1.state.browserInstance.process === 'function') {
|
|
55
|
+
state_1.state.browserInstance.process()?.kill('SIGKILL');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
state_1.state.browserInstance = null;
|
|
59
|
+
state_1.state.pageInstance = null;
|
|
60
|
+
state_1.state.blockerInstance = null;
|
|
61
|
+
state_1.state.setupPageFn = null;
|
|
62
|
+
}
|
|
63
|
+
}
|