aurix-ai 2.5.8 → 2.6.0
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/agent/AgentLoop.d.ts.map +1 -1
- package/dist/agent/AgentLoop.js +5 -0
- package/dist/agent/AgentLoop.js.map +1 -1
- package/dist/agent/Config.d.ts +3 -0
- package/dist/agent/Config.d.ts.map +1 -1
- package/dist/agent/Config.js +3 -0
- package/dist/agent/Config.js.map +1 -1
- package/dist/tools/Browser.d.ts.map +1 -1
- package/dist/tools/Browser.js +728 -640
- package/dist/tools/Browser.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/Browser.js
CHANGED
|
@@ -40,9 +40,11 @@ function readFileBase64(path) {
|
|
|
40
40
|
}
|
|
41
41
|
async function visionClassify(imageBase64, prompt) {
|
|
42
42
|
const config = loadConfig();
|
|
43
|
-
const
|
|
43
|
+
const visionModel = config.visionModel || config.model || 'gpt-4o';
|
|
44
|
+
const visionBaseUrl = config.visionBaseUrl || config.baseUrl;
|
|
45
|
+
const visionApiKey = config.visionApiKey || config.apiKey;
|
|
44
46
|
const body = {
|
|
45
|
-
model,
|
|
47
|
+
model: visionModel,
|
|
46
48
|
messages: [{
|
|
47
49
|
role: 'user',
|
|
48
50
|
content: [
|
|
@@ -52,38 +54,46 @@ async function visionClassify(imageBase64, prompt) {
|
|
|
52
54
|
}],
|
|
53
55
|
max_tokens: 100,
|
|
54
56
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const fetchTimeout = setTimeout(() => controller.abort(), 15_000);
|
|
59
|
+
try {
|
|
60
|
+
const resp = await fetch(`${visionBaseUrl}/chat/completions`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
...(visionApiKey ? { Authorization: `Bearer ${visionApiKey}` } : {}),
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
if (!resp.ok)
|
|
70
|
+
throw new Error(`Vision API error: ${resp.status}`);
|
|
71
|
+
const text = await resp.text();
|
|
72
|
+
if (text.includes('data: ')) {
|
|
73
|
+
let content = '';
|
|
74
|
+
for (const line of text.split('\n')) {
|
|
75
|
+
if (line.startsWith('data: ') && line.trim() !== 'data: [DONE]') {
|
|
76
|
+
try {
|
|
77
|
+
const ev = JSON.parse(line.slice(6));
|
|
78
|
+
const delta = ev.choices?.[0]?.delta;
|
|
79
|
+
if (delta?.content)
|
|
80
|
+
content += delta.content;
|
|
81
|
+
if (delta?.text)
|
|
82
|
+
content += delta.text;
|
|
83
|
+
if (ev.choices?.[0]?.message?.content)
|
|
84
|
+
content += ev.choices[0].message.content;
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
79
87
|
}
|
|
80
|
-
catch { }
|
|
81
88
|
}
|
|
89
|
+
return content.trim();
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
const json = JSON.parse(text);
|
|
92
|
+
return (json.choices?.[0]?.message?.content || '').trim();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(fetchTimeout);
|
|
84
96
|
}
|
|
85
|
-
const json = JSON.parse(text);
|
|
86
|
-
return (json.choices?.[0]?.message?.content || '').trim();
|
|
87
97
|
}
|
|
88
98
|
async function solveCaptchaGrid(page, frame, provider) {
|
|
89
99
|
const results = [];
|
|
@@ -189,56 +199,7 @@ async function solveCaptchaGrid(page, frame, provider) {
|
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
if (isRecaptcha && matchedIndices.length > 0) {
|
|
192
|
-
await page.waitForTimeout(
|
|
193
|
-
const afterTiles = await findGridTiles(frame, provider);
|
|
194
|
-
const evalPromises = matchedIndices
|
|
195
|
-
.filter(idx => idx < afterTiles.length)
|
|
196
|
-
.map(async (idx) => {
|
|
197
|
-
try {
|
|
198
|
-
const tilePath = join(homedir(), `.aurix-tile-after-${idx}.png`);
|
|
199
|
-
await afterTiles[idx].screenshot({ path: tilePath });
|
|
200
|
-
const base64 = readFileBase64(tilePath);
|
|
201
|
-
const resp = await visionClassify(base64, `Does this image contain ${instruction}? Reply YES or NO only.`);
|
|
202
|
-
return { idx, match: resp.toLowerCase().includes('yes') };
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return { idx, match: false };
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
const evalResults = await Promise.all(evalPromises);
|
|
209
|
-
const newMatches = evalResults.filter(r => r.match);
|
|
210
|
-
if (newMatches.length > 0) {
|
|
211
|
-
results.push(` Replacement tiles matched: [${newMatches.map(r => r.idx).join(', ')}]`);
|
|
212
|
-
for (const { idx } of newMatches) {
|
|
213
|
-
try {
|
|
214
|
-
const freshTiles = await findGridTiles(frame, provider);
|
|
215
|
-
if (idx >= freshTiles.length)
|
|
216
|
-
continue;
|
|
217
|
-
const tile = freshTiles[idx];
|
|
218
|
-
const tileBox = await tile.boundingBox();
|
|
219
|
-
if (tileBox) {
|
|
220
|
-
const cx = tileBox.x + tileBox.width * (0.3 + Math.random() * 0.4);
|
|
221
|
-
const cy = tileBox.y + tileBox.height * (0.3 + Math.random() * 0.4);
|
|
222
|
-
await humanMove(cx, cy, page);
|
|
223
|
-
await page.waitForTimeout(80 + Math.random() * 120);
|
|
224
|
-
await page.mouse.down();
|
|
225
|
-
await page.waitForTimeout(60 + Math.random() * 100);
|
|
226
|
-
await page.mouse.up();
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
await tile.click({ force: true });
|
|
230
|
-
}
|
|
231
|
-
results.push(` Clicked replacement tile ${idx}`);
|
|
232
|
-
}
|
|
233
|
-
catch (e) {
|
|
234
|
-
results.push(` Failed replacement tile ${idx}: ${e.message}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
await page.waitForTimeout(1500 + Math.random() * 1000);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
results.push(' No replacement tiles matched');
|
|
241
|
-
}
|
|
202
|
+
await page.waitForTimeout(1500 + Math.random() * 500);
|
|
242
203
|
}
|
|
243
204
|
results.push('Clicking verify...');
|
|
244
205
|
try {
|
|
@@ -1581,562 +1542,614 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
1581
1542
|
case 'solve-captcha': {
|
|
1582
1543
|
const p = await ensureBrowser();
|
|
1583
1544
|
const results = [];
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1545
|
+
const _solveTimeout = 60_000;
|
|
1546
|
+
const _solveLogic = async () => {
|
|
1547
|
+
const frames = p.frames();
|
|
1548
|
+
let captchaType = 'unknown';
|
|
1549
|
+
let recaptchaAnchor = null;
|
|
1550
|
+
let recaptchaBframe = null;
|
|
1551
|
+
let hcaptchaCheckbox = null;
|
|
1552
|
+
let hcaptchaChallenge = null;
|
|
1553
|
+
let funcaptchaFrame = null;
|
|
1554
|
+
let mtcaptchaFrame = null;
|
|
1555
|
+
let geetestFrame = null;
|
|
1556
|
+
for (const frame of frames) {
|
|
1557
|
+
const url = frame.url();
|
|
1558
|
+
if (url.includes('/recaptcha/') && url.includes('/anchor'))
|
|
1559
|
+
recaptchaAnchor = frame;
|
|
1560
|
+
if (url.includes('/recaptcha/') && url.includes('/bframe'))
|
|
1561
|
+
recaptchaBframe = frame;
|
|
1562
|
+
if (url.includes('newassets.hcaptcha.com') && !url.includes('challenge'))
|
|
1563
|
+
hcaptchaCheckbox = frame;
|
|
1564
|
+
if (url.includes('hcaptcha') && url.includes('challenge'))
|
|
1565
|
+
hcaptchaChallenge = frame;
|
|
1566
|
+
if (url.includes('funcaptcha') || url.includes('arkoselabs'))
|
|
1567
|
+
funcaptchaFrame = frame;
|
|
1568
|
+
if (url.includes('service.mtcaptcha'))
|
|
1569
|
+
mtcaptchaFrame = frame;
|
|
1570
|
+
if ((url.includes('geetest.com') || url.includes('captcha.com')) && !url.includes('recaptcha') && !url.includes('hcaptcha'))
|
|
1571
|
+
geetestFrame = frame;
|
|
1572
|
+
}
|
|
1573
|
+
if (recaptchaAnchor)
|
|
1574
|
+
captchaType = 'recaptcha';
|
|
1575
|
+
else if (hcaptchaCheckbox)
|
|
1576
|
+
captchaType = 'hcaptcha';
|
|
1577
|
+
else if (funcaptchaFrame)
|
|
1578
|
+
captchaType = 'funcaptcha';
|
|
1579
|
+
else if (mtcaptchaFrame)
|
|
1580
|
+
captchaType = 'mtcaptcha';
|
|
1581
|
+
else if (geetestFrame)
|
|
1582
|
+
captchaType = 'geetest';
|
|
1583
|
+
const pageContent = await p.content();
|
|
1584
|
+
if (captchaType === 'unknown' && (pageContent.includes('cf-turnstile') || pageContent.includes('challenges.cloudflare'))) {
|
|
1585
|
+
captchaType = 'turnstile';
|
|
1586
|
+
}
|
|
1587
|
+
if (captchaType === 'unknown' && (pageContent.includes('mtcaptcha') || pageContent.includes('MTCaptcha'))) {
|
|
1588
|
+
captchaType = 'mtcaptcha';
|
|
1589
|
+
}
|
|
1590
|
+
if (captchaType === 'unknown' && recaptchaBframe)
|
|
1591
|
+
captchaType = 'recaptcha';
|
|
1592
|
+
if (captchaType === 'recaptcha') {
|
|
1593
|
+
results.push('Attempting reCAPTCHA...');
|
|
1594
|
+
try {
|
|
1595
|
+
let checkboxFrame = recaptchaAnchor;
|
|
1596
|
+
if (!checkboxFrame) {
|
|
1597
|
+
for (const frame of frames) {
|
|
1598
|
+
const url = frame.url();
|
|
1599
|
+
if (url.includes('recaptcha') && (url.includes('anchor') || url.includes('api2/'))) {
|
|
1600
|
+
checkboxFrame = frame;
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1639
1603
|
}
|
|
1640
1604
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1605
|
+
if (checkboxFrame) {
|
|
1606
|
+
const checkbox = checkboxFrame.locator('#recaptcha-anchor, .recaptcha-checkbox, .rc-anchor-checkbox');
|
|
1607
|
+
if (await checkbox.count() > 0) {
|
|
1608
|
+
await p.waitForTimeout(1000 + Math.random() * 1500);
|
|
1609
|
+
await humanClick(checkbox, p);
|
|
1610
|
+
await p.waitForTimeout(3000);
|
|
1611
|
+
const updatedFrames = p.frames();
|
|
1612
|
+
const challengeFrame = updatedFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
|
|
1613
|
+
if (challengeFrame) {
|
|
1614
|
+
results.push('Image challenge appeared. Auto-solving...');
|
|
1615
|
+
const maxRetries = 3;
|
|
1616
|
+
let solved = false;
|
|
1617
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1618
|
+
if (attempt > 0)
|
|
1619
|
+
results.push(`\nRetry attempt ${attempt}/${maxRetries - 1}...`);
|
|
1620
|
+
const solveResult = await solveCaptchaGrid(p, challengeFrame, 'recaptcha');
|
|
1621
|
+
results.push(solveResult);
|
|
1622
|
+
if (solveResult.includes('Captcha solved!')) {
|
|
1623
|
+
solved = true;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
if (solveResult.includes('Falling back to manual mode')) {
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
await p.waitForTimeout(2000);
|
|
1630
|
+
const refreshedFrames = p.frames();
|
|
1631
|
+
const newChallenge = refreshedFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
|
|
1632
|
+
if (!newChallenge) {
|
|
1633
|
+
results.push('Challenge frame disappeared, captcha may be solved');
|
|
1634
|
+
solved = true;
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1665
1637
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const newChallenge = refreshedFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
|
|
1669
|
-
if (!newChallenge) {
|
|
1670
|
-
results.push('Challenge frame disappeared, captcha may be solved');
|
|
1671
|
-
solved = true;
|
|
1672
|
-
break;
|
|
1638
|
+
if (!solved && !results.some(r => r.includes('Falling back'))) {
|
|
1639
|
+
results.push(`\nAuto-solve exhausted after ${maxRetries} attempts. Use "captcha-grid" and "click-tile" for manual solving.`);
|
|
1673
1640
|
}
|
|
1674
1641
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1642
|
+
else {
|
|
1643
|
+
const checkmark = checkboxFrame.locator('.recaptcha-checkbox-checked, .rc-anchor-checkbox-checked');
|
|
1644
|
+
if (await checkmark.count() > 0) {
|
|
1645
|
+
results.push(ok('reCAPTCHA solved — no image challenge needed', { status: 'verified' }));
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
results.push(warn('Checkbox clicked but verification unclear', { suggestion: 'Use "captcha-grid" to check for image challenge' }));
|
|
1649
|
+
}
|
|
1677
1650
|
}
|
|
1678
1651
|
}
|
|
1679
1652
|
else {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1653
|
+
results.push(warn('reCAPTCHA anchor frame found but checkbox element missing', { action: 'clicking anchor body to trigger challenge' }));
|
|
1654
|
+
const anchor = checkboxFrame.locator('#recaptcha-anchor, .recaptcha-checkbox-area, [role="presentation"]').first();
|
|
1655
|
+
if (await anchor.count() === 0) {
|
|
1656
|
+
const body = checkboxFrame.locator('body');
|
|
1657
|
+
await humanClick(body, p).catch(() => { });
|
|
1683
1658
|
}
|
|
1684
1659
|
else {
|
|
1685
|
-
|
|
1660
|
+
await humanClick(anchor, p).catch(() => { });
|
|
1661
|
+
}
|
|
1662
|
+
await p.waitForTimeout(3000);
|
|
1663
|
+
const retryFrames = p.frames();
|
|
1664
|
+
const challengeFrame = retryFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
|
|
1665
|
+
if (challengeFrame) {
|
|
1666
|
+
results.push('Image challenge appeared after clicking anchor. Auto-solving...');
|
|
1667
|
+
const maxRetries = 3;
|
|
1668
|
+
let solved = false;
|
|
1669
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1670
|
+
if (attempt > 0)
|
|
1671
|
+
results.push(`\nRetry attempt ${attempt}/${maxRetries - 1}...`);
|
|
1672
|
+
const solveResult = await solveCaptchaGrid(p, challengeFrame, 'recaptcha');
|
|
1673
|
+
results.push(solveResult);
|
|
1674
|
+
if (solveResult.includes('Captcha solved!')) {
|
|
1675
|
+
solved = true;
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
if (solveResult.includes('Falling back to manual mode'))
|
|
1679
|
+
break;
|
|
1680
|
+
await p.waitForTimeout(2000);
|
|
1681
|
+
const refreshedFrames = p.frames();
|
|
1682
|
+
const newChallenge = refreshedFrames.find(f => f.url().includes('/recaptcha/') && f.url().includes('/bframe'));
|
|
1683
|
+
if (!newChallenge) {
|
|
1684
|
+
results.push('Challenge frame disappeared, captcha may be solved');
|
|
1685
|
+
solved = true;
|
|
1686
|
+
break;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (!solved && !results.some(r => r.includes('Falling back'))) {
|
|
1690
|
+
results.push(`\nAuto-solve exhausted after ${maxRetries} attempts. Use "captcha-grid" and "click-tile" for manual solving.`);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
results.push('No challenge appeared after clicking anchor. Use "captcha-grid" to check state.');
|
|
1686
1695
|
}
|
|
1687
1696
|
}
|
|
1688
1697
|
}
|
|
1689
1698
|
else {
|
|
1690
|
-
results.push(warn('reCAPTCHA anchor frame found
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1699
|
+
results.push(warn('No reCAPTCHA anchor frame found', { action: 'trying main page widget' }));
|
|
1700
|
+
const mainCheckbox = p.locator('.g-recaptcha, [data-sitekey]');
|
|
1701
|
+
if (await mainCheckbox.count() > 0) {
|
|
1702
|
+
await humanClick(mainCheckbox, p);
|
|
1703
|
+
await p.waitForTimeout(3000);
|
|
1704
|
+
results.push('Clicked reCAPTCHA widget. Use "captcha-grid" if image challenge appeared.');
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
results.push(err('reCAPTCHA widget not found on page', 'Check if the page has loaded or use "detect-captcha" first'));
|
|
1708
|
+
}
|
|
1695
1709
|
}
|
|
1696
1710
|
}
|
|
1697
|
-
|
|
1698
|
-
results.push(
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1711
|
+
catch (e) {
|
|
1712
|
+
results.push(err(`reCAPTCHA click failed: ${e.message}`, 'Use "detect-captcha" first, then retry "solve-captcha"'));
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (captchaType === 'hcaptcha') {
|
|
1716
|
+
results.push('Attempting hCaptcha...');
|
|
1717
|
+
try {
|
|
1718
|
+
const checkboxFrame = hcaptchaCheckbox;
|
|
1719
|
+
if (checkboxFrame) {
|
|
1720
|
+
const checkbox = checkboxFrame.locator('#checkbox, .check');
|
|
1721
|
+
if (await checkbox.count() > 0) {
|
|
1722
|
+
await p.waitForTimeout(800 + Math.random() * 1200);
|
|
1723
|
+
await humanClick(checkbox, p);
|
|
1724
|
+
await p.waitForTimeout(3000);
|
|
1725
|
+
const updatedFrames = p.frames();
|
|
1726
|
+
const challengeFrame = updatedFrames.find((f) => f.url().includes('hcaptcha') && f.url().includes('challenge'));
|
|
1727
|
+
if (challengeFrame) {
|
|
1728
|
+
results.push('Image challenge appeared. Auto-solving...');
|
|
1729
|
+
const maxRetries = 3;
|
|
1730
|
+
let solved = false;
|
|
1731
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1732
|
+
if (attempt > 0)
|
|
1733
|
+
results.push(`\nRetry attempt ${attempt}/${maxRetries - 1}...`);
|
|
1734
|
+
const solveResult = await solveCaptchaGrid(p, challengeFrame, 'hcaptcha');
|
|
1735
|
+
results.push(solveResult);
|
|
1736
|
+
if (solveResult.includes('Captcha solved!')) {
|
|
1737
|
+
solved = true;
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
if (solveResult.includes('Falling back to manual mode')) {
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
await p.waitForTimeout(2000);
|
|
1744
|
+
const refreshedFrames = p.frames();
|
|
1745
|
+
const newChallenge = refreshedFrames.find((f) => f.url().includes('hcaptcha') && f.url().includes('challenge'));
|
|
1746
|
+
if (!newChallenge) {
|
|
1747
|
+
results.push('Challenge frame disappeared, captcha may be solved');
|
|
1748
|
+
solved = true;
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (!solved && !results.some(r => r.includes('Falling back'))) {
|
|
1753
|
+
results.push(`\nAuto-solve exhausted after ${maxRetries} attempts. Use "captcha-grid" and "click-tile" for manual solving.`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
const checkmark = checkboxFrame.locator('.check.solved, #checkbox[aria-checked="true"]');
|
|
1758
|
+
if (await checkmark.count() > 0) {
|
|
1759
|
+
results.push(ok('hCaptcha solved', { status: 'verified' }));
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
results.push(warn('hCaptcha checkbox clicked, status unclear', { suggestion: 'Use "captcha-grid" to check for image challenge' }));
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
results.push(err('hCaptcha checkbox element not found in frame'));
|
|
1768
|
+
}
|
|
1704
1769
|
}
|
|
1705
1770
|
else {
|
|
1706
|
-
results.push(err('
|
|
1771
|
+
results.push(err('hCaptcha checkbox frame not found', 'Use "detect-captcha" to scan for captcha type'));
|
|
1707
1772
|
}
|
|
1708
1773
|
}
|
|
1774
|
+
catch (e) {
|
|
1775
|
+
results.push(err(`hCaptcha click failed: ${e.message}`));
|
|
1776
|
+
}
|
|
1709
1777
|
}
|
|
1710
|
-
|
|
1711
|
-
results.push(
|
|
1778
|
+
if (captchaType === 'turnstile') {
|
|
1779
|
+
results.push('Attempting Cloudflare Turnstile...');
|
|
1780
|
+
try {
|
|
1781
|
+
const turnstileFrame = frames.find(f => f.url().includes('challenges.cloudflare'));
|
|
1782
|
+
if (turnstileFrame) {
|
|
1783
|
+
await p.waitForTimeout(1500 + Math.random() * 1000);
|
|
1784
|
+
const cb = turnstileFrame.locator('input[type="checkbox"], .cb-lb');
|
|
1785
|
+
if (await cb.count() > 0) {
|
|
1786
|
+
await humanClick(cb, p);
|
|
1787
|
+
await p.waitForTimeout(3000);
|
|
1788
|
+
results.push(ok('Turnstile checkbox clicked'));
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
await turnstileFrame.locator('body').click();
|
|
1792
|
+
await p.waitForTimeout(3000);
|
|
1793
|
+
results.push(warn('Turnstile frame clicked (managed challenge)', { next: 'Check if challenge resolved with screenshot' }));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
catch (e) {
|
|
1798
|
+
results.push(err(`Turnstile failed: ${e.message}`));
|
|
1799
|
+
}
|
|
1712
1800
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1801
|
+
if (captchaType === 'funcaptcha') {
|
|
1802
|
+
results.push('FunCaptcha (Arkose Labs) detected. Auto-solving...');
|
|
1803
|
+
try {
|
|
1804
|
+
const fcFrame = funcaptchaFrame;
|
|
1805
|
+
if (fcFrame) {
|
|
1806
|
+
await p.waitForTimeout(2000);
|
|
1807
|
+
const instruction = await fcFrame.evaluate(() => {
|
|
1808
|
+
const h2 = document.querySelector('h2, h3, .challenge-title, #challenge-stage .title, [class*="instruction"], [class*="prompt"]');
|
|
1809
|
+
return h2?.textContent?.trim() || '';
|
|
1810
|
+
}).catch(() => '');
|
|
1811
|
+
if (instruction)
|
|
1812
|
+
results.push(`Instruction: "${instruction}"`);
|
|
1813
|
+
const maxAttempts = 3;
|
|
1814
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1815
|
+
if (attempt > 0)
|
|
1816
|
+
results.push(`\nRetry ${attempt}/${maxAttempts - 1}...`);
|
|
1817
|
+
const screenshotPath = join(homedir(), '.aurix-funcaptcha-puzzle.png');
|
|
1818
|
+
try {
|
|
1819
|
+
await fcFrame.locator('#challenge-stage, .challenge-content, .game-content, body').first().screenshot({ path: screenshotPath });
|
|
1820
|
+
}
|
|
1821
|
+
catch {
|
|
1822
|
+
await p.screenshot({ path: screenshotPath });
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
const ssBase64 = readFileBase64(screenshotPath);
|
|
1826
|
+
const prompt = instruction
|
|
1827
|
+
? `This is a FunCaptcha puzzle. The instruction is: "${instruction}". Analyze the image and tell me EXACTLY what to do. Reply in this format:\n- For clicking: "CLICK x,y" (pixel coordinates relative to the puzzle image)\n- For dragging: "DRAG fromX,fromY toX,toY"\n- For rotating: "ROTATE degrees" (estimated rotation angle in degrees)\n- For selecting an option: "CLICK x,y" on the correct answer\nBe precise with coordinates.`
|
|
1828
|
+
: `This is a FunCaptcha puzzle. Analyze the image and determine what action is needed to solve it. Reply in this format:\n- For clicking: "CLICK x,y"\n- For dragging: "DRAG fromX,fromY toX,toY"\n- For rotating: "ROTATE degrees"\nBe precise with coordinates.`;
|
|
1829
|
+
const visionResp = await visionClassify(ssBase64, prompt);
|
|
1830
|
+
results.push(`Vision model: "${visionResp}"`);
|
|
1831
|
+
const clickMatch = visionResp.match(/CLICK\s+([\d.]+)\s*,\s*([\d.]+)/i);
|
|
1832
|
+
const dragMatch = visionResp.match(/DRAG\s+([\d.]+)\s*,\s*([\d.]+)\s+([\d.]+)\s*,\s*([\d.]+)/i);
|
|
1833
|
+
const rotateMatch = visionResp.match(/ROTATE\s+(-?[\d.]+)/i);
|
|
1834
|
+
const puzzleBox = await fcFrame.locator('#challenge-stage, .challenge-content, .game-content, body').first().boundingBox().catch(() => null);
|
|
1835
|
+
const offsetX = puzzleBox?.x || 0;
|
|
1836
|
+
const offsetY = puzzleBox?.y || 0;
|
|
1837
|
+
if (clickMatch) {
|
|
1838
|
+
const cx = offsetX + parseFloat(clickMatch[1]);
|
|
1839
|
+
const cy = offsetY + parseFloat(clickMatch[2]);
|
|
1840
|
+
await humanMove(cx, cy, p);
|
|
1841
|
+
await p.waitForTimeout(100 + Math.random() * 150);
|
|
1842
|
+
await p.mouse.down();
|
|
1843
|
+
await p.waitForTimeout(60 + Math.random() * 80);
|
|
1844
|
+
await p.mouse.up();
|
|
1845
|
+
results.push(`Clicked at (${Math.round(cx)}, ${Math.round(cy)})`);
|
|
1846
|
+
await p.waitForTimeout(2000);
|
|
1847
|
+
}
|
|
1848
|
+
else if (dragMatch) {
|
|
1849
|
+
const fromX = offsetX + parseFloat(dragMatch[1]);
|
|
1850
|
+
const fromY = offsetY + parseFloat(dragMatch[2]);
|
|
1851
|
+
const toX = offsetX + parseFloat(dragMatch[3]);
|
|
1852
|
+
const toY = offsetY + parseFloat(dragMatch[4]);
|
|
1853
|
+
await humanMove(fromX, fromY, p);
|
|
1854
|
+
await p.waitForTimeout(150 + Math.random() * 200);
|
|
1855
|
+
await p.mouse.down();
|
|
1856
|
+
await p.waitForTimeout(200 + Math.random() * 300);
|
|
1857
|
+
const steps = 20 + Math.floor(Math.random() * 15);
|
|
1858
|
+
for (let i = 1; i <= steps; i++) {
|
|
1859
|
+
const progress = i / steps;
|
|
1860
|
+
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
1861
|
+
await p.mouse.move(fromX + (toX - fromX) * eased, fromY + (toY - fromY) * eased + (Math.random() - 0.5) * 2);
|
|
1862
|
+
await p.waitForTimeout(10 + Math.random() * 15);
|
|
1863
|
+
}
|
|
1864
|
+
await p.mouse.move(toX, toY);
|
|
1865
|
+
await p.waitForTimeout(150);
|
|
1866
|
+
await p.mouse.up();
|
|
1867
|
+
results.push(`Dragged from (${Math.round(fromX)},${Math.round(fromY)}) to (${Math.round(toX)},${Math.round(toY)})`);
|
|
1868
|
+
await p.waitForTimeout(2000);
|
|
1869
|
+
}
|
|
1870
|
+
else if (rotateMatch) {
|
|
1871
|
+
const degrees = parseFloat(rotateMatch[1]);
|
|
1872
|
+
const rotator = fcFrame.locator('.rotator, [class*="rotate"], [class*="spinner"], canvas, .game-item').first();
|
|
1873
|
+
if (await rotator.count() > 0) {
|
|
1874
|
+
const rBox = await rotator.boundingBox();
|
|
1875
|
+
if (rBox) {
|
|
1876
|
+
const cx = rBox.x + rBox.width / 2;
|
|
1877
|
+
const cy = rBox.y + rBox.height / 2;
|
|
1878
|
+
const radius = rBox.width / 2;
|
|
1879
|
+
const startX = cx + radius;
|
|
1880
|
+
const startY = cy;
|
|
1881
|
+
const endAngle = (degrees * Math.PI) / 180;
|
|
1882
|
+
const endX = cx + radius * Math.cos(endAngle);
|
|
1883
|
+
const endY = cy + radius * Math.sin(endAngle);
|
|
1884
|
+
await humanMove(startX, startY, p);
|
|
1885
|
+
await p.waitForTimeout(150);
|
|
1886
|
+
await p.mouse.down();
|
|
1887
|
+
await p.waitForTimeout(200);
|
|
1888
|
+
const steps = 30;
|
|
1889
|
+
for (let i = 1; i <= steps; i++) {
|
|
1890
|
+
const angle = (endAngle * i) / steps;
|
|
1891
|
+
await p.mouse.move(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
|
|
1892
|
+
await p.waitForTimeout(15 + Math.random() * 10);
|
|
1893
|
+
}
|
|
1894
|
+
await p.mouse.move(endX, endY);
|
|
1895
|
+
await p.waitForTimeout(150);
|
|
1896
|
+
await p.mouse.up();
|
|
1897
|
+
results.push(`Rotated ${degrees}°`);
|
|
1898
|
+
await p.waitForTimeout(2000);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
results.push('[WARN] No rotatable element found');
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
results.push(`Could not parse vision model response: "${visionResp}"`);
|
|
1907
|
+
results.push('Falling back to manual mode. Read the puzzle screenshot and use click/drag-to/evaluate to solve.');
|
|
1737
1908
|
break;
|
|
1738
1909
|
}
|
|
1739
|
-
|
|
1910
|
+
const stillChallenge = await fcFrame.locator('#challenge-stage, .challenge-content').count();
|
|
1911
|
+
const successIndicators = await fcFrame.locator('[class*="success"], [class*="correct"], [class*="verified"], .game-success').count();
|
|
1912
|
+
if (successIndicators > 0) {
|
|
1913
|
+
results.push('[OK] FunCaptcha solved!');
|
|
1740
1914
|
break;
|
|
1741
1915
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const newChallenge = refreshedFrames.find((f) => f.url().includes('hcaptcha') && f.url().includes('challenge'));
|
|
1745
|
-
if (!newChallenge) {
|
|
1746
|
-
results.push('Challenge frame disappeared, captcha may be solved');
|
|
1747
|
-
solved = true;
|
|
1916
|
+
if (stillChallenge === 0) {
|
|
1917
|
+
results.push('[OK] FunCaptcha challenge dismissed — likely solved.');
|
|
1748
1918
|
break;
|
|
1749
1919
|
}
|
|
1920
|
+
if (attempt === maxAttempts - 1) {
|
|
1921
|
+
results.push(`Auto-solve exhausted after ${maxAttempts} attempts. Use click/drag-to/evaluate for manual solving.`);
|
|
1922
|
+
}
|
|
1923
|
+
else {
|
|
1924
|
+
results.push('Attempt did not solve, retrying...');
|
|
1925
|
+
await p.waitForTimeout(1500);
|
|
1926
|
+
}
|
|
1750
1927
|
}
|
|
1751
|
-
|
|
1752
|
-
results.push(
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
else {
|
|
1756
|
-
const checkmark = checkboxFrame.locator('.check.solved, #checkbox[aria-checked="true"]');
|
|
1757
|
-
if (await checkmark.count() > 0) {
|
|
1758
|
-
results.push(ok('hCaptcha solved', { status: 'verified' }));
|
|
1759
|
-
}
|
|
1760
|
-
else {
|
|
1761
|
-
results.push(warn('hCaptcha checkbox clicked, status unclear', { suggestion: 'Use "captcha-grid" to check for image challenge' }));
|
|
1928
|
+
catch (e) {
|
|
1929
|
+
results.push(`Vision model failed: ${e.message}`);
|
|
1930
|
+
results.push('Auto-solve requires a vision-capable model. Read the puzzle screenshot at .aurix-funcaptcha-puzzle.png and use click/drag-to/evaluate to solve manually.');
|
|
1931
|
+
break;
|
|
1762
1932
|
}
|
|
1763
1933
|
}
|
|
1764
1934
|
}
|
|
1765
1935
|
else {
|
|
1766
|
-
results.push(err('
|
|
1936
|
+
results.push(err('FunCaptcha frame not found', 'Use "detect-captcha" to scan the page first'));
|
|
1767
1937
|
}
|
|
1768
1938
|
}
|
|
1769
|
-
|
|
1770
|
-
results.push(err(
|
|
1939
|
+
catch (e) {
|
|
1940
|
+
results.push(err(`FunCaptcha auto-solve failed: ${e.message}`));
|
|
1771
1941
|
}
|
|
1772
1942
|
}
|
|
1773
|
-
|
|
1774
|
-
results.push(
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
await p.waitForTimeout(3000);
|
|
1787
|
-
results.push(ok('Turnstile checkbox clicked'));
|
|
1943
|
+
if (captchaType === 'mtcaptcha' || captchaType === 'geetest') {
|
|
1944
|
+
results.push(`Detected ${captchaType} challenge. Analyzing...`);
|
|
1945
|
+
const targetFrame = mtcaptchaFrame || geetestFrame || p;
|
|
1946
|
+
const hasSlider = await targetFrame.locator('.geetest_slider_button, .geetest_slider, [class*="slider_button"], [class*="slider-track"]').count();
|
|
1947
|
+
if (hasSlider > 0) {
|
|
1948
|
+
results.push('Type: SLIDER puzzle');
|
|
1949
|
+
const puzzleEl = targetFrame.locator('.geetest_panel, .geetest_widget, [class*="geetest_container"]').first();
|
|
1950
|
+
const screenshotPath = join(homedir(), '.aurix-slider-puzzle.png');
|
|
1951
|
+
try {
|
|
1952
|
+
if (await puzzleEl.count() > 0)
|
|
1953
|
+
await puzzleEl.screenshot({ path: screenshotPath });
|
|
1954
|
+
else
|
|
1955
|
+
await p.screenshot({ path: screenshotPath });
|
|
1788
1956
|
}
|
|
1789
|
-
|
|
1790
|
-
await
|
|
1791
|
-
await p.waitForTimeout(3000);
|
|
1792
|
-
results.push(warn('Turnstile frame clicked (managed challenge)', { next: 'Check if challenge resolved with screenshot' }));
|
|
1957
|
+
catch {
|
|
1958
|
+
await p.screenshot({ path: screenshotPath });
|
|
1793
1959
|
}
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1796
|
-
catch (e) {
|
|
1797
|
-
results.push(err(`Turnstile failed: ${e.message}`));
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
if (captchaType === 'funcaptcha') {
|
|
1801
|
-
results.push('FunCaptcha (Arkose Labs) detected. Auto-solving...');
|
|
1802
|
-
try {
|
|
1803
|
-
const fcFrame = funcaptchaFrame;
|
|
1804
|
-
if (fcFrame) {
|
|
1805
|
-
await p.waitForTimeout(2000);
|
|
1806
|
-
const instruction = await fcFrame.evaluate(() => {
|
|
1807
|
-
const h2 = document.querySelector('h2, h3, .challenge-title, #challenge-stage .title, [class*="instruction"], [class*="prompt"]');
|
|
1808
|
-
return h2?.textContent?.trim() || '';
|
|
1809
|
-
}).catch(() => '');
|
|
1810
|
-
if (instruction)
|
|
1811
|
-
results.push(`Instruction: "${instruction}"`);
|
|
1960
|
+
results.push(`Puzzle screenshot: ${screenshotPath}`);
|
|
1812
1961
|
const maxAttempts = 3;
|
|
1813
1962
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1814
1963
|
if (attempt > 0)
|
|
1815
|
-
results.push(`\
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1964
|
+
results.push(`\nSlider retry ${attempt}/${maxAttempts - 1}...`);
|
|
1965
|
+
const sliderInfo = await targetFrame.evaluate(() => {
|
|
1966
|
+
const info = {};
|
|
1967
|
+
const cut = document.querySelector('.geetest_cut, .geetest_piece_bg, [class*="geetest_cut"], [class*="slider_cut"], [class*="puzzle-gap"]');
|
|
1968
|
+
if (cut) {
|
|
1969
|
+
const cutRect = cut.getBoundingClientRect();
|
|
1970
|
+
const style = window.getComputedStyle(cut);
|
|
1971
|
+
info.cut = { left: cutRect.left, width: cutRect.width, styleLeft: parseFloat(style.left) || null, transform: style.transform || null };
|
|
1972
|
+
}
|
|
1973
|
+
const bg = document.querySelector('.geetest_canvas_bg, .geetest_bg, [class*="geetest_canvas"], canvas[class*="bg"]');
|
|
1974
|
+
if (bg) {
|
|
1975
|
+
const bgRect = bg.getBoundingClientRect();
|
|
1976
|
+
info.bg = { left: bgRect.left, width: bgRect.width };
|
|
1977
|
+
}
|
|
1978
|
+
const piece = document.querySelector('.geetest_piece, .geetest_slider_piece, [class*="slider_piece"]');
|
|
1979
|
+
if (piece) {
|
|
1980
|
+
const pieceRect = piece.getBoundingClientRect();
|
|
1981
|
+
info.piece = { left: pieceRect.left, width: pieceRect.width };
|
|
1982
|
+
}
|
|
1983
|
+
const slider = document.querySelector('.geetest_slider_button, .geetest_slider_knob, [class*="slider_button"]');
|
|
1984
|
+
if (slider) {
|
|
1985
|
+
const sliderRect = slider.getBoundingClientRect();
|
|
1986
|
+
info.slider = { left: sliderRect.left, width: sliderRect.width, centerX: sliderRect.left + sliderRect.width / 2, centerY: sliderRect.top + sliderRect.height / 2 };
|
|
1987
|
+
}
|
|
1988
|
+
const track = document.querySelector('.geetest_slider_track, .geetest_slider, [class*="slider_track"]');
|
|
1989
|
+
if (track)
|
|
1990
|
+
info.track = { width: track.getBoundingClientRect().width };
|
|
1991
|
+
return info;
|
|
1992
|
+
});
|
|
1993
|
+
let gapOffset = null;
|
|
1994
|
+
if (sliderInfo.cut && sliderInfo.bg) {
|
|
1995
|
+
if (sliderInfo.cut.styleLeft && sliderInfo.cut.styleLeft > 0) {
|
|
1996
|
+
gapOffset = Math.round(sliderInfo.cut.styleLeft);
|
|
1997
|
+
}
|
|
1998
|
+
else {
|
|
1999
|
+
gapOffset = Math.round(sliderInfo.cut.left - sliderInfo.bg.left);
|
|
2000
|
+
}
|
|
1819
2001
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
2002
|
+
if (gapOffset === null && sliderInfo.cut?.transform && sliderInfo.cut.transform !== 'none') {
|
|
2003
|
+
const match = sliderInfo.cut.transform.match(/matrix\(.*?,\s*([\d.]+)/);
|
|
2004
|
+
if (match)
|
|
2005
|
+
gapOffset = Math.round(parseFloat(match[1]));
|
|
1822
2006
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
if (clickMatch) {
|
|
1837
|
-
const cx = offsetX + parseFloat(clickMatch[1]);
|
|
1838
|
-
const cy = offsetY + parseFloat(clickMatch[2]);
|
|
1839
|
-
await humanMove(cx, cy, p);
|
|
1840
|
-
await p.waitForTimeout(100 + Math.random() * 150);
|
|
1841
|
-
await p.mouse.down();
|
|
1842
|
-
await p.waitForTimeout(60 + Math.random() * 80);
|
|
1843
|
-
await p.mouse.up();
|
|
1844
|
-
results.push(`Clicked at (${Math.round(cx)}, ${Math.round(cy)})`);
|
|
1845
|
-
await p.waitForTimeout(2000);
|
|
2007
|
+
if (gapOffset === null) {
|
|
2008
|
+
results.push('DOM gap detection failed, using vision model...');
|
|
2009
|
+
try {
|
|
2010
|
+
const ssBase64 = readFileBase64(screenshotPath);
|
|
2011
|
+
const visionResp = await visionClassify(ssBase64, 'This is a slider puzzle captcha. There is a gap/hole in the background image where a puzzle piece needs to go. Estimate the horizontal pixel position of the CENTER of the gap, measured from the LEFT edge of the puzzle image. Reply with ONLY the number (e.g. "145").');
|
|
2012
|
+
const parsed = parseInt(visionResp.replace(/[^\d]/g, ''));
|
|
2013
|
+
if (!isNaN(parsed) && parsed > 10 && parsed < 500) {
|
|
2014
|
+
gapOffset = parsed;
|
|
2015
|
+
results.push(`Vision model: gap at ~${gapOffset}px`);
|
|
2016
|
+
}
|
|
2017
|
+
else {
|
|
2018
|
+
results.push(`Vision model returned: "${visionResp}" — could not parse gap position`);
|
|
2019
|
+
}
|
|
1846
2020
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2021
|
+
catch (e) {
|
|
2022
|
+
results.push(`Vision model failed: ${e.message}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (gapOffset === null) {
|
|
2026
|
+
results.push('[WARN] Could not determine gap position. Use "slider-analyze" for manual analysis, then "drag-to" to slide.');
|
|
2027
|
+
break;
|
|
2028
|
+
}
|
|
2029
|
+
const pieceHalf = Math.round((sliderInfo.piece?.width || 44) / 2);
|
|
2030
|
+
const adjusted = gapOffset - pieceHalf;
|
|
2031
|
+
results.push(`Gap: ${gapOffset}px, piece half: ${pieceHalf}px, drag distance: ${adjusted}px`);
|
|
2032
|
+
if (sliderInfo.slider) {
|
|
2033
|
+
try {
|
|
2034
|
+
const startX = sliderInfo.slider.centerX;
|
|
2035
|
+
const startY = sliderInfo.slider.centerY;
|
|
2036
|
+
const endX = startX + adjusted;
|
|
2037
|
+
await humanMove(startX, startY, p);
|
|
2038
|
+
await p.waitForTimeout(150 + Math.random() * 250);
|
|
1854
2039
|
await p.mouse.down();
|
|
1855
2040
|
await p.waitForTimeout(200 + Math.random() * 300);
|
|
1856
|
-
const steps =
|
|
2041
|
+
const steps = 25 + Math.floor(Math.random() * 20);
|
|
1857
2042
|
for (let i = 1; i <= steps; i++) {
|
|
1858
2043
|
const progress = i / steps;
|
|
1859
2044
|
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
1860
|
-
|
|
1861
|
-
|
|
2045
|
+
const x = startX + adjusted * eased + (Math.random() - 0.5) * 2;
|
|
2046
|
+
const y = startY + (Math.random() - 0.5) * 2;
|
|
2047
|
+
await p.mouse.move(x, y);
|
|
2048
|
+
await p.waitForTimeout(10 + Math.random() * 20);
|
|
1862
2049
|
}
|
|
1863
|
-
await p.mouse.move(
|
|
2050
|
+
await p.mouse.move(endX, startY);
|
|
1864
2051
|
await p.waitForTimeout(150);
|
|
1865
2052
|
await p.mouse.up();
|
|
1866
|
-
results.push(`Dragged from (${Math.round(fromX)},${Math.round(fromY)}) to (${Math.round(toX)},${Math.round(toY)})`);
|
|
1867
2053
|
await p.waitForTimeout(2000);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
const rBox = await rotator.boundingBox();
|
|
1874
|
-
if (rBox) {
|
|
1875
|
-
const cx = rBox.x + rBox.width / 2;
|
|
1876
|
-
const cy = rBox.y + rBox.height / 2;
|
|
1877
|
-
const radius = rBox.width / 2;
|
|
1878
|
-
const startX = cx + radius;
|
|
1879
|
-
const startY = cy;
|
|
1880
|
-
const endAngle = (degrees * Math.PI) / 180;
|
|
1881
|
-
const endX = cx + radius * Math.cos(endAngle);
|
|
1882
|
-
const endY = cy + radius * Math.sin(endAngle);
|
|
1883
|
-
await humanMove(startX, startY, p);
|
|
1884
|
-
await p.waitForTimeout(150);
|
|
1885
|
-
await p.mouse.down();
|
|
1886
|
-
await p.waitForTimeout(200);
|
|
1887
|
-
const steps = 30;
|
|
1888
|
-
for (let i = 1; i <= steps; i++) {
|
|
1889
|
-
const angle = (endAngle * i) / steps;
|
|
1890
|
-
await p.mouse.move(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle));
|
|
1891
|
-
await p.waitForTimeout(15 + Math.random() * 10);
|
|
1892
|
-
}
|
|
1893
|
-
await p.mouse.move(endX, endY);
|
|
1894
|
-
await p.waitForTimeout(150);
|
|
1895
|
-
await p.mouse.up();
|
|
1896
|
-
results.push(`Rotated ${degrees}°`);
|
|
1897
|
-
await p.waitForTimeout(2000);
|
|
1898
|
-
}
|
|
2054
|
+
results.push('Slider dragged, checking result...');
|
|
2055
|
+
const successEl = await targetFrame.locator('.geetest_success, .geetest_tip_success, [class*="success"], [class*="verified"]').count();
|
|
2056
|
+
if (successEl > 0) {
|
|
2057
|
+
results.push('[OK] Slider captcha solved!');
|
|
2058
|
+
break;
|
|
1899
2059
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2060
|
+
const failEl = await targetFrame.locator('.geetest_fail, .geetest_tip_fail, [class*="fail"], [class*="error"], [class*="retry"]').count();
|
|
2061
|
+
if (failEl > 0) {
|
|
2062
|
+
results.push('Slider attempt failed, retrying...');
|
|
2063
|
+
const refreshBtn = targetFrame.locator('.geetest_refresh, [class*="refresh"], [class*="retry"]').first();
|
|
2064
|
+
if (await refreshBtn.count() > 0)
|
|
2065
|
+
await refreshBtn.click().catch(() => { });
|
|
2066
|
+
await p.waitForTimeout(1500);
|
|
2067
|
+
try {
|
|
2068
|
+
if (await puzzleEl.count() > 0)
|
|
2069
|
+
await puzzleEl.screenshot({ path: screenshotPath });
|
|
2070
|
+
}
|
|
2071
|
+
catch { }
|
|
2072
|
+
continue;
|
|
1902
2073
|
}
|
|
1903
|
-
|
|
1904
|
-
else {
|
|
1905
|
-
results.push(`Could not parse vision model response: "${visionResp}"`);
|
|
1906
|
-
results.push('Falling back to manual mode. Read the puzzle screenshot and use click/drag-to/evaluate to solve.');
|
|
2074
|
+
results.push('[OK] Slider dragged — outcome unconfirmed, check page state.');
|
|
1907
2075
|
break;
|
|
1908
2076
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
if (successIndicators > 0) {
|
|
1912
|
-
results.push('[OK] FunCaptcha solved!');
|
|
2077
|
+
catch (e) {
|
|
2078
|
+
results.push(`Drag failed: ${e.message}`);
|
|
1913
2079
|
break;
|
|
1914
2080
|
}
|
|
1915
|
-
if (stillChallenge === 0) {
|
|
1916
|
-
results.push('[OK] FunCaptcha challenge dismissed — likely solved.');
|
|
1917
|
-
break;
|
|
1918
|
-
}
|
|
1919
|
-
if (attempt === maxAttempts - 1) {
|
|
1920
|
-
results.push(`Auto-solve exhausted after ${maxAttempts} attempts. Use click/drag-to/evaluate for manual solving.`);
|
|
1921
|
-
}
|
|
1922
|
-
else {
|
|
1923
|
-
results.push('Attempt did not solve, retrying...');
|
|
1924
|
-
await p.waitForTimeout(1500);
|
|
1925
|
-
}
|
|
1926
2081
|
}
|
|
1927
|
-
|
|
1928
|
-
results.push(
|
|
1929
|
-
results.push('Auto-solve requires a vision-capable model. Read the puzzle screenshot at .aurix-funcaptcha-puzzle.png and use click/drag-to/evaluate to solve manually.');
|
|
2082
|
+
else {
|
|
2083
|
+
results.push('[WARN] Slider handle not found in DOM.');
|
|
1930
2084
|
break;
|
|
1931
2085
|
}
|
|
1932
2086
|
}
|
|
1933
2087
|
}
|
|
1934
2088
|
else {
|
|
1935
|
-
results.push(
|
|
2089
|
+
results.push('Type: IMAGE challenge');
|
|
2090
|
+
const gridResult = await solveCaptchaGrid(p, targetFrame, captchaType);
|
|
2091
|
+
results.push(gridResult);
|
|
1936
2092
|
}
|
|
1937
2093
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
catch {
|
|
1957
|
-
await p.screenshot({ path: screenshotPath });
|
|
1958
|
-
}
|
|
1959
|
-
results.push(`Puzzle screenshot: ${screenshotPath}`);
|
|
1960
|
-
const maxAttempts = 3;
|
|
1961
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1962
|
-
if (attempt > 0)
|
|
1963
|
-
results.push(`\nSlider retry ${attempt}/${maxAttempts - 1}...`);
|
|
1964
|
-
const sliderInfo = await targetFrame.evaluate(() => {
|
|
1965
|
-
const info = {};
|
|
1966
|
-
const cut = document.querySelector('.geetest_cut, .geetest_piece_bg, [class*="geetest_cut"], [class*="slider_cut"], [class*="puzzle-gap"]');
|
|
1967
|
-
if (cut) {
|
|
1968
|
-
const cutRect = cut.getBoundingClientRect();
|
|
1969
|
-
const style = window.getComputedStyle(cut);
|
|
1970
|
-
info.cut = { left: cutRect.left, width: cutRect.width, styleLeft: parseFloat(style.left) || null, transform: style.transform || null };
|
|
1971
|
-
}
|
|
1972
|
-
const bg = document.querySelector('.geetest_canvas_bg, .geetest_bg, [class*="geetest_canvas"], canvas[class*="bg"]');
|
|
1973
|
-
if (bg) {
|
|
1974
|
-
const bgRect = bg.getBoundingClientRect();
|
|
1975
|
-
info.bg = { left: bgRect.left, width: bgRect.width };
|
|
1976
|
-
}
|
|
1977
|
-
const piece = document.querySelector('.geetest_piece, .geetest_slider_piece, [class*="slider_piece"]');
|
|
1978
|
-
if (piece) {
|
|
1979
|
-
const pieceRect = piece.getBoundingClientRect();
|
|
1980
|
-
info.piece = { left: pieceRect.left, width: pieceRect.width };
|
|
1981
|
-
}
|
|
1982
|
-
const slider = document.querySelector('.geetest_slider_button, .geetest_slider_knob, [class*="slider_button"]');
|
|
1983
|
-
if (slider) {
|
|
1984
|
-
const sliderRect = slider.getBoundingClientRect();
|
|
1985
|
-
info.slider = { left: sliderRect.left, width: sliderRect.width, centerX: sliderRect.left + sliderRect.width / 2, centerY: sliderRect.top + sliderRect.height / 2 };
|
|
1986
|
-
}
|
|
1987
|
-
const track = document.querySelector('.geetest_slider_track, .geetest_slider, [class*="slider_track"]');
|
|
1988
|
-
if (track)
|
|
1989
|
-
info.track = { width: track.getBoundingClientRect().width };
|
|
1990
|
-
return info;
|
|
1991
|
-
});
|
|
1992
|
-
let gapOffset = null;
|
|
1993
|
-
if (sliderInfo.cut && sliderInfo.bg) {
|
|
1994
|
-
if (sliderInfo.cut.styleLeft && sliderInfo.cut.styleLeft > 0) {
|
|
1995
|
-
gapOffset = Math.round(sliderInfo.cut.styleLeft);
|
|
1996
|
-
}
|
|
1997
|
-
else {
|
|
1998
|
-
gapOffset = Math.round(sliderInfo.cut.left - sliderInfo.bg.left);
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
if (gapOffset === null && sliderInfo.cut?.transform && sliderInfo.cut.transform !== 'none') {
|
|
2002
|
-
const match = sliderInfo.cut.transform.match(/matrix\(.*?,\s*([\d.]+)/);
|
|
2003
|
-
if (match)
|
|
2004
|
-
gapOffset = Math.round(parseFloat(match[1]));
|
|
2005
|
-
}
|
|
2006
|
-
if (gapOffset === null) {
|
|
2007
|
-
results.push('DOM gap detection failed, using vision model...');
|
|
2008
|
-
try {
|
|
2009
|
-
const ssBase64 = readFileBase64(screenshotPath);
|
|
2010
|
-
const visionResp = await visionClassify(ssBase64, 'This is a slider puzzle captcha. There is a gap/hole in the background image where a puzzle piece needs to go. Estimate the horizontal pixel position of the CENTER of the gap, measured from the LEFT edge of the puzzle image. Reply with ONLY the number (e.g. "145").');
|
|
2011
|
-
const parsed = parseInt(visionResp.replace(/[^\d]/g, ''));
|
|
2012
|
-
if (!isNaN(parsed) && parsed > 10 && parsed < 500) {
|
|
2013
|
-
gapOffset = parsed;
|
|
2014
|
-
results.push(`Vision model: gap at ~${gapOffset}px`);
|
|
2015
|
-
}
|
|
2016
|
-
else {
|
|
2017
|
-
results.push(`Vision model returned: "${visionResp}" — could not parse gap position`);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
catch (e) {
|
|
2021
|
-
results.push(`Vision model failed: ${e.message}`);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
if (gapOffset === null) {
|
|
2025
|
-
results.push('[WARN] Could not determine gap position. Use "slider-analyze" for manual analysis, then "drag-to" to slide.');
|
|
2026
|
-
break;
|
|
2027
|
-
}
|
|
2028
|
-
const pieceHalf = Math.round((sliderInfo.piece?.width || 44) / 2);
|
|
2029
|
-
const adjusted = gapOffset - pieceHalf;
|
|
2030
|
-
results.push(`Gap: ${gapOffset}px, piece half: ${pieceHalf}px, drag distance: ${adjusted}px`);
|
|
2031
|
-
if (sliderInfo.slider) {
|
|
2032
|
-
try {
|
|
2033
|
-
const startX = sliderInfo.slider.centerX;
|
|
2034
|
-
const startY = sliderInfo.slider.centerY;
|
|
2035
|
-
const endX = startX + adjusted;
|
|
2036
|
-
await humanMove(startX, startY, p);
|
|
2037
|
-
await p.waitForTimeout(150 + Math.random() * 250);
|
|
2038
|
-
await p.mouse.down();
|
|
2039
|
-
await p.waitForTimeout(200 + Math.random() * 300);
|
|
2040
|
-
const steps = 25 + Math.floor(Math.random() * 20);
|
|
2041
|
-
for (let i = 1; i <= steps; i++) {
|
|
2042
|
-
const progress = i / steps;
|
|
2043
|
-
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
|
|
2044
|
-
const x = startX + adjusted * eased + (Math.random() - 0.5) * 2;
|
|
2045
|
-
const y = startY + (Math.random() - 0.5) * 2;
|
|
2046
|
-
await p.mouse.move(x, y);
|
|
2047
|
-
await p.waitForTimeout(10 + Math.random() * 20);
|
|
2048
|
-
}
|
|
2049
|
-
await p.mouse.move(endX, startY);
|
|
2050
|
-
await p.waitForTimeout(150);
|
|
2051
|
-
await p.mouse.up();
|
|
2052
|
-
await p.waitForTimeout(2000);
|
|
2053
|
-
results.push('Slider dragged, checking result...');
|
|
2054
|
-
const successEl = await targetFrame.locator('.geetest_success, .geetest_tip_success, [class*="success"], [class*="verified"]').count();
|
|
2055
|
-
if (successEl > 0) {
|
|
2056
|
-
results.push('[OK] Slider captcha solved!');
|
|
2057
|
-
break;
|
|
2058
|
-
}
|
|
2059
|
-
const failEl = await targetFrame.locator('.geetest_fail, .geetest_tip_fail, [class*="fail"], [class*="error"], [class*="retry"]').count();
|
|
2060
|
-
if (failEl > 0) {
|
|
2061
|
-
results.push('Slider attempt failed, retrying...');
|
|
2062
|
-
const refreshBtn = targetFrame.locator('.geetest_refresh, [class*="refresh"], [class*="retry"]').first();
|
|
2063
|
-
if (await refreshBtn.count() > 0)
|
|
2064
|
-
await refreshBtn.click().catch(() => { });
|
|
2065
|
-
await p.waitForTimeout(1500);
|
|
2066
|
-
try {
|
|
2067
|
-
if (await puzzleEl.count() > 0)
|
|
2068
|
-
await puzzleEl.screenshot({ path: screenshotPath });
|
|
2094
|
+
if (captchaType === 'unknown') {
|
|
2095
|
+
const imgCaptcha = p.locator('img[src*="captcha"], #captcha-image, .captcha-image, img.captcha');
|
|
2096
|
+
if (await imgCaptcha.count() > 0) {
|
|
2097
|
+
results.push('Text-based captcha detected.');
|
|
2098
|
+
const screenshotPath = join(homedir(), '.aurix-captcha-challenge.png');
|
|
2099
|
+
await imgCaptcha.first().screenshot({ path: screenshotPath });
|
|
2100
|
+
results.push(`Captcha image saved: ${screenshotPath}`);
|
|
2101
|
+
try {
|
|
2102
|
+
const ssBase64 = readFileBase64(screenshotPath);
|
|
2103
|
+
const visionResp = await visionClassify(ssBase64, 'Read the text/numbers in this captcha image. Reply with ONLY the exact text shown, nothing else.');
|
|
2104
|
+
const captchaText = visionResp.replace(/[^a-zA-Z0-9]/g, '').trim();
|
|
2105
|
+
if (captchaText.length >= 2) {
|
|
2106
|
+
const input = p.locator('input[name*="captcha"], input[id*="captcha"], input[placeholder*="captcha" i], input[placeholder*="code" i]');
|
|
2107
|
+
if (await input.count() > 0) {
|
|
2108
|
+
await input.first().click();
|
|
2109
|
+
await input.first().fill('');
|
|
2110
|
+
for (const char of captchaText) {
|
|
2111
|
+
await input.first().type(char, { delay: 80 + Math.random() * 120 });
|
|
2069
2112
|
}
|
|
2070
|
-
|
|
2071
|
-
continue;
|
|
2113
|
+
results.push(`[OK] Auto-filled captcha text: "${captchaText}"`);
|
|
2072
2114
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
}
|
|
2076
|
-
catch (e) {
|
|
2077
|
-
results.push(`Drag failed: ${e.message}`);
|
|
2078
|
-
break;
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
else {
|
|
2082
|
-
results.push('[WARN] Slider handle not found in DOM.');
|
|
2083
|
-
break;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
else {
|
|
2088
|
-
results.push('Type: IMAGE challenge');
|
|
2089
|
-
const gridResult = await solveCaptchaGrid(p, targetFrame, captchaType);
|
|
2090
|
-
results.push(gridResult);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
if (captchaType === 'unknown') {
|
|
2094
|
-
const imgCaptcha = p.locator('img[src*="captcha"], #captcha-image, .captcha-image, img.captcha');
|
|
2095
|
-
if (await imgCaptcha.count() > 0) {
|
|
2096
|
-
results.push('Text-based captcha detected.');
|
|
2097
|
-
const screenshotPath = join(homedir(), '.aurix-captcha-challenge.png');
|
|
2098
|
-
await imgCaptcha.first().screenshot({ path: screenshotPath });
|
|
2099
|
-
results.push(`Captcha image saved: ${screenshotPath}`);
|
|
2100
|
-
try {
|
|
2101
|
-
const ssBase64 = readFileBase64(screenshotPath);
|
|
2102
|
-
const visionResp = await visionClassify(ssBase64, 'Read the text/numbers in this captcha image. Reply with ONLY the exact text shown, nothing else.');
|
|
2103
|
-
const captchaText = visionResp.replace(/[^a-zA-Z0-9]/g, '').trim();
|
|
2104
|
-
if (captchaText.length >= 2) {
|
|
2105
|
-
const input = p.locator('input[name*="captcha"], input[id*="captcha"], input[placeholder*="captcha" i], input[placeholder*="code" i]');
|
|
2106
|
-
if (await input.count() > 0) {
|
|
2107
|
-
await input.first().click();
|
|
2108
|
-
await input.first().fill('');
|
|
2109
|
-
for (const char of captchaText) {
|
|
2110
|
-
await input.first().type(char, { delay: 80 + Math.random() * 120 });
|
|
2115
|
+
else {
|
|
2116
|
+
results.push(`Vision model read: "${captchaText}" — but no captcha input field found. Use "fill" to type it manually.`);
|
|
2111
2117
|
}
|
|
2112
|
-
results.push(`[OK] Auto-filled captcha text: "${captchaText}"`);
|
|
2113
2118
|
}
|
|
2114
2119
|
else {
|
|
2115
|
-
results.push(`Vision model
|
|
2120
|
+
results.push(`Vision model returned: "${visionResp}" — could not read captcha text`);
|
|
2121
|
+
results.push('Read the screenshot and use "fill" to type the captcha text manually.');
|
|
2116
2122
|
}
|
|
2117
2123
|
}
|
|
2118
|
-
|
|
2119
|
-
results.push(`Vision
|
|
2120
|
-
results.push('Read the screenshot and use "fill" to type
|
|
2124
|
+
catch (e) {
|
|
2125
|
+
results.push(`Vision auto-fill failed: ${e.message}`);
|
|
2126
|
+
results.push('Read the captcha screenshot and use "fill" to type it manually.');
|
|
2121
2127
|
}
|
|
2122
2128
|
}
|
|
2123
|
-
|
|
2124
|
-
results.push(
|
|
2125
|
-
|
|
2129
|
+
else {
|
|
2130
|
+
results.push('No recognizable captcha. Taking screenshot and scanning...');
|
|
2131
|
+
const screenshotPath = join(homedir(), '.aurix-captcha-challenge.png');
|
|
2132
|
+
await p.screenshot({ path: screenshotPath });
|
|
2133
|
+
results.push(`Screenshot saved: ${screenshotPath}`);
|
|
2134
|
+
results.push('Use "captcha-grid" to scan for any challenge overlay.');
|
|
2126
2135
|
}
|
|
2127
2136
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2137
|
+
const screenshotPath = join(homedir(), '.aurix-captcha-after.png');
|
|
2138
|
+
await p.screenshot({ path: screenshotPath });
|
|
2139
|
+
results.push(`\nPost-attempt screenshot: ${screenshotPath}`);
|
|
2140
|
+
return results.join('\n');
|
|
2141
|
+
};
|
|
2142
|
+
try {
|
|
2143
|
+
return await Promise.race([
|
|
2144
|
+
_solveLogic(),
|
|
2145
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('solve-captcha timed out (60s)')), _solveTimeout)),
|
|
2146
|
+
]);
|
|
2147
|
+
}
|
|
2148
|
+
catch (e) {
|
|
2149
|
+
results.push(`\n[TIMEOUT] ${e.message}`);
|
|
2150
|
+
results.push('Auto-solve did not complete. Use "captcha-grid" and "click-tile" for manual solving.');
|
|
2151
|
+
return results.join('\n');
|
|
2135
2152
|
}
|
|
2136
|
-
const screenshotPath = join(homedir(), '.aurix-captcha-after.png');
|
|
2137
|
-
await p.screenshot({ path: screenshotPath });
|
|
2138
|
-
results.push(`\nPost-attempt screenshot: ${screenshotPath}`);
|
|
2139
|
-
return results.join('\n');
|
|
2140
2153
|
}
|
|
2141
2154
|
case 'captcha-grid': {
|
|
2142
2155
|
const p = await ensureBrowser();
|
|
@@ -2611,6 +2624,27 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2611
2624
|
results.push('=== SIGNUP ASSIST ===');
|
|
2612
2625
|
results.push(`Provided: ${Object.keys(data).join(', ')}`);
|
|
2613
2626
|
results.push('');
|
|
2627
|
+
const cookieSelectors = [
|
|
2628
|
+
'button:has-text("Accept All")', 'button:has-text("Accept all")', 'button:has-text("accept all")',
|
|
2629
|
+
'button:has-text("Accept")', 'button:has-text("Accept Cookies")',
|
|
2630
|
+
'button:has-text("I agree")', 'button:has-text("Got it")', 'button:has-text("OK")',
|
|
2631
|
+
'button:has-text("Allow All")', 'button:has-text("Allow all")',
|
|
2632
|
+
'[id*="cookie"] button', '[class*="cookie"] button',
|
|
2633
|
+
'[id*="consent"] button', '[class*="consent"] button',
|
|
2634
|
+
'.cc-accept', '.cookie-accept', '#accept-cookies',
|
|
2635
|
+
];
|
|
2636
|
+
for (const sel of cookieSelectors) {
|
|
2637
|
+
try {
|
|
2638
|
+
const btn = p.locator(sel).first();
|
|
2639
|
+
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
2640
|
+
await btn.click({ timeout: 2000 });
|
|
2641
|
+
results.push(` ✓ Dismissed cookie banner: ${sel}`);
|
|
2642
|
+
await p.waitForTimeout(500);
|
|
2643
|
+
break;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
catch { }
|
|
2647
|
+
}
|
|
2614
2648
|
const allFrames = [p, ...p.frames().filter(f => f !== p.mainFrame())];
|
|
2615
2649
|
let activeFrame = p;
|
|
2616
2650
|
for (const frame of allFrames) {
|
|
@@ -2620,6 +2654,42 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2620
2654
|
break;
|
|
2621
2655
|
}
|
|
2622
2656
|
}
|
|
2657
|
+
const hasEmailField = await activeFrame.locator('input[type="email"]:visible, input[name*="email" i]:visible, input[autocomplete="email"]:visible').count() > 0;
|
|
2658
|
+
const hasPasswordField = await activeFrame.locator('input[type="password"]:visible').count() > 0;
|
|
2659
|
+
if (!hasEmailField && !hasPasswordField) {
|
|
2660
|
+
const ctaSelectors = [
|
|
2661
|
+
'button:has-text("Sign Up With Email")', 'button:has-text("sign up with email")',
|
|
2662
|
+
'a:has-text("Sign Up With Email")', 'a:has-text("sign up with email")',
|
|
2663
|
+
'button:has-text("Sign up with email")',
|
|
2664
|
+
'button:has-text("Create Account")', 'button:has-text("create account")',
|
|
2665
|
+
'button:has-text("Sign Up")', 'button:has-text("sign up")',
|
|
2666
|
+
'button:has-text("Register")', 'button:has-text("register")',
|
|
2667
|
+
'button:has-text("Get Started")', 'button:has-text("get started")',
|
|
2668
|
+
'button:has-text("Continue with Email")', 'button:has-text("continue with email")',
|
|
2669
|
+
'button:has-text("Use Email")', 'button:has-text("use email")',
|
|
2670
|
+
'a:has-text("Sign Up")', 'a:has-text("Register")',
|
|
2671
|
+
'[data-testid*="signup"]', '[data-testid*="email"]',
|
|
2672
|
+
];
|
|
2673
|
+
for (const sel of ctaSelectors) {
|
|
2674
|
+
try {
|
|
2675
|
+
const btn = activeFrame.locator(sel).first();
|
|
2676
|
+
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
2677
|
+
await btn.click({ timeout: 3000 });
|
|
2678
|
+
results.push(` ✓ Clicked CTA: ${sel}`);
|
|
2679
|
+
await p.waitForTimeout(1500);
|
|
2680
|
+
break;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
catch { }
|
|
2684
|
+
}
|
|
2685
|
+
for (const frame of [p, ...p.frames().filter(f => f !== p.mainFrame())]) {
|
|
2686
|
+
const inputs = await frame.locator('input:visible, select:visible, textarea:visible').count();
|
|
2687
|
+
if (inputs > 0) {
|
|
2688
|
+
activeFrame = frame;
|
|
2689
|
+
break;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2623
2693
|
results.push(`Active frame: ${activeFrame === p ? 'main page' : 'iframe'} (${await activeFrame.locator('input:visible, select:visible, textarea:visible').count()} fields)`);
|
|
2624
2694
|
const fillField = async (selectors, val, label) => {
|
|
2625
2695
|
for (const sel of selectors) {
|
|
@@ -2632,11 +2702,11 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2632
2702
|
return true;
|
|
2633
2703
|
}
|
|
2634
2704
|
try {
|
|
2635
|
-
await loc.fill(val, { timeout:
|
|
2705
|
+
await loc.fill(val, { timeout: 1500 });
|
|
2636
2706
|
}
|
|
2637
2707
|
catch {
|
|
2638
|
-
await loc.click({ timeout:
|
|
2639
|
-
await loc.pressSequentially(val, { delay: 30, timeout:
|
|
2708
|
+
await loc.click({ timeout: 1500 });
|
|
2709
|
+
await loc.pressSequentially(val, { delay: 30, timeout: 5000 });
|
|
2640
2710
|
}
|
|
2641
2711
|
results.push(` ✓ ${label}: filled`);
|
|
2642
2712
|
return true;
|
|
@@ -2656,7 +2726,7 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2656
2726
|
results.push(` ✓ ${label}: already checked`);
|
|
2657
2727
|
return true;
|
|
2658
2728
|
}
|
|
2659
|
-
await loc.click({ timeout:
|
|
2729
|
+
await loc.click({ timeout: 1500 });
|
|
2660
2730
|
results.push(` ✓ ${label}: clicked`);
|
|
2661
2731
|
return true;
|
|
2662
2732
|
}
|
|
@@ -2720,102 +2790,73 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2720
2790
|
results.push('--- Filling fields ---');
|
|
2721
2791
|
if (data.email) {
|
|
2722
2792
|
await fillField([
|
|
2723
|
-
'input[type="email"]',
|
|
2724
|
-
'input[
|
|
2725
|
-
'input[name*="username" i]', 'input[name*="MemberName"]',
|
|
2726
|
-
'input[id*="email" i]', 'input[id*="username" i]',
|
|
2727
|
-
'input[placeholder*="email" i]', 'input[placeholder*="Email"]',
|
|
2728
|
-
'input[autocomplete="email"]', 'input[autocomplete="username"]',
|
|
2729
|
-
'input[name="loginfmt"]',
|
|
2793
|
+
'input[type="email"]', 'input[name*="email" i]', 'input[id*="email" i]',
|
|
2794
|
+
'input[autocomplete="email"]', 'input[placeholder*="email" i]',
|
|
2730
2795
|
], data.email, 'Email');
|
|
2731
2796
|
}
|
|
2732
2797
|
if (data.password) {
|
|
2733
2798
|
await fillField([
|
|
2734
|
-
'input[type="password"]',
|
|
2735
|
-
'input[
|
|
2736
|
-
'input[id*="password" i]', 'input[name*="pass" i]',
|
|
2737
|
-
'input[autocomplete="new-password"]', 'input[autocomplete="current-password"]',
|
|
2799
|
+
'input[type="password"]', 'input[name*="password" i]', 'input[id*="password" i]',
|
|
2800
|
+
'input[autocomplete="new-password"]',
|
|
2738
2801
|
], data.password, 'Password');
|
|
2739
2802
|
}
|
|
2740
2803
|
if (data.firstName) {
|
|
2741
2804
|
await fillField([
|
|
2742
|
-
'input[name*="
|
|
2743
|
-
'input[
|
|
2744
|
-
'input[autocomplete="given-name"]',
|
|
2745
|
-
'input[placeholder*="first name" i]', 'input[placeholder*="First"]',
|
|
2746
|
-
'input[name="NameInput"]',
|
|
2805
|
+
'input[name*="first" i]', 'input[id*="first" i]',
|
|
2806
|
+
'input[autocomplete="given-name"]', 'input[placeholder*="first" i]',
|
|
2747
2807
|
], data.firstName, 'First name');
|
|
2748
2808
|
}
|
|
2749
2809
|
if (data.lastName) {
|
|
2750
2810
|
await fillField([
|
|
2751
|
-
'input[name*="
|
|
2752
|
-
'input[
|
|
2753
|
-
'input[autocomplete="family-name"]',
|
|
2754
|
-
'input[placeholder*="last name" i]', 'input[placeholder*="Last"]',
|
|
2755
|
-
'input[name="LastName"]',
|
|
2811
|
+
'input[name*="last" i]', 'input[id*="last" i]',
|
|
2812
|
+
'input[autocomplete="family-name"]', 'input[placeholder*="last" i]',
|
|
2756
2813
|
], data.lastName, 'Last name');
|
|
2757
2814
|
}
|
|
2758
2815
|
if (data.firstName && !data.lastName) {
|
|
2759
2816
|
await fillField([
|
|
2760
|
-
'input[name*="name" i]', 'input[id*="name" i]',
|
|
2761
|
-
'input[autocomplete="name"]',
|
|
2817
|
+
'input[name*="name" i]', 'input[id*="name" i]', 'input[autocomplete="name"]',
|
|
2762
2818
|
], data.firstName + ' User', 'Full name');
|
|
2763
2819
|
}
|
|
2764
2820
|
if (data.phone) {
|
|
2765
2821
|
await fillField([
|
|
2766
|
-
'input[type="tel"]', 'input[name*="phone" i]', 'input[
|
|
2767
|
-
'input[id*="phone" i]', 'input[autocomplete="tel"]',
|
|
2768
|
-
'input[placeholder*="phone" i]',
|
|
2822
|
+
'input[type="tel"]', 'input[name*="phone" i]', 'input[autocomplete="tel"]',
|
|
2769
2823
|
], data.phone, 'Phone');
|
|
2770
2824
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2825
|
+
if (data.birthYear || data.birthMonth || data.birthDay) {
|
|
2826
|
+
const birthYear = data.birthYear || '2003';
|
|
2827
|
+
const birthMonth = data.birthMonth || 'January';
|
|
2828
|
+
const birthDay = data.birthDay || '15';
|
|
2829
|
+
await selectDropdown([
|
|
2830
|
+
'select[id*="year" i]', 'select[name*="year" i]',
|
|
2831
|
+
], birthYear, 'Birth year');
|
|
2832
|
+
await selectDropdown([
|
|
2833
|
+
'select[id*="month" i]', 'select[name*="month" i]',
|
|
2834
|
+
], birthMonth, 'Birth month');
|
|
2835
|
+
await selectDropdown([
|
|
2836
|
+
'select[id*="day" i]', 'select[name*="day" i]',
|
|
2837
|
+
], birthDay, 'Birth day');
|
|
2838
|
+
}
|
|
2839
|
+
if (data.country) {
|
|
2840
|
+
await selectDropdown([
|
|
2841
|
+
'select[name*="country" i]', 'select[id*="country" i]',
|
|
2842
|
+
], data.country, 'Country');
|
|
2843
|
+
}
|
|
2844
|
+
if (data.username) {
|
|
2845
|
+
await fillField([
|
|
2846
|
+
'input[name*="username" i]', 'input[id*="username" i]',
|
|
2847
|
+
], data.username, 'Username');
|
|
2848
|
+
}
|
|
2794
2849
|
await clickField([
|
|
2795
2850
|
'input[type="checkbox"][name*="agree" i]',
|
|
2796
|
-
'input[type="checkbox"][name*="tos" i]',
|
|
2797
2851
|
'input[type="checkbox"][name*="terms" i]',
|
|
2798
2852
|
'input[type="checkbox"][name*="consent" i]',
|
|
2799
|
-
'input[type="checkbox"][name*="privacy" i]',
|
|
2800
|
-
'input[type="checkbox"][name*="policy" i]',
|
|
2801
2853
|
'input[type="checkbox"][id*="agree" i]',
|
|
2802
2854
|
'input[type="checkbox"][id*="terms" i]',
|
|
2803
|
-
'input[type="checkbox"][id*="consent" i]',
|
|
2804
|
-
'input[type="checkbox"][id*="privacy" i]',
|
|
2805
|
-
'input[type="checkbox"][aria-label*="agree" i]',
|
|
2806
|
-
'input[type="checkbox"][aria-label*="terms" i]',
|
|
2807
|
-
'input[type="checkbox"][aria-label*="consent" i]',
|
|
2808
|
-
'input[type="checkbox"][aria-label*="accept" i]',
|
|
2809
2855
|
'label:has-text("agree") input[type="checkbox"]',
|
|
2810
2856
|
'label:has-text("terms") input[type="checkbox"]',
|
|
2811
2857
|
'label:has-text("accept") input[type="checkbox"]',
|
|
2812
|
-
'label:has-text("consent") input[type="checkbox"]',
|
|
2813
|
-
'label:has-text("privacy") input[type="checkbox"]',
|
|
2814
2858
|
'label:has-text("I agree") input[type="checkbox"]',
|
|
2815
|
-
'label:has-text("I accept") input[type="checkbox"]',
|
|
2816
2859
|
'[role="checkbox"][aria-checked="false"]',
|
|
2817
|
-
'div:has-text("I agree"):not(:has(div:has-text("I agree")))',
|
|
2818
|
-
'span:has-text("I agree"):not(:has(span:has-text("I agree")))',
|
|
2819
2860
|
], 'Terms/Agreement checkbox');
|
|
2820
2861
|
await p.waitForTimeout(500);
|
|
2821
2862
|
const hasCaptcha = p.frames().some(f => {
|
|
@@ -2828,7 +2869,16 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2828
2869
|
results.push('');
|
|
2829
2870
|
results.push('--- Verification step detected ---');
|
|
2830
2871
|
results.push('Attempting to complete automatically...');
|
|
2831
|
-
|
|
2872
|
+
let solveResults;
|
|
2873
|
+
try {
|
|
2874
|
+
solveResults = await Promise.race([
|
|
2875
|
+
autoSolveCaptcha(p),
|
|
2876
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('auto-solve timed out (30s)')), 30000)),
|
|
2877
|
+
]);
|
|
2878
|
+
}
|
|
2879
|
+
catch (e) {
|
|
2880
|
+
solveResults = [`Auto-solve: ${e.message}`];
|
|
2881
|
+
}
|
|
2832
2882
|
solveResults.forEach(r => results.push(` ${r}`));
|
|
2833
2883
|
const needsVision = solveResults.some(r => r.includes('VERIFICATION COMPLETION STEPS') || r.includes('REQUIRES_VISION'));
|
|
2834
2884
|
const unconfirmed = solveResults.some(r => /unconfirmed|shows an error/i.test(r));
|
|
@@ -2844,19 +2894,27 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2844
2894
|
results.push('Verification confirmed. Continuing form submission...');
|
|
2845
2895
|
}
|
|
2846
2896
|
}
|
|
2847
|
-
|
|
2848
|
-
'button[type="submit"]',
|
|
2849
|
-
'
|
|
2850
|
-
'button:has-text("
|
|
2851
|
-
'button:has-text("
|
|
2852
|
-
'button:has-text("Submit")', 'button:has-text("submit")',
|
|
2853
|
-
'button:has-text("Create")', 'button:has-text("create")',
|
|
2854
|
-
'button:has-text("Sign up")', 'button:has-text("sign up")',
|
|
2897
|
+
let clicked = await clickField([
|
|
2898
|
+
'button[type="submit"]', 'input[type="submit"]',
|
|
2899
|
+
'button:has-text("Sign Up With Email")', 'button:has-text("Sign up")',
|
|
2900
|
+
'button:has-text("Sign Up")', 'button:has-text("sign up")',
|
|
2901
|
+
'button:has-text("Create Account")', 'button:has-text("create account")',
|
|
2855
2902
|
'button:has-text("Register")', 'button:has-text("register")',
|
|
2856
|
-
'button:has-text("
|
|
2857
|
-
'
|
|
2858
|
-
'
|
|
2903
|
+
'button:has-text("Next")', 'button:has-text("Continue")',
|
|
2904
|
+
'button:has-text("Submit")', 'button:has-text("Create")',
|
|
2905
|
+
'#signup-button', '#submit-btn',
|
|
2859
2906
|
], 'Submit/Next button');
|
|
2907
|
+
if (!clicked) {
|
|
2908
|
+
const submitBtn = activeFrame.locator('button').filter({ hasText: /sign\s*up|register|submit|create|continue|next/i }).first();
|
|
2909
|
+
if (await submitBtn.count() > 0 && await submitBtn.isVisible()) {
|
|
2910
|
+
try {
|
|
2911
|
+
await submitBtn.click({ timeout: 3000 });
|
|
2912
|
+
results.push(' ✓ Submit button: clicked (regex match)');
|
|
2913
|
+
clicked = true;
|
|
2914
|
+
}
|
|
2915
|
+
catch { }
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2860
2918
|
await p.waitForTimeout(2000);
|
|
2861
2919
|
results.push('');
|
|
2862
2920
|
results.push(`--- Result ---`);
|
|
@@ -2900,6 +2958,27 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2900
2958
|
}
|
|
2901
2959
|
const results = [];
|
|
2902
2960
|
results.push('=== SIGNIN ASSIST ===');
|
|
2961
|
+
const cookieSelectors = [
|
|
2962
|
+
'button:has-text("Accept All")', 'button:has-text("Accept all")', 'button:has-text("accept all")',
|
|
2963
|
+
'button:has-text("Accept")', 'button:has-text("Accept Cookies")',
|
|
2964
|
+
'button:has-text("I agree")', 'button:has-text("Got it")', 'button:has-text("OK")',
|
|
2965
|
+
'button:has-text("Allow All")', 'button:has-text("Allow all")',
|
|
2966
|
+
'[id*="cookie"] button', '[class*="cookie"] button',
|
|
2967
|
+
'[id*="consent"] button', '[class*="consent"] button',
|
|
2968
|
+
'.cc-accept', '.cookie-accept', '#accept-cookies',
|
|
2969
|
+
];
|
|
2970
|
+
for (const sel of cookieSelectors) {
|
|
2971
|
+
try {
|
|
2972
|
+
const btn = p.locator(sel).first();
|
|
2973
|
+
if (await btn.count() > 0 && await btn.isVisible()) {
|
|
2974
|
+
await btn.click({ timeout: 2000 });
|
|
2975
|
+
results.push(` ✓ Dismissed cookie banner: ${sel}`);
|
|
2976
|
+
await p.waitForTimeout(500);
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
catch { }
|
|
2981
|
+
}
|
|
2903
2982
|
const allFrames = [p, ...p.frames().filter(f => f !== p.mainFrame())];
|
|
2904
2983
|
let activeFrame = p;
|
|
2905
2984
|
for (const frame of allFrames) {
|
|
@@ -2921,11 +3000,11 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2921
3000
|
return true;
|
|
2922
3001
|
}
|
|
2923
3002
|
try {
|
|
2924
|
-
await loc.fill(val, { timeout:
|
|
3003
|
+
await loc.fill(val, { timeout: 1500 });
|
|
2925
3004
|
}
|
|
2926
3005
|
catch {
|
|
2927
|
-
await loc.click({ timeout:
|
|
2928
|
-
await loc.pressSequentially(val, { delay: 30, timeout:
|
|
3006
|
+
await loc.click({ timeout: 1500 });
|
|
3007
|
+
await loc.pressSequentially(val, { delay: 30, timeout: 5000 });
|
|
2929
3008
|
}
|
|
2930
3009
|
results.push(` ✓ ${label}: filled`);
|
|
2931
3010
|
return true;
|
|
@@ -2945,7 +3024,7 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2945
3024
|
results.push(` ✓ ${label}: already checked`);
|
|
2946
3025
|
return true;
|
|
2947
3026
|
}
|
|
2948
|
-
await loc.click({ timeout:
|
|
3027
|
+
await loc.click({ timeout: 1500 });
|
|
2949
3028
|
results.push(` ✓ ${label}: clicked`);
|
|
2950
3029
|
return true;
|
|
2951
3030
|
}
|
|
@@ -2994,7 +3073,16 @@ Sessions: session="a"/"b"/"c" for parallel browsers. proxy="host:port:user:pass"
|
|
|
2994
3073
|
results.push('');
|
|
2995
3074
|
results.push('--- Verification step detected ---');
|
|
2996
3075
|
results.push('Attempting to complete automatically...');
|
|
2997
|
-
|
|
3076
|
+
let solveResults;
|
|
3077
|
+
try {
|
|
3078
|
+
solveResults = await Promise.race([
|
|
3079
|
+
autoSolveCaptcha(p),
|
|
3080
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('auto-solve timed out (30s)')), 30000)),
|
|
3081
|
+
]);
|
|
3082
|
+
}
|
|
3083
|
+
catch (e) {
|
|
3084
|
+
solveResults = [`Auto-solve: ${e.message}`];
|
|
3085
|
+
}
|
|
2998
3086
|
solveResults.forEach(r => results.push(` ${r}`));
|
|
2999
3087
|
const needsVision = solveResults.some(r => r.includes('VERIFICATION COMPLETION STEPS') || r.includes('REQUIRES_VISION'));
|
|
3000
3088
|
const unconfirmed = solveResults.some(r => /unconfirmed|shows an error/i.test(r));
|