app-manager-edge-worker 1.2.4 → 1.3.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/README.md +19 -1
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +4 -1
- package/src/handlers/interactiveHandler.js +275 -72
package/README.md
CHANGED
|
@@ -201,7 +201,18 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
### v1.3.0
|
|
205
|
+
- **Multi-Error Identifier Rules & Inspection Resilience Engine**:
|
|
206
|
+
- **Custom Error Identifier Rules**: Supported typed error detection rules (`errorDetection.rules`) allowing classification of multiple distinct errors (e.g. `INVALID_GSTIN`, `INVALID_CAPTCHA`, `NOT_FOUND`) with tailored actions (`RETRY_CAPTCHA`, `BUSINESS_ERROR`, `ABORT_SESSION`).
|
|
207
|
+
- **Inspection Mode Zero-Purge Resilience**: When `preservePageForInspection: true`, the worker never terminates the session on errors or timeouts. Instead, it settles the DOM, takes a full-page snapshot, and returns the live updated screenshot to the visual element inspector so users can inspect red error messages and map error selectors.
|
|
208
|
+
- **Broadened Intelligent Error Heuristics**: Added automatic recognition for standard Indian government and SPA error selectors (`p#alertMsg`, `#alertMsg`, `.alertMsg`, `.text-danger`, `.invalid-feedback`, `[aria-invalid='true']`), resolving outcome races in sub-second time without 15s timeouts.
|
|
209
|
+
- **Added `HITL_SESSION_SNAPSHOT`**: On-demand snapshot handler allowing the visual inspector to capture and synchronize current browser page state at any point.
|
|
210
|
+
|
|
211
|
+
### v1.2.5
|
|
212
|
+
- **Interactive Session Visual Inspector Sync & Robust Captcha Refresh**:
|
|
213
|
+
- `HITL_SESSION_INIT` now captures and returns full-page `pageScreenshotBase64`, dimensions, and title after typing initial form inputs, keeping the Visual Element Inspector synchronized with the active session.
|
|
214
|
+
- Enhanced `HITL_SESSION_REFRESH` with multi-selector fallback candidates (`button[ng-click*="refreshCaptcha"]`, `button[ng-click*="captcha"]`, `#reloadCaptcha`, etc.) ensuring reliable refresh on AngularJS and custom portal architectures.
|
|
215
|
+
- Captures and returns updated `pageScreenshotBase64` upon captcha refresh to keep inspect canvas up to date.
|
|
205
216
|
|
|
206
217
|
### v1.2.4
|
|
207
218
|
- **Preserved Live Inspection & Field Extraction on Results Pages**:
|
|
@@ -252,6 +263,13 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
252
263
|
- Automatic backup of pre-existing local disk sessions to cluster database on initial connection.
|
|
253
264
|
- Coordinated session wiping on remote logout and phone unlinking.
|
|
254
265
|
|
|
266
|
+
### v1.2.5
|
|
267
|
+
- **Interactive Human-In-The-Loop (HITL) Web Scraping**:
|
|
268
|
+
- Pinned stateful browser sessions for interactive portals (GSTIN, PAN, Parivahan, e-Courts).
|
|
269
|
+
- Multi-step execution protocol: `HITL_SESSION_INIT`, `HITL_SESSION_REFRESH`, `HITL_SESSION_SOLVE`, `HITL_SESSION_EXTRACT`, and `HITL_SESSION_ABORT`.
|
|
270
|
+
- Memory-safe binary captcha streaming and bounded 90s session TTL auto-termination.
|
|
271
|
+
- Server URL resolution propagation for Baileys companion document relay.
|
|
272
|
+
|
|
255
273
|
### v1.1.0
|
|
256
274
|
- **WhatsApp Multi-Device Companion Relay**:
|
|
257
275
|
- Independent companion device pairing via live QR streaming to Admin Dashboard.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/banner.js
CHANGED
package/src/client.js
CHANGED
|
@@ -14,12 +14,13 @@ import {
|
|
|
14
14
|
handleInteractiveSessionSolve,
|
|
15
15
|
handleInteractiveSessionAbort,
|
|
16
16
|
handleInteractiveSessionExtract,
|
|
17
|
+
handleInteractiveSessionSnapshot,
|
|
17
18
|
} from "./handlers/interactiveHandler.js";
|
|
18
19
|
import fs from "fs";
|
|
19
20
|
import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
|
|
20
21
|
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
21
22
|
|
|
22
|
-
let workerPkgVersion = "1.
|
|
23
|
+
let workerPkgVersion = "1.3.0";
|
|
23
24
|
try {
|
|
24
25
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
25
26
|
if (pkg?.version) workerPkgVersion = pkg.version;
|
|
@@ -263,6 +264,8 @@ export async function startWorkerClient(config) {
|
|
|
263
264
|
resultData = await handleInteractiveSessionRefresh(payload);
|
|
264
265
|
} else if (jobType === "HITL_SESSION_SOLVE") {
|
|
265
266
|
resultData = await handleInteractiveSessionSolve(payload);
|
|
267
|
+
} else if (jobType === "HITL_SESSION_SNAPSHOT") {
|
|
268
|
+
resultData = await handleInteractiveSessionSnapshot(payload);
|
|
266
269
|
} else if (jobType === "HITL_SESSION_EXTRACT") {
|
|
267
270
|
resultData = await handleInteractiveSessionExtract(payload);
|
|
268
271
|
} else if (jobType === "HITL_SESSION_ABORT") {
|
|
@@ -223,6 +223,25 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
223
223
|
});
|
|
224
224
|
const imageBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
225
225
|
|
|
226
|
+
// Settle briefly for form state & portal validation UI
|
|
227
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
228
|
+
|
|
229
|
+
// Capture full-page screenshot showing the filled inputs and revealed captcha
|
|
230
|
+
const pageScreenshotBuffer = await page.screenshot({
|
|
231
|
+
encoding: "binary",
|
|
232
|
+
fullPage: false,
|
|
233
|
+
type: "jpeg",
|
|
234
|
+
quality: 85,
|
|
235
|
+
});
|
|
236
|
+
const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
|
|
237
|
+
const title = (await page.title().catch(() => "")) || "Interactive Portal Session";
|
|
238
|
+
const dimensions = await page
|
|
239
|
+
.evaluate(() => ({
|
|
240
|
+
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
241
|
+
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
242
|
+
}))
|
|
243
|
+
.catch(() => ({ width: 1366, height: 900 }));
|
|
244
|
+
|
|
226
245
|
const sessionTtlSeconds = captchaConfig.sessionTimeoutSeconds || 90;
|
|
227
246
|
|
|
228
247
|
// Set auto-purge timer to prevent resource leaks
|
|
@@ -244,6 +263,9 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
244
263
|
return {
|
|
245
264
|
sessionId,
|
|
246
265
|
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
266
|
+
pageScreenshotBase64,
|
|
267
|
+
dimensions,
|
|
268
|
+
title,
|
|
247
269
|
expiresInSeconds: sessionTtlSeconds,
|
|
248
270
|
};
|
|
249
271
|
} catch (err) {
|
|
@@ -269,10 +291,6 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
269
291
|
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
270
292
|
const refreshSelector = cfg.refreshSelector;
|
|
271
293
|
|
|
272
|
-
if (!refreshSelector) {
|
|
273
|
-
throw new Error("refreshSelector is not configured for this captcha");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
294
|
// 1. Snapshot current image hash and src attribute
|
|
277
295
|
const oldEl = await page.$(imageSelector);
|
|
278
296
|
if (!oldEl) {
|
|
@@ -287,9 +305,41 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
287
305
|
return el ? el.getAttribute("src") : null;
|
|
288
306
|
}, imageSelector);
|
|
289
307
|
|
|
290
|
-
// 2. Click the reload button
|
|
291
|
-
|
|
292
|
-
|
|
308
|
+
// 2. Click the reload button with robust candidate fallbacks
|
|
309
|
+
const candidateSelectors = [
|
|
310
|
+
refreshSelector,
|
|
311
|
+
"button[ng-click*='refreshCaptcha' i]",
|
|
312
|
+
"button[ng-click*='captcha' i]",
|
|
313
|
+
"*[ng-click*='refreshCaptcha' i]",
|
|
314
|
+
"*[ng-click*='captcha' i]",
|
|
315
|
+
"#reloadCaptcha",
|
|
316
|
+
"#refreshCaptcha",
|
|
317
|
+
"a[title*='refresh' i]",
|
|
318
|
+
"button[title*='refresh' i]",
|
|
319
|
+
"a[title*='reload' i]",
|
|
320
|
+
"button[title*='reload' i]",
|
|
321
|
+
".captcha-refresh",
|
|
322
|
+
".reload-captcha",
|
|
323
|
+
".glyphicon-refresh",
|
|
324
|
+
"i.fa-refresh",
|
|
325
|
+
"i.fa-arrows-rotate",
|
|
326
|
+
].filter(Boolean);
|
|
327
|
+
|
|
328
|
+
let clicked = false;
|
|
329
|
+
for (const sel of candidateSelectors) {
|
|
330
|
+
try {
|
|
331
|
+
const btn = await page.$(sel);
|
|
332
|
+
if (btn) {
|
|
333
|
+
await btn.click();
|
|
334
|
+
clicked = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
} catch (_) {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!clicked) {
|
|
341
|
+
throw new Error(`Captcha refresh button not found on page (tried: ${refreshSelector || "standard reload selectors"})`);
|
|
342
|
+
}
|
|
293
343
|
|
|
294
344
|
// 3. Wait for src attribute change first
|
|
295
345
|
await page
|
|
@@ -326,9 +376,20 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
326
376
|
}
|
|
327
377
|
|
|
328
378
|
const imageBase64 = Buffer.from(newBuffer).toString("base64");
|
|
379
|
+
|
|
380
|
+
// Capture updated full page screenshot showing new captcha
|
|
381
|
+
const pageScreenshotBuffer = await page.screenshot({
|
|
382
|
+
encoding: "binary",
|
|
383
|
+
fullPage: false,
|
|
384
|
+
type: "jpeg",
|
|
385
|
+
quality: 85,
|
|
386
|
+
});
|
|
387
|
+
const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
|
|
388
|
+
|
|
329
389
|
return {
|
|
330
390
|
sessionId,
|
|
331
391
|
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
392
|
+
pageScreenshotBase64,
|
|
332
393
|
};
|
|
333
394
|
}
|
|
334
395
|
|
|
@@ -361,7 +422,24 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
361
422
|
const inputSelector = cfg.inputSelector || "#captcha, input[name='captcha']";
|
|
362
423
|
const submitSelector = cfg.submitSelector || "button[type='submit']";
|
|
363
424
|
const successSelector = cfg.waitForAfterSubmit?.selector || ".table-responsive, table, .result-pane";
|
|
364
|
-
|
|
425
|
+
|
|
426
|
+
// Broad default error selectors covering Indian gov and modern SPA patterns
|
|
427
|
+
const defaultBroadErrorSelector =
|
|
428
|
+
"p#alertMsg, #alertMsg, .alertMsg, .alert-danger, .alert-warning, " +
|
|
429
|
+
".has-error, .text-danger, #error-msg, .error-message, .err-msg, " +
|
|
430
|
+
".ui-messages-error, [class*='error-msg'], [class*='alert-message'], " +
|
|
431
|
+
".invalid-feedback, .help-block.error, .form-error, [aria-invalid='true']";
|
|
432
|
+
|
|
433
|
+
const customRules = cfg.errorDetection?.rules || [];
|
|
434
|
+
const ruleSelectors = customRules.map((r) => r.selector).filter(Boolean);
|
|
435
|
+
const combinedErrorSelectors = [
|
|
436
|
+
cfg.errorDetection?.selector,
|
|
437
|
+
...ruleSelectors,
|
|
438
|
+
defaultBroadErrorSelector,
|
|
439
|
+
]
|
|
440
|
+
.filter(Boolean)
|
|
441
|
+
.join(", ");
|
|
442
|
+
|
|
365
443
|
const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
|
|
366
444
|
|
|
367
445
|
// 1. Type captcha solution
|
|
@@ -375,17 +453,32 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
375
453
|
await page.click(submitSelector);
|
|
376
454
|
|
|
377
455
|
// 3. Post-Submit Race: Wait for Success Selector OR Error Selector
|
|
456
|
+
// In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
|
|
457
|
+
const safetyTimeoutMs = preservePageForInspection
|
|
458
|
+
? Math.min(cfg.waitForAfterSubmit?.timeoutMs || 8000, 6000)
|
|
459
|
+
: (cfg.waitForAfterSubmit?.timeoutMs || 15000);
|
|
460
|
+
|
|
378
461
|
const matchedOutcome = await page
|
|
379
462
|
.waitForFunction(
|
|
380
463
|
(succ, err) => {
|
|
381
464
|
if (document.querySelector(succ)) return "SUCCESS";
|
|
382
|
-
|
|
383
|
-
|
|
465
|
+
try {
|
|
466
|
+
const elements = document.querySelectorAll(err);
|
|
467
|
+
for (const el of elements) {
|
|
468
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
469
|
+
if (txt.length > 0) {
|
|
470
|
+
const style = window.getComputedStyle(el);
|
|
471
|
+
if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
|
|
472
|
+
return "ERROR";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (e) {}
|
|
384
477
|
return null;
|
|
385
478
|
},
|
|
386
|
-
{ timeout:
|
|
479
|
+
{ timeout: safetyTimeoutMs },
|
|
387
480
|
successSelector,
|
|
388
|
-
|
|
481
|
+
combinedErrorSelectors
|
|
389
482
|
)
|
|
390
483
|
.then((handle) => handle.jsonValue())
|
|
391
484
|
.catch(() => "TIMEOUT");
|
|
@@ -421,28 +514,12 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
421
514
|
// Settle time for tables & styles
|
|
422
515
|
await new Promise((r) => setTimeout(r, 600));
|
|
423
516
|
|
|
424
|
-
const
|
|
425
|
-
encoding: "binary",
|
|
426
|
-
fullPage: false,
|
|
427
|
-
type: "jpeg",
|
|
428
|
-
quality: 85,
|
|
429
|
-
});
|
|
430
|
-
const screenshotBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
431
|
-
const title = await page.title().catch(() => "");
|
|
432
|
-
const dimensions = await page
|
|
433
|
-
.evaluate(() => ({
|
|
434
|
-
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
435
|
-
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
436
|
-
}))
|
|
437
|
-
.catch(() => ({ width: 1366, height: 900 }));
|
|
438
|
-
|
|
517
|
+
const snap = await captureInspectionSnapshot(page);
|
|
439
518
|
return {
|
|
440
519
|
status: "COMPLETED",
|
|
441
520
|
success: true,
|
|
442
521
|
data: raw,
|
|
443
|
-
|
|
444
|
-
dimensions,
|
|
445
|
-
title: title || "Results Page",
|
|
522
|
+
...snap,
|
|
446
523
|
sessionId,
|
|
447
524
|
isInspectionMode: true,
|
|
448
525
|
};
|
|
@@ -458,55 +535,129 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
458
535
|
};
|
|
459
536
|
}
|
|
460
537
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
538
|
+
// Evaluate error rules and messages if matchedOutcome === "ERROR" or if we are inspecting
|
|
539
|
+
let matchedRule = null;
|
|
540
|
+
let errorText = "";
|
|
541
|
+
|
|
542
|
+
// 1. Check custom configured rules first
|
|
543
|
+
for (const rule of customRules) {
|
|
544
|
+
if (!rule.selector) continue;
|
|
545
|
+
const check = await page.evaluate((sel, pat) => {
|
|
546
|
+
try {
|
|
547
|
+
const els = document.querySelectorAll(sel);
|
|
548
|
+
for (const el of els) {
|
|
549
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
550
|
+
if (txt) {
|
|
551
|
+
const style = window.getComputedStyle(el);
|
|
552
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
|
|
553
|
+
if (!pat) return { matched: true, text: txt };
|
|
554
|
+
const re = new RegExp(pat, "i");
|
|
555
|
+
if (re.test(txt)) return { matched: true, text: txt };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch (e) {}
|
|
559
|
+
return null;
|
|
560
|
+
}, rule.selector, rule.pattern);
|
|
561
|
+
|
|
562
|
+
if (check && check.matched) {
|
|
563
|
+
matchedRule = rule;
|
|
564
|
+
errorText = check.text;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
467
568
|
|
|
468
|
-
|
|
469
|
-
|
|
569
|
+
// 2. If no custom rule matched, extract text from combined error selectors
|
|
570
|
+
if (!matchedRule) {
|
|
571
|
+
errorText = await page.evaluate((sel) => {
|
|
572
|
+
try {
|
|
573
|
+
const els = document.querySelectorAll(sel);
|
|
574
|
+
for (const el of els) {
|
|
575
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
576
|
+
if (txt) {
|
|
577
|
+
const style = window.getComputedStyle(el);
|
|
578
|
+
if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
|
|
579
|
+
return txt;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (e) {}
|
|
584
|
+
return "";
|
|
585
|
+
}, combinedErrorSelectors);
|
|
586
|
+
}
|
|
470
587
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
session.timeoutTimer = setTimeout(() => {
|
|
476
|
-
purgeSession(sessionId).catch(() => {});
|
|
477
|
-
}, retryTtlSeconds * 1000);
|
|
588
|
+
const regex = new RegExp(invalidPattern, "i");
|
|
589
|
+
const isCaptchaError = matchedRule
|
|
590
|
+
? matchedRule.action === "RETRY_CAPTCHA"
|
|
591
|
+
: (Boolean(errorText) && regex.test(errorText));
|
|
478
592
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
593
|
+
// Check if new captcha image was rendered on page
|
|
594
|
+
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
595
|
+
let newImageBase64 = null;
|
|
596
|
+
try {
|
|
597
|
+
const newCaptchaEl = await page.$(imageSelector);
|
|
598
|
+
if (newCaptchaEl) {
|
|
599
|
+
const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
|
|
600
|
+
newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {}
|
|
603
|
+
|
|
604
|
+
// CRITICAL FOR INSPECTOR: In inspection mode, NEVER purge the session on errors or timeout!
|
|
605
|
+
// Always snap the live DOM and return it so the user can inspect the red text and configure selectors
|
|
606
|
+
if (preservePageForInspection) {
|
|
607
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
608
|
+
const inspectTtl = 300;
|
|
609
|
+
session.timeoutTimer = setTimeout(() => {
|
|
610
|
+
purgeSession(sessionId).catch(() => {});
|
|
611
|
+
}, inspectTtl * 1000);
|
|
484
612
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
613
|
+
const snap = await captureInspectionSnapshot(page);
|
|
614
|
+
const effectiveStatus = isCaptchaError
|
|
615
|
+
? "INVALID_CAPTCHA"
|
|
616
|
+
: (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
|
|
489
617
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
618
|
+
return {
|
|
619
|
+
status: effectiveStatus,
|
|
620
|
+
success: true, // Allows frontend to render the inspection screenshot
|
|
621
|
+
errorCode: (matchedRule && matchedRule.id) || (isCaptchaError ? "INVALID_CAPTCHA" : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE")),
|
|
622
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || (matchedOutcome === "TIMEOUT" ? "Page updated (no success table found). You can now inspect elements or error messages." : "Page outcome revealed for inspection."),
|
|
623
|
+
newImageBase64,
|
|
624
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
625
|
+
sessionId,
|
|
626
|
+
isInspectionMode: true,
|
|
627
|
+
...snap,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Runtime non-inspection mode:
|
|
632
|
+
if (isCaptchaError) {
|
|
633
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
634
|
+
const retryTtlSeconds = cfg.sessionTimeoutSeconds || 90;
|
|
635
|
+
session.timeoutTimer = setTimeout(() => {
|
|
636
|
+
purgeSession(sessionId).catch(() => {});
|
|
637
|
+
}, retryTtlSeconds * 1000);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
status: "INVALID_CAPTCHA",
|
|
641
|
+
success: false,
|
|
642
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
|
|
643
|
+
canRetry: true,
|
|
644
|
+
newImageBase64,
|
|
645
|
+
errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
|
|
646
|
+
};
|
|
507
647
|
}
|
|
508
648
|
|
|
509
|
-
|
|
649
|
+
if (errorText || matchedRule) {
|
|
650
|
+
await purgeSession(sessionId);
|
|
651
|
+
return {
|
|
652
|
+
status: "BUSINESS_ERROR",
|
|
653
|
+
success: false,
|
|
654
|
+
errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
|
|
655
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
|
|
656
|
+
canRetry: false,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Runtime Timeout
|
|
510
661
|
await purgeSession(sessionId);
|
|
511
662
|
return {
|
|
512
663
|
status: "TIMEOUT",
|
|
@@ -516,6 +667,58 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
516
667
|
};
|
|
517
668
|
}
|
|
518
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Capture full inspection snapshot helper
|
|
672
|
+
*/
|
|
673
|
+
async function captureInspectionSnapshot(page) {
|
|
674
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
675
|
+
const screenshotBuffer = await page.screenshot({
|
|
676
|
+
encoding: "binary",
|
|
677
|
+
fullPage: false,
|
|
678
|
+
type: "jpeg",
|
|
679
|
+
quality: 85,
|
|
680
|
+
});
|
|
681
|
+
const screenshotBase64 = `data:image/jpeg;base64,${Buffer.from(screenshotBuffer).toString("base64")}`;
|
|
682
|
+
const title = await page.title().catch(() => "");
|
|
683
|
+
const dimensions = await page
|
|
684
|
+
.evaluate(() => ({
|
|
685
|
+
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
686
|
+
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
687
|
+
}))
|
|
688
|
+
.catch(() => ({ width: 1366, height: 900 }));
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
screenshotBase64,
|
|
692
|
+
dimensions,
|
|
693
|
+
title: title || "Inspection Page",
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Force capture snapshot of the active interactive session's page
|
|
699
|
+
*/
|
|
700
|
+
export async function handleInteractiveSessionSnapshot(payload) {
|
|
701
|
+
const { sessionId } = payload;
|
|
702
|
+
const session = activeSessions.get(sessionId);
|
|
703
|
+
|
|
704
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
705
|
+
throw new Error("Interactive session not found or already expired");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const { page } = session;
|
|
709
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
710
|
+
session.timeoutTimer = setTimeout(() => {
|
|
711
|
+
purgeSession(sessionId).catch(() => {});
|
|
712
|
+
}, 300 * 1000);
|
|
713
|
+
|
|
714
|
+
const snap = await captureInspectionSnapshot(page);
|
|
715
|
+
return {
|
|
716
|
+
success: true,
|
|
717
|
+
sessionId,
|
|
718
|
+
...snap,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
519
722
|
/**
|
|
520
723
|
* 4. Handles HITL_SESSION_ABORT:
|
|
521
724
|
* Explicit client abort or session abandonment.
|