app-manager-edge-worker 1.3.2 → 1.3.3
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 +8 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +1 -1
- package/src/handlers/interactiveHandler.js +78 -7
package/README.md
CHANGED
|
@@ -201,6 +201,14 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
+
### v1.3.3
|
|
205
|
+
- **Captcha Session Preservation & Asynchronous Image Resynchronization**:
|
|
206
|
+
- **Deterministic Error Differentiation**: Disentangled captcha validation rejections from business errors (e.g., invalid GSTIN). When captcha validation fails, the worker and backend preserve the browser page and interactive session alive, reset the session TTL timer, and never abort prematurely.
|
|
207
|
+
- **Expanded Captcha Failure Signatures**: Added broad pattern matching (`letters?`, `characters?`, `image below`, `shown below`, `security/verification code`, `invalid_captcha`) and DOM attribute inspection (`input[name='cap'].ng-invalid`, `[data-ng-bind*='ERR_CAPTCHA']`) to immediately classify captcha errors even when portals omit the word "captcha".
|
|
208
|
+
- **Asynchronous Captcha Settle Logic**: Introduced post-rejection settle delays (`waitForFunction` monitoring `img.src` change and AJAX completion) before taking the replacement captcha screenshot, preventing stale/expired captcha image captures.
|
|
209
|
+
- **Session TTL Auto-Refresh**: Automatically resets the session expiration timer on both edge worker and central backend whenever a captcha is retried or refreshed, granting users the full interaction window.
|
|
210
|
+
- **Clean Session Termination on Business Errors**: Genuine business rejections (such as invalid GSTIN or record not found) immediately purge browser contexts and trigger clean state cleanup across worker, backend, and UI test playground.
|
|
211
|
+
|
|
204
212
|
### v1.3.2
|
|
205
213
|
- **Sub-Second Inline Error Resolution & Strict Outcome Race Protocol**:
|
|
206
214
|
- **Dynamic In-Page Error Detection**: Expanded `defaultBroadErrorSelector` to capture generic inline portal validation elements (`.err`, `span.err`, `p.err`, `div.err`, `[class*='err-']`, `[data-ng-bind*='ERR']`, `[data-ng-show*='err']`, `[data-ng-show*='w_gst']`, `[role='alert']`), eliminating 15-second hangs when portal rejections insert error lines without page reload.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
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
|
@@ -20,7 +20,7 @@ import fs from "fs";
|
|
|
20
20
|
import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
|
|
21
21
|
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
22
22
|
|
|
23
|
-
let workerPkgVersion = "1.3.
|
|
23
|
+
let workerPkgVersion = "1.3.3";
|
|
24
24
|
try {
|
|
25
25
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
26
26
|
if (pkg?.version) workerPkgVersion = pkg.version;
|
|
@@ -529,6 +529,13 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
529
529
|
});
|
|
530
530
|
const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
|
|
531
531
|
|
|
532
|
+
// Reset auto-purge TTL timer to grant user full window to solve refreshed captcha
|
|
533
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
534
|
+
const sessionTtlSeconds = cfg.sessionTimeoutSeconds || 180;
|
|
535
|
+
session.timeoutTimer = setTimeout(() => {
|
|
536
|
+
purgeSession(sessionId).catch(() => { });
|
|
537
|
+
}, sessionTtlSeconds * 1000);
|
|
538
|
+
|
|
532
539
|
return {
|
|
533
540
|
sessionId,
|
|
534
541
|
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
@@ -587,7 +594,19 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
587
594
|
.filter(Boolean)
|
|
588
595
|
.join(", ");
|
|
589
596
|
|
|
590
|
-
const
|
|
597
|
+
const defaultCaptchaPattern =
|
|
598
|
+
"captcha.*(incorrect|invalid|wrong|mismatch|enter|letters?|text|try again)|" +
|
|
599
|
+
"(invalid|incorrect|wrong|mismatch).*captcha|" +
|
|
600
|
+
"enter\\s*valid\\s*(letters?|characters?|code)|" +
|
|
601
|
+
"letters?\\s*shown\\s*(below|in\\s*the\\s*image)|" +
|
|
602
|
+
"characters?\\s*shown|" +
|
|
603
|
+
"invalid_captcha|" +
|
|
604
|
+
"verification\\s*code.*(invalid|wrong|incorrect)|" +
|
|
605
|
+
"security\\s*code.*(invalid|wrong|incorrect)";
|
|
606
|
+
|
|
607
|
+
const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern
|
|
608
|
+
? `${cfg.errorDetection.invalidCaptchaPattern}|${defaultCaptchaPattern}`
|
|
609
|
+
: defaultCaptchaPattern;
|
|
591
610
|
|
|
592
611
|
// 1. Type captcha solution with robust candidate fallbacks & frame search
|
|
593
612
|
const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
|
|
@@ -880,9 +899,38 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
880
899
|
}
|
|
881
900
|
}
|
|
882
901
|
|
|
902
|
+
// 5. DOM-level captcha error detection: check if captcha input or form has invalid captcha state
|
|
903
|
+
let hasDomCaptchaError = false;
|
|
904
|
+
try {
|
|
905
|
+
const capInput = document.querySelector("input[name='cap'], input[id*='captcha' i], input[name*='captcha' i], #fo-captcha");
|
|
906
|
+
if (capInput) {
|
|
907
|
+
const cls = capInput.className || "";
|
|
908
|
+
if (cls.includes("invalid_captcha") || cls.includes("invalid-captcha") || cls.includes("ng-invalid-invalid_captcha")) {
|
|
909
|
+
hasDomCaptchaError = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const form = document.querySelector("form");
|
|
913
|
+
if (form && !hasDomCaptchaError) {
|
|
914
|
+
const formCls = form.className || "";
|
|
915
|
+
if (formCls.includes("invalid_captcha") || formCls.includes("invalid-captcha") || formCls.includes("ng-invalid-invalid_captcha")) {
|
|
916
|
+
hasDomCaptchaError = true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!hasDomCaptchaError) {
|
|
920
|
+
const capErrSpan = document.querySelector("[data-ng-bind*='ERR_CAPTCHA'], [ng-bind*='ERR_CAPTCHA'], [data-ng-if*='invalid_captcha'], [ng-if*='invalid_captcha']");
|
|
921
|
+
if (capErrSpan) {
|
|
922
|
+
const s = window.getComputedStyle(capErrSpan);
|
|
923
|
+
if (s.display !== "none" && s.visibility !== "hidden") {
|
|
924
|
+
hasDomCaptchaError = true;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
} catch (_) { }
|
|
929
|
+
|
|
883
930
|
return {
|
|
884
931
|
matchedRuleId: fallbackRuleId,
|
|
885
932
|
text: foundText,
|
|
933
|
+
hasDomCaptchaError,
|
|
886
934
|
};
|
|
887
935
|
}, customRules, combinedErrorSelectors);
|
|
888
936
|
|
|
@@ -892,13 +940,32 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
892
940
|
errorText = (checkRulesAndText && checkRulesAndText.text) || "";
|
|
893
941
|
|
|
894
942
|
const regex = new RegExp(invalidPattern, "i");
|
|
895
|
-
const isCaptchaError =
|
|
896
|
-
|
|
897
|
-
|
|
943
|
+
const isCaptchaError =
|
|
944
|
+
Boolean(checkRulesAndText?.hasDomCaptchaError) ||
|
|
945
|
+
(matchedRule ? matchedRule.action === "RETRY_CAPTCHA" : false) ||
|
|
946
|
+
(Boolean(errorText) && regex.test(errorText));
|
|
898
947
|
|
|
899
|
-
//
|
|
948
|
+
// If captcha error occurred, give target portal a brief window to load the refreshed captcha image
|
|
900
949
|
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
901
950
|
let newImageBase64 = null;
|
|
951
|
+
if (isCaptchaError) {
|
|
952
|
+
try {
|
|
953
|
+
const initialSrc = await page.$eval(imageSelector, (el) => el.src || el.getAttribute("src")).catch(() => null);
|
|
954
|
+
await Promise.race([
|
|
955
|
+
page.waitForFunction(
|
|
956
|
+
(sel, old) => {
|
|
957
|
+
const el = document.querySelector(sel);
|
|
958
|
+
return el && (el.src || el.getAttribute("src")) !== old && el.complete;
|
|
959
|
+
},
|
|
960
|
+
{ timeout: 1600 },
|
|
961
|
+
imageSelector,
|
|
962
|
+
initialSrc
|
|
963
|
+
).catch(() => { }),
|
|
964
|
+
new Promise((r) => setTimeout(r, 650)),
|
|
965
|
+
]);
|
|
966
|
+
} catch (_) { }
|
|
967
|
+
}
|
|
968
|
+
|
|
902
969
|
try {
|
|
903
970
|
const newCaptchaEl = await page.$(imageSelector);
|
|
904
971
|
if (newCaptchaEl) {
|
|
@@ -936,8 +1003,9 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
936
1003
|
|
|
937
1004
|
// Runtime non-inspection mode:
|
|
938
1005
|
if (isCaptchaError) {
|
|
1006
|
+
// For captcha errors: NEVER abort session! Keep the browser session alive and reset TTL timer
|
|
939
1007
|
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
940
|
-
const retryTtlSeconds = cfg.sessionTimeoutSeconds ||
|
|
1008
|
+
const retryTtlSeconds = cfg.sessionTimeoutSeconds || 180;
|
|
941
1009
|
session.timeoutTimer = setTimeout(() => {
|
|
942
1010
|
purgeSession(sessionId).catch(() => { });
|
|
943
1011
|
}, retryTtlSeconds * 1000);
|
|
@@ -945,14 +1013,16 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
945
1013
|
return {
|
|
946
1014
|
status: "INVALID_CAPTCHA",
|
|
947
1015
|
success: false,
|
|
948
|
-
message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
|
|
1016
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect. Please retry with the updated image.",
|
|
949
1017
|
canRetry: true,
|
|
950
1018
|
newImageBase64,
|
|
951
1019
|
errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
|
|
1020
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
952
1021
|
};
|
|
953
1022
|
}
|
|
954
1023
|
|
|
955
1024
|
if (errorText || matchedRule) {
|
|
1025
|
+
// For business errors (e.g. invalid GSTIN, not found): abort and purge session immediately
|
|
956
1026
|
await purgeSession(sessionId);
|
|
957
1027
|
return {
|
|
958
1028
|
status: "BUSINESS_ERROR",
|
|
@@ -960,6 +1030,7 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
960
1030
|
errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
|
|
961
1031
|
message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
|
|
962
1032
|
canRetry: false,
|
|
1033
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
963
1034
|
};
|
|
964
1035
|
}
|
|
965
1036
|
|