app-manager-edge-worker 1.2.5 → 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 +14 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +4 -1
- package/src/handlers/interactiveHandler.js +207 -65
package/README.md
CHANGED
|
@@ -201,6 +201,13 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
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
|
+
|
|
204
211
|
### v1.2.5
|
|
205
212
|
- **Interactive Session Visual Inspector Sync & Robust Captcha Refresh**:
|
|
206
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.
|
|
@@ -256,6 +263,13 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
256
263
|
- Automatic backup of pre-existing local disk sessions to cluster database on initial connection.
|
|
257
264
|
- Coordinated session wiping on remote logout and phone unlinking.
|
|
258
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
|
+
|
|
259
273
|
### v1.1.0
|
|
260
274
|
- **WhatsApp Multi-Device Companion Relay**:
|
|
261
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") {
|
|
@@ -422,7 +422,24 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
422
422
|
const inputSelector = cfg.inputSelector || "#captcha, input[name='captcha']";
|
|
423
423
|
const submitSelector = cfg.submitSelector || "button[type='submit']";
|
|
424
424
|
const successSelector = cfg.waitForAfterSubmit?.selector || ".table-responsive, table, .result-pane";
|
|
425
|
-
|
|
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
|
+
|
|
426
443
|
const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
|
|
427
444
|
|
|
428
445
|
// 1. Type captcha solution
|
|
@@ -436,17 +453,32 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
436
453
|
await page.click(submitSelector);
|
|
437
454
|
|
|
438
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
|
+
|
|
439
461
|
const matchedOutcome = await page
|
|
440
462
|
.waitForFunction(
|
|
441
463
|
(succ, err) => {
|
|
442
464
|
if (document.querySelector(succ)) return "SUCCESS";
|
|
443
|
-
|
|
444
|
-
|
|
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) {}
|
|
445
477
|
return null;
|
|
446
478
|
},
|
|
447
|
-
{ timeout:
|
|
479
|
+
{ timeout: safetyTimeoutMs },
|
|
448
480
|
successSelector,
|
|
449
|
-
|
|
481
|
+
combinedErrorSelectors
|
|
450
482
|
)
|
|
451
483
|
.then((handle) => handle.jsonValue())
|
|
452
484
|
.catch(() => "TIMEOUT");
|
|
@@ -482,28 +514,12 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
482
514
|
// Settle time for tables & styles
|
|
483
515
|
await new Promise((r) => setTimeout(r, 600));
|
|
484
516
|
|
|
485
|
-
const
|
|
486
|
-
encoding: "binary",
|
|
487
|
-
fullPage: false,
|
|
488
|
-
type: "jpeg",
|
|
489
|
-
quality: 85,
|
|
490
|
-
});
|
|
491
|
-
const screenshotBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
492
|
-
const title = await page.title().catch(() => "");
|
|
493
|
-
const dimensions = await page
|
|
494
|
-
.evaluate(() => ({
|
|
495
|
-
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
496
|
-
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
497
|
-
}))
|
|
498
|
-
.catch(() => ({ width: 1366, height: 900 }));
|
|
499
|
-
|
|
517
|
+
const snap = await captureInspectionSnapshot(page);
|
|
500
518
|
return {
|
|
501
519
|
status: "COMPLETED",
|
|
502
520
|
success: true,
|
|
503
521
|
data: raw,
|
|
504
|
-
|
|
505
|
-
dimensions,
|
|
506
|
-
title: title || "Results Page",
|
|
522
|
+
...snap,
|
|
507
523
|
sessionId,
|
|
508
524
|
isInspectionMode: true,
|
|
509
525
|
};
|
|
@@ -519,55 +535,129 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
519
535
|
};
|
|
520
536
|
}
|
|
521
537
|
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
}
|
|
528
568
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
}
|
|
531
587
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
session.timeoutTimer = setTimeout(() => {
|
|
537
|
-
purgeSession(sessionId).catch(() => {});
|
|
538
|
-
}, retryTtlSeconds * 1000);
|
|
588
|
+
const regex = new RegExp(invalidPattern, "i");
|
|
589
|
+
const isCaptchaError = matchedRule
|
|
590
|
+
? matchedRule.action === "RETRY_CAPTCHA"
|
|
591
|
+
: (Boolean(errorText) && regex.test(errorText));
|
|
539
592
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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);
|
|
545
612
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
613
|
+
const snap = await captureInspectionSnapshot(page);
|
|
614
|
+
const effectiveStatus = isCaptchaError
|
|
615
|
+
? "INVALID_CAPTCHA"
|
|
616
|
+
: (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
|
|
550
617
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
};
|
|
568
647
|
}
|
|
569
648
|
|
|
570
|
-
|
|
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
|
|
571
661
|
await purgeSession(sessionId);
|
|
572
662
|
return {
|
|
573
663
|
status: "TIMEOUT",
|
|
@@ -577,6 +667,58 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
577
667
|
};
|
|
578
668
|
}
|
|
579
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
|
+
|
|
580
722
|
/**
|
|
581
723
|
* 4. Handles HITL_SESSION_ABORT:
|
|
582
724
|
* Explicit client abort or session abandonment.
|