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 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.2.5",
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
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "module";
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
- let workerVersion = "1.2.5";
4
+ let workerVersion = "1.3.0";
5
5
  try {
6
6
  const pkg = require("../package.json");
7
7
  if (pkg?.version) workerVersion = pkg.version;
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.2.5";
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
- const errorSelector = cfg.errorDetection?.selector || ".alert-danger, .has-error, #error-msg, .ui-messages-error";
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
- const errEl = document.querySelector(err);
444
- if (errEl && errEl.innerText && errEl.innerText.trim().length > 0) return "ERROR";
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: cfg.waitForAfterSubmit?.timeoutMs || 15000 },
479
+ { timeout: safetyTimeoutMs },
448
480
  successSelector,
449
- errorSelector
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 screenshotBuffer = await page.screenshot({
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
- screenshotBase64: `data:image/jpeg;base64,${screenshotBase64}`,
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
- // Case B: In-Page Error Banner Detected
523
- if (matchedOutcome === "ERROR") {
524
- const errorText = await page.evaluate((sel) => {
525
- const el = document.querySelector(sel);
526
- return el ? el.innerText.trim() : "";
527
- }, errorSelector);
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
- const regex = new RegExp(invalidPattern, "i");
530
- const isCaptchaError = regex.test(errorText);
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
- if (isCaptchaError) {
533
- // Keep page alive! Reset TTL timer for retry
534
- if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
535
- const retryTtlSeconds = cfg.sessionTimeoutSeconds || 90;
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
- // Give the target portal 400ms to re-render the refreshed captcha image
541
- await new Promise((r) => setTimeout(r, 400));
542
- const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
543
- const newCaptchaEl = await page.$(imageSelector);
544
- let newImageBase64 = null;
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
- if (newCaptchaEl) {
547
- const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
548
- newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
549
- }
613
+ const snap = await captureInspectionSnapshot(page);
614
+ const effectiveStatus = isCaptchaError
615
+ ? "INVALID_CAPTCHA"
616
+ : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
550
617
 
551
- return {
552
- status: "INVALID_CAPTCHA",
553
- success: false,
554
- message: errorText || "The captcha entered was incorrect.",
555
- canRetry: true,
556
- newImageBase64,
557
- };
558
- } else {
559
- // Business error (e.g. "No record found for this number")
560
- await purgeSession(sessionId);
561
- return {
562
- status: "BUSINESS_ERROR",
563
- success: false,
564
- message: errorText || "Target portal returned an error.",
565
- canRetry: false,
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
- // Case C: Timeout
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.