app-manager-edge-worker 1.3.1 → 1.3.2

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.
@@ -87,9 +87,9 @@ async function purgeSession(sessionId) {
87
87
 
88
88
  try {
89
89
  if (session.page && !session.page.isClosed()) {
90
- await session.page.close().catch(() => {});
90
+ await session.page.close().catch(() => { });
91
91
  }
92
- } catch (_) {}
92
+ } catch (_) { }
93
93
 
94
94
  activeSessions.delete(sessionId);
95
95
  }
@@ -163,9 +163,9 @@ async function locateElementInPageOrFrames(page, selectorList, timeoutMs = 15000
163
163
  if (fEl) {
164
164
  return { target: frame, element: fEl, matchedSelector: combinedSelector };
165
165
  }
166
- } catch (_) {}
166
+ } catch (_) { }
167
167
  }
168
- } catch (_) {}
168
+ } catch (_) { }
169
169
 
170
170
  return null;
171
171
  }
@@ -217,10 +217,10 @@ export async function handleInteractiveSessionInit(payload) {
217
217
  path: c.path || "/",
218
218
  }))
219
219
  )
220
- .catch(() => {});
220
+ .catch(() => { });
221
221
  }
222
222
  if (authentication.type === "HEADERS" && authentication.headers) {
223
- await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
223
+ await page.setExtraHTTPHeaders(authentication.headers).catch(() => { });
224
224
  }
225
225
  }
226
226
 
@@ -230,8 +230,8 @@ export async function handleInteractiveSessionInit(payload) {
230
230
 
231
231
  await page.goto(url, { waitUntil: navWaitUntil, timeout: navTimeout });
232
232
  try {
233
- await page.waitForNetworkIdle({ idleTime: 500, timeout: 4000 }).catch(() => {});
234
- } catch (_) {}
233
+ await page.waitForNetworkIdle({ idleTime: 500, timeout: 4000 }).catch(() => { });
234
+ } catch (_) { }
235
235
 
236
236
  // Execute pre-scrape actions if present
237
237
  if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
@@ -280,8 +280,8 @@ export async function handleInteractiveSessionInit(payload) {
280
280
  const { target, element, matchedSelector } = located;
281
281
 
282
282
  if (input.clearFirst !== false) {
283
- await element.click({ clickCount: 3 }).catch(() => {});
284
- await page.keyboard.press("Backspace").catch(() => {});
283
+ await element.click({ clickCount: 3 }).catch(() => { });
284
+ await page.keyboard.press("Backspace").catch(() => { });
285
285
  }
286
286
 
287
287
  if (input.actionType === "SELECT") {
@@ -290,36 +290,36 @@ export async function handleInteractiveSessionInit(payload) {
290
290
  await element.type(String(input.value), { delay: input.delayMs || 30 });
291
291
  }
292
292
 
293
- // Explicitly dispatch input, change, keydown, keyup, and blur events, plus sync AngularJS ngModel if present
294
- await target.evaluate((sel) => {
295
- const el = document.querySelector(sel);
296
- if (el) {
297
- el.dispatchEvent(new Event("input", { bubbles: true }));
298
- el.dispatchEvent(new Event("change", { bubbles: true }));
299
- el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
300
- el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
301
- el.dispatchEvent(new Event("blur", { bubbles: true }));
302
-
303
- // AngularJS scope & ngModel synchronization
304
- if (window.angular) {
305
- try {
306
- const ngEl = window.angular.element(el);
307
- const ngModel = ngEl.controller("ngModel");
308
- if (ngModel) {
309
- ngModel.$setViewValue(el.value);
310
- ngModel.$render();
311
- ngModel.$commitViewValue();
312
- }
313
- const scope = ngEl.scope();
314
- if (scope && !scope.$$phase) {
315
- scope.$apply();
316
- }
317
- } catch (_) {}
293
+ // Explicitly dispatch input, change, keydown, keyup, and blur events, plus sync AngularJS ngModel if present
294
+ await target.evaluate((sel) => {
295
+ const el = document.querySelector(sel);
296
+ if (el) {
297
+ el.dispatchEvent(new Event("input", { bubbles: true }));
298
+ el.dispatchEvent(new Event("change", { bubbles: true }));
299
+ el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
300
+ el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
301
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
302
+
303
+ // AngularJS scope & ngModel synchronization
304
+ if (window.angular) {
305
+ try {
306
+ const ngEl = window.angular.element(el);
307
+ const ngModel = ngEl.controller("ngModel");
308
+ if (ngModel) {
309
+ ngModel.$setViewValue(el.value);
310
+ ngModel.$render();
311
+ ngModel.$commitViewValue();
312
+ }
313
+ const scope = ngEl.scope();
314
+ if (scope && !scope.$$phase) {
315
+ scope.$apply();
316
+ }
317
+ } catch (_) { }
318
+ }
318
319
  }
319
- }
320
- }, matchedSelector).catch(() => {});
320
+ }, matchedSelector).catch(() => { });
321
321
 
322
- await page.keyboard.press("Tab").catch(() => {});
322
+ await page.keyboard.press("Tab").catch(() => { });
323
323
  await new Promise((r) => setTimeout(r, 300));
324
324
  }
325
325
  }
@@ -338,632 +338,740 @@ export async function handleInteractiveSessionInit(payload) {
338
338
  "#captcha_image",
339
339
  ].filter(Boolean);
340
340
 
341
- const locatedImage = await locateElementInPageOrFrames(page, candidateImageSelectors, imageTimeout);
342
- if (!locatedImage || !locatedImage.element) {
343
- const currentUrl = page.url();
344
- const pageTitle = (await page.title().catch(() => "")) || "Untitled";
345
- throw new Error(
346
- `Captcha element not found with selector '${captchaConfig.imageSelector || candidateImageSelectors[0]}' on page "${pageTitle}" (${currentUrl}) after ${imageTimeout}ms`
347
- );
348
- }
341
+ const locatedImage = await locateElementInPageOrFrames(page, candidateImageSelectors, imageTimeout);
342
+ if (!locatedImage || !locatedImage.element) {
343
+ const currentUrl = page.url();
344
+ const pageTitle = (await page.title().catch(() => "")) || "Untitled";
345
+ throw new Error(
346
+ `Captcha element not found with selector '${captchaConfig.imageSelector || candidateImageSelectors[0]}' on page "${pageTitle}" (${currentUrl}) after ${imageTimeout}ms`
347
+ );
348
+ }
349
+
350
+ const captchaElement = locatedImage.element;
351
+
352
+ // Capture screenshot of the exact visual captcha DOM node
353
+ const screenshotBuffer = await captchaElement.screenshot({
354
+ encoding: "binary",
355
+ type: "png",
356
+ });
357
+ const imageBase64 = Buffer.from(screenshotBuffer).toString("base64");
358
+
359
+ // Settle briefly for form state & portal validation UI
360
+ await new Promise((r) => setTimeout(r, 400));
361
+
362
+ // Capture full-page screenshot showing the filled inputs and revealed captcha
363
+ const pageScreenshotBuffer = await page.screenshot({
364
+ encoding: "binary",
365
+ fullPage: false,
366
+ type: "jpeg",
367
+ quality: 85,
368
+ });
369
+ const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
370
+ const title = (await page.title().catch(() => "")) || "Interactive Portal Session";
371
+ const dimensions = await page
372
+ .evaluate(() => ({
373
+ width: document.documentElement.scrollWidth || window.innerWidth,
374
+ height: document.documentElement.scrollHeight || window.innerHeight,
375
+ }))
376
+ .catch(() => ({ width: 1366, height: 900 }));
377
+
378
+ const sessionTtlSeconds = captchaConfig.sessionTimeoutSeconds || 90;
379
+
380
+ // Set auto-purge timer to prevent resource leaks
381
+ const timeoutTimer = setTimeout(() => {
382
+ purgeSession(sessionId).catch(() => { });
383
+ }, sessionTtlSeconds * 1000);
384
+
385
+ // Store active session in memory
386
+ activeSessions.set(sessionId, {
387
+ page,
388
+ timeoutTimer,
389
+ createdAt: Date.now(),
390
+ url,
391
+ captchaConfig,
392
+ fieldMappings,
393
+ collections,
394
+ });
349
395
 
350
- const captchaElement = locatedImage.element;
351
-
352
- // Capture screenshot of the exact visual captcha DOM node
353
- const screenshotBuffer = await captchaElement.screenshot({
354
- encoding: "binary",
355
- type: "png",
356
- });
357
- const imageBase64 = Buffer.from(screenshotBuffer).toString("base64");
358
-
359
- // Settle briefly for form state & portal validation UI
360
- await new Promise((r) => setTimeout(r, 400));
361
-
362
- // Capture full-page screenshot showing the filled inputs and revealed captcha
363
- const pageScreenshotBuffer = await page.screenshot({
364
- encoding: "binary",
365
- fullPage: false,
366
- type: "jpeg",
367
- quality: 85,
368
- });
369
- const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
370
- const title = (await page.title().catch(() => "")) || "Interactive Portal Session";
371
- const dimensions = await page
372
- .evaluate(() => ({
373
- width: document.documentElement.scrollWidth || window.innerWidth,
374
- height: document.documentElement.scrollHeight || window.innerHeight,
375
- }))
376
- .catch(() => ({ width: 1366, height: 900 }));
377
-
378
- const sessionTtlSeconds = captchaConfig.sessionTimeoutSeconds || 90;
379
-
380
- // Set auto-purge timer to prevent resource leaks
381
- const timeoutTimer = setTimeout(() => {
382
- purgeSession(sessionId).catch(() => {});
383
- }, sessionTtlSeconds * 1000);
384
-
385
- // Store active session in memory
386
- activeSessions.set(sessionId, {
387
- page,
388
- timeoutTimer,
389
- createdAt: Date.now(),
390
- url,
391
- captchaConfig,
392
- fieldMappings,
393
- collections,
394
- });
395
-
396
- return {
397
- sessionId,
398
- imageBase64: `data:image/png;base64,${imageBase64}`,
399
- pageScreenshotBase64,
400
- dimensions,
401
- title,
402
- expiresInSeconds: sessionTtlSeconds,
403
- };
404
- } catch (err) {
405
- await page.close().catch(() => {});
406
- throw err;
396
+ return {
397
+ sessionId,
398
+ imageBase64: `data:image/png;base64,${imageBase64}`,
399
+ pageScreenshotBase64,
400
+ dimensions,
401
+ title,
402
+ expiresInSeconds: sessionTtlSeconds,
403
+ };
404
+ } catch (err) {
405
+ await page.close().catch(() => { });
406
+ throw err;
407
+ }
407
408
  }
408
- }
409
409
 
410
410
  /**
411
411
  * 2. Handles HITL_SESSION_REFRESH:
412
412
  * Clicks the refresh button and verifies the visual MD5 hash of the captcha changed.
413
413
  */
414
414
  export async function handleInteractiveSessionRefresh(payload) {
415
- const { sessionId, captchaConfig = {} } = payload;
416
- const session = activeSessions.get(sessionId);
415
+ const { sessionId, captchaConfig = {} } = payload;
416
+ const session = activeSessions.get(sessionId);
417
417
 
418
- if (!session || !session.page || session.page.isClosed()) {
419
- throw new Error("Interactive session not found or already expired");
420
- }
418
+ if (!session || !session.page || session.page.isClosed()) {
419
+ throw new Error("Interactive session not found or already expired");
420
+ }
421
421
 
422
- const { page } = session;
423
- const cfg = { ...session.captchaConfig, ...captchaConfig };
424
- const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
425
- const refreshSelector = cfg.refreshSelector;
426
-
427
- const candidateImageSelectors = [
428
- cfg.imageSelector,
429
- "#imgCaptcha",
430
- "#captchaImg",
431
- "img[src*='captcha' i]",
432
- "img[id*='captcha' i]",
433
- ].filter(Boolean);
434
-
435
- // 1. Snapshot current image hash and src attribute
436
- const locatedOldImg = await locateElementInPageOrFrames(page, candidateImageSelectors, 8000);
437
- if (!locatedOldImg || !locatedOldImg.element) {
438
- throw new Error(`Captcha element not found with selector: ${imageSelector}`);
439
- }
440
- const oldEl = locatedOldImg.element;
441
- const effectiveImageSelector = locatedOldImg.matchedSelector;
442
-
443
- const oldBuffer = await oldEl.screenshot({ encoding: "binary" });
444
- const oldHash = crypto.createHash("md5").update(oldBuffer).digest("hex");
445
-
446
- const oldSrc = await locatedOldImg.target.evaluate((sel) => {
447
- const el = document.querySelector(sel);
448
- return el ? el.getAttribute("src") : null;
449
- }, effectiveImageSelector);
450
-
451
- // 2. Click the reload button with robust candidate fallbacks
452
- const candidateSelectors = [
453
- refreshSelector,
454
- "button[ng-click*='refreshCaptcha' i]",
455
- "button[ng-click*='captcha' i]",
456
- "*[ng-click*='refreshCaptcha' i]",
457
- "*[ng-click*='captcha' i]",
458
- "#reloadCaptcha",
459
- "#refreshCaptcha",
460
- "a[title*='refresh' i]",
461
- "button[title*='refresh' i]",
462
- "a[title*='reload' i]",
463
- "button[title*='reload' i]",
464
- ".captcha-refresh",
465
- ".reload-captcha",
466
- ".glyphicon-refresh",
467
- "i.fa-refresh",
468
- "i.fa-arrows-rotate",
469
- ].filter(Boolean);
470
-
471
- let clicked = false;
472
- for (const sel of candidateSelectors) {
473
- try {
474
- const locatedBtn = await locateElementInPageOrFrames(page, [sel], 500);
475
- if (locatedBtn && locatedBtn.element) {
476
- await locatedBtn.element.click();
477
- clicked = true;
478
- break;
479
- }
480
- } catch (_) {}
481
- }
422
+ const { page } = session;
423
+ const cfg = { ...session.captchaConfig, ...captchaConfig };
424
+ const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
425
+ const refreshSelector = cfg.refreshSelector;
482
426
 
483
- if (!clicked) {
484
- throw new Error(`Captcha refresh button not found on page (tried: ${refreshSelector || "standard reload selectors"})`);
485
- }
427
+ const candidateImageSelectors = [
428
+ cfg.imageSelector,
429
+ "#imgCaptcha",
430
+ "#captchaImg",
431
+ "img[src*='captcha' i]",
432
+ "img[id*='captcha' i]",
433
+ ].filter(Boolean);
486
434
 
487
- // 3. Wait for src attribute change first
488
- await page
489
- .waitForFunction(
490
- (sel, prevSrc) => {
491
- const el = document.querySelector(sel);
492
- return el && el.getAttribute("src") !== prevSrc;
493
- },
494
- { timeout: 3500 },
495
- imageSelector,
496
- oldSrc
497
- )
498
- .catch(() => {});
499
-
500
- // 4. Verify visual pixel change via MD5 hash check
501
- let newBuffer = null;
502
- let retries = 0;
503
-
504
- while (retries < 6) {
505
- await new Promise((r) => setTimeout(r, 350));
506
- const newEl = await page.$(imageSelector);
507
- if (newEl) {
508
- newBuffer = await newEl.screenshot({ encoding: "binary" });
509
- const newHash = crypto.createHash("md5").update(newBuffer).digest("hex");
510
- if (newHash !== oldHash) {
511
- break; // Verified fresh captcha
435
+ // 1. Snapshot current image hash and src attribute
436
+ const locatedOldImg = await locateElementInPageOrFrames(page, candidateImageSelectors, 8000);
437
+ if (!locatedOldImg || !locatedOldImg.element) {
438
+ throw new Error(`Captcha element not found with selector: ${imageSelector}`);
512
439
  }
513
- }
514
- retries++;
515
- }
516
-
517
- if (!newBuffer) {
518
- newBuffer = await oldEl.screenshot({ encoding: "binary" });
519
- }
440
+ const oldEl = locatedOldImg.element;
441
+ const effectiveImageSelector = locatedOldImg.matchedSelector;
520
442
 
521
- const imageBase64 = Buffer.from(newBuffer).toString("base64");
443
+ const oldBuffer = await oldEl.screenshot({ encoding: "binary" });
444
+ const oldHash = crypto.createHash("md5").update(oldBuffer).digest("hex");
522
445
 
523
- // Capture updated full page screenshot showing new captcha
524
- const pageScreenshotBuffer = await page.screenshot({
525
- encoding: "binary",
526
- fullPage: false,
527
- type: "jpeg",
528
- quality: 85,
529
- });
530
- const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
446
+ const oldSrc = await locatedOldImg.target.evaluate((sel) => {
447
+ const el = document.querySelector(sel);
448
+ return el ? el.getAttribute("src") : null;
449
+ }, effectiveImageSelector);
450
+
451
+ // 2. Click the reload button with robust candidate fallbacks
452
+ const candidateSelectors = [
453
+ refreshSelector,
454
+ "button[ng-click*='refreshCaptcha' i]",
455
+ "button[ng-click*='captcha' i]",
456
+ "*[ng-click*='refreshCaptcha' i]",
457
+ "*[ng-click*='captcha' i]",
458
+ "#reloadCaptcha",
459
+ "#refreshCaptcha",
460
+ "a[title*='refresh' i]",
461
+ "button[title*='refresh' i]",
462
+ "a[title*='reload' i]",
463
+ "button[title*='reload' i]",
464
+ ".captcha-refresh",
465
+ ".reload-captcha",
466
+ ".glyphicon-refresh",
467
+ "i.fa-refresh",
468
+ "i.fa-arrows-rotate",
469
+ ].filter(Boolean);
470
+
471
+ let clicked = false;
472
+ for (const sel of candidateSelectors) {
473
+ try {
474
+ const locatedBtn = await locateElementInPageOrFrames(page, [sel], 500);
475
+ if (locatedBtn && locatedBtn.element) {
476
+ await locatedBtn.element.click();
477
+ clicked = true;
478
+ break;
479
+ }
480
+ } catch (_) { }
481
+ }
531
482
 
532
- return {
533
- sessionId,
534
- imageBase64: `data:image/png;base64,${imageBase64}`,
535
- pageScreenshotBase64,
536
- };
537
- }
483
+ if (!clicked) {
484
+ throw new Error(`Captcha refresh button not found on page (tried: ${refreshSelector || "standard reload selectors"})`);
485
+ }
538
486
 
539
- /**
540
- * 3. Handles HITL_SESSION_SOLVE:
541
- * Types the captcha solution, clicks submit, and performs a post-submit race.
542
- * If captcha is invalid, keeps session alive and snaps new captcha for retry.
543
- * If success, extracts data and closes page.
544
- */
545
- export async function handleInteractiveSessionSolve(payload) {
546
- const {
547
- sessionId,
548
- captchaSolution,
549
- captchaConfig = {},
550
- fieldMappings = [],
551
- collections = [],
552
- preservePageForInspection = false,
553
- } = payload;
554
- const session = activeSessions.get(sessionId);
487
+ // 3. Wait for src attribute change first
488
+ await page
489
+ .waitForFunction(
490
+ (sel, prevSrc) => {
491
+ const el = document.querySelector(sel);
492
+ return el && el.getAttribute("src") !== prevSrc;
493
+ },
494
+ { timeout: 3500 },
495
+ imageSelector,
496
+ oldSrc
497
+ )
498
+ .catch(() => { });
499
+
500
+ // 4. Verify visual pixel change via MD5 hash check
501
+ let newBuffer = null;
502
+ let retries = 0;
503
+
504
+ while (retries < 6) {
505
+ await new Promise((r) => setTimeout(r, 350));
506
+ const newEl = await page.$(imageSelector);
507
+ if (newEl) {
508
+ newBuffer = await newEl.screenshot({ encoding: "binary" });
509
+ const newHash = crypto.createHash("md5").update(newBuffer).digest("hex");
510
+ if (newHash !== oldHash) {
511
+ break; // Verified fresh captcha
512
+ }
513
+ }
514
+ retries++;
515
+ }
555
516
 
556
- if (!session || !session.page || session.page.isClosed()) {
557
- throw new Error("Interactive session not found or already expired");
558
- }
517
+ if (!newBuffer) {
518
+ newBuffer = await oldEl.screenshot({ encoding: "binary" });
519
+ }
559
520
 
560
- const { page } = session;
561
- const cfg = { ...session.captchaConfig, ...captchaConfig };
562
- const mappings = fieldMappings.length > 0 ? fieldMappings : session.fieldMappings || [];
563
- const colls = collections.length > 0 ? collections : session.collections || [];
564
-
565
- const inputSelector = cfg.inputSelector || "#captcha, input[name='captcha']";
566
- const submitSelector = cfg.submitSelector || "button[type='submit']";
567
- const successSelector = cfg.waitForAfterSubmit?.selector || ".table-responsive, table, .result-pane";
568
-
569
- // Broad default error selectors covering Indian gov and modern SPA patterns
570
- const defaultBroadErrorSelector =
571
- "p#alertMsg, #alertMsg, .alertMsg, .alert-danger, .alert-warning, " +
572
- ".has-error, .text-danger, #error-msg, .error-message, .err-msg, " +
573
- ".ui-messages-error, [class*='error-msg'], [class*='alert-message'], " +
574
- ".invalid-feedback, .help-block.error, .form-error, [aria-invalid='true']";
575
-
576
- const customRules = cfg.errorDetection?.rules || [];
577
- const ruleSelectors = customRules.map((r) => r.selector).filter(Boolean);
578
- const combinedErrorSelectors = [
579
- cfg.errorDetection?.selector,
580
- ...ruleSelectors,
581
- defaultBroadErrorSelector,
582
- ]
583
- .filter(Boolean)
584
- .join(", ");
585
-
586
- const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
587
-
588
- // 1. Type captcha solution with robust candidate fallbacks & frame search
589
- const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
590
- const candidateInputSelectors = [
591
- inputSelector,
592
- "#fo-captcha",
593
- "#captcha",
594
- "input[name='cap']",
595
- "input[name*='captcha' i]",
596
- "input[id*='captcha' i]",
597
- "input[placeholder*='captcha' i]",
598
- ].filter(Boolean);
599
-
600
- const locatedInput = await locateElementInPageOrFrames(page, candidateInputSelectors, solveTimeout);
601
- if (!locatedInput || !locatedInput.element) {
602
- throw new Error(`Captcha input field not found with selector: ${inputSelector}`);
603
- }
521
+ const imageBase64 = Buffer.from(newBuffer).toString("base64");
522
+
523
+ // Capture updated full page screenshot showing new captcha
524
+ const pageScreenshotBuffer = await page.screenshot({
525
+ encoding: "binary",
526
+ fullPage: false,
527
+ type: "jpeg",
528
+ quality: 85,
529
+ });
530
+ const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
531
+
532
+ return {
533
+ sessionId,
534
+ imageBase64: `data:image/png;base64,${imageBase64}`,
535
+ pageScreenshotBase64,
536
+ };
537
+ }
604
538
 
605
- await locatedInput.element.click({ clickCount: 3 }).catch(() => {});
606
- await page.keyboard.press("Backspace").catch(() => {});
607
- await locatedInput.element.type(String(captchaSolution).trim(), { delay: 35 });
539
+ /**
540
+ * 3. Handles HITL_SESSION_SOLVE:
541
+ * Types the captcha solution, clicks submit, and performs a post-submit race.
542
+ * If captcha is invalid, keeps session alive and snaps new captcha for retry.
543
+ * If success, extracts data and closes page.
544
+ */
545
+ export async function handleInteractiveSessionSolve(payload) {
546
+ const {
547
+ sessionId,
548
+ captchaSolution,
549
+ captchaConfig = {},
550
+ fieldMappings = [],
551
+ collections = [],
552
+ preservePageForInspection = false,
553
+ } = payload;
554
+ const session = activeSessions.get(sessionId);
555
+
556
+ if (!session || !session.page || session.page.isClosed()) {
557
+ throw new Error("Interactive session not found or already expired");
558
+ }
608
559
 
609
- // Sync input events and AngularJS ngModel for captcha input
610
- await locatedInput.target.evaluate((sel) => {
611
- const el = document.querySelector(sel);
612
- if (el) {
613
- el.dispatchEvent(new Event("input", { bubbles: true }));
614
- el.dispatchEvent(new Event("change", { bubbles: true }));
615
- el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
616
- el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
617
- el.dispatchEvent(new Event("blur", { bubbles: true }));
560
+ const { page } = session;
561
+ const cfg = { ...session.captchaConfig, ...captchaConfig };
562
+ const mappings = fieldMappings.length > 0 ? fieldMappings : session.fieldMappings || [];
563
+ const colls = collections.length > 0 ? collections : session.collections || [];
564
+
565
+ const inputSelector = cfg.inputSelector || "#captcha, input[name='captcha']";
566
+ const submitSelector = cfg.submitSelector || "button[type='submit']";
567
+ const successSelector = cfg.waitForAfterSubmit?.selector || ".table-responsive, table, .result-pane";
568
+
569
+ // Broad default error selectors covering Indian gov and modern SPA patterns
570
+ const defaultBroadErrorSelector =
571
+ "p#alertMsg, #alertMsg, .alertMsg, .alert-danger, .alert-warning, " +
572
+ ".has-error, .text-danger, #error-msg, .error-message, .err-msg, " +
573
+ ".ui-messages-error, [class*='error-msg'], [class*='alert-message'], " +
574
+ ".invalid-feedback, .help-block.error, .form-error, [aria-invalid='true'], " +
575
+ ".err, span.err, p.err, div.err, [class*='err-'], [class*='-error'], " +
576
+ "[data-ng-bind*='ERR'], [data-ng-bind*='err'], [data-ng-show*='err'], " +
577
+ "[data-ng-show*='w_gst'], [data-ng-if*='captcha'], [data-ng-show*='captcha'], [role='alert'], " +
578
+ "[style*='color:red'], [style*='color: red']";
579
+
580
+ const customRules = cfg.errorDetection?.rules || [];
581
+ const ruleSelectors = customRules.map((r) => r.selector).filter(Boolean);
582
+ const combinedErrorSelectors = [
583
+ cfg.errorDetection?.selector,
584
+ ...ruleSelectors,
585
+ defaultBroadErrorSelector,
586
+ ]
587
+ .filter(Boolean)
588
+ .join(", ");
589
+
590
+ const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
591
+
592
+ // 1. Type captcha solution with robust candidate fallbacks & frame search
593
+ const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
594
+ const candidateInputSelectors = [
595
+ inputSelector,
596
+ "#fo-captcha",
597
+ "#captcha",
598
+ "input[name='cap']",
599
+ "input[name*='captcha' i]",
600
+ "input[id*='captcha' i]",
601
+ "input[placeholder*='captcha' i]",
602
+ ].filter(Boolean);
603
+
604
+ const locatedInput = await locateElementInPageOrFrames(page, candidateInputSelectors, solveTimeout);
605
+ if (!locatedInput || !locatedInput.element) {
606
+ throw new Error(`Captcha input field not found with selector: ${inputSelector}`);
607
+ }
618
608
 
619
- if (window.angular) {
620
- try {
621
- const ngEl = window.angular.element(el);
622
- const ngModel = ngEl.controller("ngModel");
623
- if (ngModel) {
624
- ngModel.$setViewValue(el.value);
625
- ngModel.$render();
626
- ngModel.$commitViewValue();
627
- }
628
- const scope = ngEl.scope();
629
- if (scope && !scope.$$phase) scope.$apply();
630
- } catch (_) {}
631
- }
632
- }
633
- }, locatedInput.matchedSelector).catch(() => {});
634
-
635
- // 2. Click submit button with candidate fallbacks & frame search
636
- const candidateSubmitSelectors = [
637
- submitSelector,
638
- "#lotsearch",
639
- "button[type='submit']",
640
- "input[type='submit']",
641
- "button[id*='search' i]",
642
- "button[id*='submit' i]",
643
- "button.btn-primary",
644
- ].filter(Boolean);
645
-
646
- const locatedSubmit = await locateElementInPageOrFrames(page, candidateSubmitSelectors, solveTimeout);
647
- if (!locatedSubmit || !locatedSubmit.element) {
648
- throw new Error(`Submit button not found with selector: ${submitSelector}`);
649
- }
609
+ await locatedInput.element.click({ clickCount: 3 }).catch(() => { });
610
+ await page.keyboard.press("Backspace").catch(() => { });
611
+ await locatedInput.element.type(String(captchaSolution).trim(), { delay: 35 });
650
612
 
651
- await locatedSubmit.element.click();
613
+ // Sync input events and AngularJS ngModel for captcha input
614
+ await locatedInput.target.evaluate((sel) => {
615
+ const el = document.querySelector(sel);
616
+ if (el) {
617
+ el.dispatchEvent(new Event("input", { bubbles: true }));
618
+ el.dispatchEvent(new Event("change", { bubbles: true }));
619
+ el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
620
+ el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
621
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
652
622
 
653
- // 3. Post-Submit Race: Wait for Success Selector OR Error Selector
654
- // In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
655
- const safetyTimeoutMs = preservePageForInspection
656
- ? Math.min(cfg.waitForAfterSubmit?.timeoutMs || 8000, 6000)
657
- : (cfg.waitForAfterSubmit?.timeoutMs || 15000);
623
+ if (window.angular) {
624
+ try {
625
+ const ngEl = window.angular.element(el);
626
+ const ngModel = ngEl.controller("ngModel");
627
+ if (ngModel) {
628
+ ngModel.$setViewValue(el.value);
629
+ ngModel.$render();
630
+ ngModel.$commitViewValue();
631
+ }
632
+ const scope = ngEl.scope();
633
+ if (scope && !scope.$$phase) scope.$apply();
634
+ } catch (_) { }
635
+ }
636
+ }
637
+ }, locatedInput.matchedSelector).catch(() => { });
638
+
639
+ // 2. Click submit button with candidate fallbacks & frame search
640
+ const candidateSubmitSelectors = [
641
+ submitSelector,
642
+ "#lotsearch",
643
+ "button[type='submit']",
644
+ "input[type='submit']",
645
+ "button[id*='search' i]",
646
+ "button[id*='submit' i]",
647
+ "button.btn-primary",
648
+ ].filter(Boolean);
649
+
650
+ const locatedSubmit = await locateElementInPageOrFrames(page, candidateSubmitSelectors, solveTimeout);
651
+ if (!locatedSubmit || !locatedSubmit.element) {
652
+ throw new Error(`Submit button not found with selector: ${submitSelector}`);
653
+ }
658
654
 
659
- const matchedOutcome = await page
660
- .waitForFunction(
661
- (succ, err) => {
662
- if (document.querySelector(succ)) return "SUCCESS";
663
- try {
664
- const elements = document.querySelectorAll(err);
665
- for (const el of elements) {
666
- const txt = (el.innerText || el.textContent || "").trim();
667
- if (txt.length > 0) {
668
- const style = window.getComputedStyle(el);
669
- if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
670
- return "ERROR";
671
- }
655
+ await locatedSubmit.element.click().catch(() => { });
656
+ // Also dispatch native click & submit events to ensure AngularJS/React/SPA form listeners fire
657
+ await locatedSubmit.target.evaluate((sel) => {
658
+ const btn = document.querySelector(sel);
659
+ if (btn) {
660
+ try { btn.click(); } catch (_) { }
661
+ if (btn.form) {
662
+ try {
663
+ btn.form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
664
+ } catch (_) { }
672
665
  }
673
666
  }
674
- } catch (e) {}
675
- return null;
676
- },
677
- { timeout: safetyTimeoutMs },
678
- successSelector,
679
- combinedErrorSelectors
680
- )
681
- .then((handle) => handle.jsonValue())
682
- .catch(() => "TIMEOUT");
683
-
684
- // Case A: Success Result Page
685
- if (matchedOutcome === "SUCCESS") {
686
- const raw = {};
687
- for (const mapping of mappings) {
688
- let val = null;
689
- const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
690
- for (const strat of strategies) {
691
- val = await extractBrowserField(page, strat);
692
- if (val !== null && val !== undefined && val !== "") break;
693
- }
667
+ }, locatedSubmit.matchedSelector).catch(() => { });
668
+
669
+ // 3. Post-Submit Race: Wait for Success Selector OR Error Selector
670
+ // In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
671
+ const safetyTimeoutMs = preservePageForInspection
672
+ ? Math.min(cfg.waitForAfterSubmit?.timeoutMs || 8000, 6000)
673
+ : (cfg.waitForAfterSubmit?.timeoutMs || 15000);
674
+
675
+ const matchedOutcome = await page
676
+ .waitForFunction(
677
+ (succ, err) => {
678
+ const isVisible = (el) => {
679
+ if (!el) return false;
680
+ const style = window.getComputedStyle(el);
681
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
682
+ const rects = el.getClientRects();
683
+ if (rects.length === 0) return false;
684
+ return rects[0].width > 0 || rects[0].height > 0;
685
+ };
686
+
687
+ const isRealError = (txt) => {
688
+ if (!txt || txt.length < 3) return false;
689
+ const lower = txt.trim().toLowerCase();
690
+ if (lower.startsWith("loading") || lower.startsWith("please wait") || lower.startsWith("processing")) {
691
+ return false;
692
+ }
693
+ return true;
694
+ };
694
695
 
695
- if (mapping.transforms && mapping.transforms.length > 0) {
696
- val = runTransformPipeline(val, mapping.transforms);
697
- }
698
- if (mapping.type) {
699
- val = castValueToType(val, mapping.type);
700
- }
701
- raw[mapping.field || mapping.key] = val;
702
- }
696
+ // 1. Error selectors check (Must run FIRST to prevent false positives from generic table selectors)
697
+ try {
698
+ const elements = document.querySelectorAll(err);
699
+ for (const el of elements) {
700
+ const txt = (el.innerText || el.textContent || "").trim();
701
+ if (isRealError(txt) && isVisible(el)) {
702
+ return "ERROR";
703
+ }
704
+ }
705
+ } catch (_) { }
703
706
 
704
- if (preservePageForInspection) {
705
- // Keep the page alive for visual inspection & testing
706
- if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
707
- const inspectTtl = 300; // 5 minutes for interactive element picking
708
- session.timeoutTimer = setTimeout(() => {
709
- purgeSession(sessionId).catch(() => {});
710
- }, inspectTtl * 1000);
707
+ // 2. Semantic error text scan across visible leaf elements
708
+ try {
709
+ const errorRegex = /\b(invalid\s*(gstin|uin|captcha|code|input|format|number|pan)|gstin.*invalid|does\s*not\s*exist|record\s*not\s*found|no\s*records?\s*found|incorrect\s*captcha|wrong\s*captcha|enter\s*valid\s*letters|enter\s*a\s*valid|not\s*registered|mandatory\s*field|action\s*failed)\b/i;
710
+ const candidates = document.querySelectorAll("span, p, div, label, font, b, strong");
711
+ for (const c of candidates) {
712
+ if (c.children.length === 0 || c.tagName === "P" || c.tagName === "SPAN") {
713
+ const txt = (c.innerText || c.textContent || "").trim();
714
+ if (txt.length >= 5 && txt.length < 250 && errorRegex.test(txt) && isRealError(txt)) {
715
+ if (isVisible(c)) {
716
+ return "ERROR";
717
+ }
718
+ }
719
+ }
720
+ }
721
+ } catch (_) { }
711
722
 
712
- // Settle time for tables & styles
713
- await new Promise((r) => setTimeout(r, 600));
723
+ // 3. Success check: must be genuinely visible and contain substantive data
724
+ try {
725
+ const successElements = document.querySelectorAll(succ);
726
+ for (const el of successElements) {
727
+ if (isVisible(el)) {
728
+ // Ignore empty 1-cell layout tables like captcha wrapper tables
729
+ if (el.tagName === "TABLE" && (el.innerText || "").trim().length < 15) {
730
+ continue;
731
+ }
732
+ return "SUCCESS";
733
+ }
734
+ }
735
+ } catch (_) { }
714
736
 
715
- const snap = await captureInspectionSnapshot(page);
716
- return {
717
- status: "COMPLETED",
718
- success: true,
719
- data: raw,
720
- ...snap,
721
- sessionId,
722
- isInspectionMode: true,
723
- };
724
- }
737
+ return null;
738
+ },
739
+ { timeout: safetyTimeoutMs },
740
+ successSelector,
741
+ combinedErrorSelectors
742
+ )
743
+ .then((handle) => handle.jsonValue())
744
+ .catch(() => "TIMEOUT");
745
+
746
+ // Case A: Success Result Page
747
+ if (matchedOutcome === "SUCCESS") {
748
+ const raw = {};
749
+ for (const mapping of mappings) {
750
+ let val = null;
751
+ const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
752
+ for (const strat of strategies) {
753
+ val = await extractBrowserField(page, strat);
754
+ if (val !== null && val !== undefined && val !== "") break;
755
+ }
725
756
 
726
- // Clean up session and close page
727
- await purgeSession(sessionId);
757
+ if (mapping.transforms && mapping.transforms.length > 0) {
758
+ val = runTransformPipeline(val, mapping.transforms);
759
+ }
760
+ if (mapping.type) {
761
+ val = castValueToType(val, mapping.type);
762
+ }
763
+ raw[mapping.field || mapping.key] = val;
764
+ }
728
765
 
729
- return {
730
- status: "COMPLETED",
731
- success: true,
732
- data: raw,
733
- };
734
- }
766
+ if (preservePageForInspection) {
767
+ // Keep the page alive for visual inspection & testing
768
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
769
+ const inspectTtl = 300; // 5 minutes for interactive element picking
770
+ session.timeoutTimer = setTimeout(() => {
771
+ purgeSession(sessionId).catch(() => { });
772
+ }, inspectTtl * 1000);
773
+
774
+ // Settle time for tables & styles
775
+ await new Promise((r) => setTimeout(r, 600));
776
+
777
+ const snap = await captureInspectionSnapshot(page);
778
+ return {
779
+ status: "COMPLETED",
780
+ success: true,
781
+ data: raw,
782
+ ...snap,
783
+ sessionId,
784
+ isInspectionMode: true,
785
+ };
786
+ }
735
787
 
736
- // Evaluate error rules and messages if matchedOutcome === "ERROR" or if we are inspecting
737
- let matchedRule = null;
738
- let errorText = "";
788
+ // Clean up session and close page
789
+ await purgeSession(sessionId);
739
790
 
740
- // 1. Check custom configured rules first
741
- for (const rule of customRules) {
742
- if (!rule.selector) continue;
743
- const check = await page.evaluate((sel, pat) => {
744
- try {
745
- const els = document.querySelectorAll(sel);
746
- for (const el of els) {
747
- const txt = (el.innerText || el.textContent || "").trim();
748
- if (txt) {
749
- const style = window.getComputedStyle(el);
750
- if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
751
- if (!pat) return { matched: true, text: txt };
752
- const re = new RegExp(pat, "i");
753
- if (re.test(txt)) return { matched: true, text: txt };
754
- }
791
+ return {
792
+ status: "COMPLETED",
793
+ success: true,
794
+ data: raw,
795
+ };
755
796
  }
756
- } catch (e) {}
757
- return null;
758
- }, rule.selector, rule.pattern);
759
-
760
- if (check && check.matched) {
761
- matchedRule = rule;
762
- errorText = check.text;
763
- break;
764
- }
765
- }
766
797
 
767
- // 2. If no custom rule matched, extract text from combined error selectors
768
- if (!matchedRule) {
769
- errorText = await page.evaluate((sel) => {
770
- try {
771
- const els = document.querySelectorAll(sel);
772
- for (const el of els) {
773
- const txt = (el.innerText || el.textContent || "").trim();
774
- if (txt) {
798
+ // Evaluate error rules and messages if matchedOutcome === "ERROR" or if we are inspecting or if timed out
799
+ let matchedRule = null;
800
+ let errorText = "";
801
+
802
+ const checkRulesAndText = await page.evaluate((rules, errorSelector) => {
803
+ const isVisible = (el) => {
804
+ if (!el) return false;
775
805
  const style = window.getComputedStyle(el);
776
- if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
777
- return txt;
806
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
807
+ const rects = el.getClientRects();
808
+ if (rects.length === 0) return false;
809
+ return rects[0].width > 0 || rects[0].height > 0;
810
+ };
811
+
812
+ const isRealError = (txt) => {
813
+ if (!txt || txt.length < 3) return false;
814
+ const lower = txt.trim().toLowerCase();
815
+ if (lower.startsWith("loading") || lower.startsWith("please wait") || lower.startsWith("processing")) {
816
+ return false;
778
817
  }
818
+ return true;
819
+ };
820
+
821
+ // 1. Check custom rules with rule.selector
822
+ for (const rule of rules) {
823
+ if (!rule.selector) continue;
824
+ try {
825
+ const els = document.querySelectorAll(rule.selector);
826
+ for (const el of els) {
827
+ const txt = (el.innerText || el.textContent || "").trim();
828
+ if (isRealError(txt) && isVisible(el)) {
829
+ if (!rule.pattern) return { matchedRuleId: rule.id, text: txt };
830
+ const re = new RegExp(rule.pattern, "i");
831
+ if (re.test(txt)) return { matchedRuleId: rule.id, text: txt };
832
+ }
833
+ }
834
+ } catch (_) { }
779
835
  }
780
- }
781
- } catch (e) {}
782
- return "";
783
- }, combinedErrorSelectors);
784
- }
785
836
 
786
- const regex = new RegExp(invalidPattern, "i");
787
- const isCaptchaError = matchedRule
788
- ? matchedRule.action === "RETRY_CAPTCHA"
789
- : (Boolean(errorText) && regex.test(errorText));
837
+ // 2. Extract visible text from combinedErrorSelectors
838
+ let foundText = "";
839
+ try {
840
+ const els = document.querySelectorAll(errorSelector);
841
+ for (const el of els) {
842
+ const txt = (el.innerText || el.textContent || "").trim();
843
+ if (isRealError(txt) && isVisible(el)) {
844
+ foundText = txt;
845
+ break;
846
+ }
847
+ }
848
+ } catch (_) { }
849
+
850
+ // 3. Semantic fallback if foundText is still empty
851
+ if (!foundText) {
852
+ const errorRegex = /\b(invalid\s*(gstin|uin|captcha|code|input|format|number|pan)|gstin.*invalid|does\s*not\s*exist|record\s*not\s*found|no\s*records?\s*found|incorrect\s*captcha|wrong\s*captcha|enter\s*valid\s*letters|enter\s*a\s*valid|not\s*registered|mandatory\s*field|action\s*failed)\b/i;
853
+ const candidates = document.querySelectorAll("span, p, div, label, font, b, strong");
854
+ for (const c of candidates) {
855
+ if (c.children.length === 0 || c.tagName === "P" || c.tagName === "SPAN") {
856
+ const txt = (c.innerText || c.textContent || "").trim();
857
+ if (txt.length >= 5 && txt.length < 250 && errorRegex.test(txt) && isRealError(txt)) {
858
+ if (isVisible(c)) {
859
+ foundText = txt;
860
+ break;
861
+ }
862
+ }
863
+ }
864
+ }
865
+ }
790
866
 
791
- // Check if new captcha image was rendered on page
792
- const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
793
- let newImageBase64 = null;
794
- try {
795
- const newCaptchaEl = await page.$(imageSelector);
796
- if (newCaptchaEl) {
797
- const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
798
- newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
799
- }
800
- } catch (e) {}
801
-
802
- // CRITICAL FOR INSPECTOR: In inspection mode, NEVER purge the session on errors or timeout!
803
- // Always snap the live DOM and return it so the user can inspect the red text and configure selectors
804
- if (preservePageForInspection) {
805
- if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
806
- const inspectTtl = 300;
807
- session.timeoutTimer = setTimeout(() => {
808
- purgeSession(sessionId).catch(() => {});
809
- }, inspectTtl * 1000);
810
-
811
- const snap = await captureInspectionSnapshot(page);
812
- const effectiveStatus = isCaptchaError
813
- ? "INVALID_CAPTCHA"
814
- : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
815
-
816
- return {
817
- status: effectiveStatus,
818
- success: true, // Allows frontend to render the inspection screenshot
819
- errorCode: (matchedRule && matchedRule.id) || (isCaptchaError ? "INVALID_CAPTCHA" : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE")),
820
- 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."),
821
- newImageBase64,
822
- matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
823
- sessionId,
824
- isInspectionMode: true,
825
- ...snap,
826
- };
827
- }
867
+ // 4. If we found text, check if any custom rule matches this text by pattern (even if rule.selector didn't match!)
868
+ let fallbackRuleId = null;
869
+ if (foundText) {
870
+ for (const rule of rules) {
871
+ if (rule.pattern) {
872
+ try {
873
+ const re = new RegExp(rule.pattern, "i");
874
+ if (re.test(foundText)) {
875
+ fallbackRuleId = rule.id;
876
+ break;
877
+ }
878
+ } catch (_) { }
879
+ }
880
+ }
881
+ }
828
882
 
829
- // Runtime non-inspection mode:
830
- if (isCaptchaError) {
831
- if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
832
- const retryTtlSeconds = cfg.sessionTimeoutSeconds || 90;
833
- session.timeoutTimer = setTimeout(() => {
834
- purgeSession(sessionId).catch(() => {});
835
- }, retryTtlSeconds * 1000);
836
-
837
- return {
838
- status: "INVALID_CAPTCHA",
839
- success: false,
840
- message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
841
- canRetry: true,
842
- newImageBase64,
843
- errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
844
- };
845
- }
883
+ return {
884
+ matchedRuleId: fallbackRuleId,
885
+ text: foundText,
886
+ };
887
+ }, customRules, combinedErrorSelectors);
846
888
 
847
- if (errorText || matchedRule) {
848
- await purgeSession(sessionId);
849
- return {
850
- status: "BUSINESS_ERROR",
851
- success: false,
852
- errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
853
- message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
854
- canRetry: false,
855
- };
856
- }
889
+ if (checkRulesAndText && checkRulesAndText.matchedRuleId) {
890
+ matchedRule = customRules.find((r) => r.id === checkRulesAndText.matchedRuleId) || null;
891
+ }
892
+ errorText = (checkRulesAndText && checkRulesAndText.text) || "";
857
893
 
858
- // Runtime Timeout
859
- await purgeSession(sessionId);
860
- return {
861
- status: "TIMEOUT",
862
- success: false,
863
- message: "Target portal did not respond with results within the expected time window.",
864
- canRetry: false,
865
- };
866
- }
894
+ const regex = new RegExp(invalidPattern, "i");
895
+ const isCaptchaError = matchedRule
896
+ ? matchedRule.action === "RETRY_CAPTCHA"
897
+ : (Boolean(errorText) && regex.test(errorText));
867
898
 
868
- /**
869
- * Capture full inspection snapshot helper
870
- */
871
- async function captureInspectionSnapshot(page) {
872
- await new Promise((r) => setTimeout(r, 500));
873
- const screenshotBuffer = await page.screenshot({
874
- encoding: "binary",
875
- fullPage: false,
876
- type: "jpeg",
877
- quality: 85,
878
- });
879
- const screenshotBase64 = `data:image/jpeg;base64,${Buffer.from(screenshotBuffer).toString("base64")}`;
880
- const title = await page.title().catch(() => "");
881
- const dimensions = await page
882
- .evaluate(() => ({
883
- width: document.documentElement.scrollWidth || window.innerWidth,
884
- height: document.documentElement.scrollHeight || window.innerHeight,
885
- }))
886
- .catch(() => ({ width: 1366, height: 900 }));
887
-
888
- return {
889
- screenshotBase64,
890
- dimensions,
891
- title: title || "Inspection Page",
892
- };
893
- }
899
+ // Check if new captcha image was rendered on page
900
+ const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
901
+ let newImageBase64 = null;
902
+ try {
903
+ const newCaptchaEl = await page.$(imageSelector);
904
+ if (newCaptchaEl) {
905
+ const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
906
+ newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
907
+ }
908
+ } catch (e) { }
909
+
910
+ // CRITICAL FOR INSPECTOR: In inspection mode, NEVER purge the session on errors or timeout!
911
+ // Always snap the live DOM and return it so the user can inspect the red text and configure selectors
912
+ if (preservePageForInspection) {
913
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
914
+ const inspectTtl = 300;
915
+ session.timeoutTimer = setTimeout(() => {
916
+ purgeSession(sessionId).catch(() => { });
917
+ }, inspectTtl * 1000);
918
+
919
+ const snap = await captureInspectionSnapshot(page);
920
+ const effectiveStatus = isCaptchaError
921
+ ? "INVALID_CAPTCHA"
922
+ : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
923
+
924
+ return {
925
+ status: effectiveStatus,
926
+ success: true, // Allows frontend to render the inspection screenshot
927
+ errorCode: (matchedRule && matchedRule.id) || (isCaptchaError ? "INVALID_CAPTCHA" : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE")),
928
+ 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."),
929
+ newImageBase64,
930
+ matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
931
+ sessionId,
932
+ isInspectionMode: true,
933
+ ...snap,
934
+ };
935
+ }
894
936
 
895
- /**
896
- * Force capture snapshot of the active interactive session's page
897
- */
898
- export async function handleInteractiveSessionSnapshot(payload) {
899
- const { sessionId } = payload;
900
- const session = activeSessions.get(sessionId);
937
+ // Runtime non-inspection mode:
938
+ if (isCaptchaError) {
939
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
940
+ const retryTtlSeconds = cfg.sessionTimeoutSeconds || 90;
941
+ session.timeoutTimer = setTimeout(() => {
942
+ purgeSession(sessionId).catch(() => { });
943
+ }, retryTtlSeconds * 1000);
944
+
945
+ return {
946
+ status: "INVALID_CAPTCHA",
947
+ success: false,
948
+ message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
949
+ canRetry: true,
950
+ newImageBase64,
951
+ errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
952
+ };
953
+ }
901
954
 
902
- if (!session || !session.page || session.page.isClosed()) {
903
- throw new Error("Interactive session not found or already expired");
904
- }
955
+ if (errorText || matchedRule) {
956
+ await purgeSession(sessionId);
957
+ return {
958
+ status: "BUSINESS_ERROR",
959
+ success: false,
960
+ errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
961
+ message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
962
+ canRetry: false,
963
+ };
964
+ }
905
965
 
906
- const { page } = session;
907
- if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
908
- session.timeoutTimer = setTimeout(() => {
909
- purgeSession(sessionId).catch(() => {});
910
- }, 300 * 1000);
966
+ // Runtime Timeout
967
+ await purgeSession(sessionId);
968
+ return {
969
+ status: "TIMEOUT",
970
+ success: false,
971
+ message: "Target portal did not respond with results within the expected time window.",
972
+ canRetry: false,
973
+ };
974
+ }
911
975
 
912
- const snap = await captureInspectionSnapshot(page);
913
- return {
914
- success: true,
915
- sessionId,
916
- ...snap,
917
- };
918
- }
976
+ /**
977
+ * Capture full inspection snapshot helper
978
+ */
979
+ async function captureInspectionSnapshot(page) {
980
+ await new Promise((r) => setTimeout(r, 500));
981
+ const screenshotBuffer = await page.screenshot({
982
+ encoding: "binary",
983
+ fullPage: false,
984
+ type: "jpeg",
985
+ quality: 85,
986
+ });
987
+ const screenshotBase64 = `data:image/jpeg;base64,${Buffer.from(screenshotBuffer).toString("base64")}`;
988
+ const title = await page.title().catch(() => "");
989
+ const dimensions = await page
990
+ .evaluate(() => ({
991
+ width: document.documentElement.scrollWidth || window.innerWidth,
992
+ height: document.documentElement.scrollHeight || window.innerHeight,
993
+ }))
994
+ .catch(() => ({ width: 1366, height: 900 }));
995
+
996
+ return {
997
+ screenshotBase64,
998
+ dimensions,
999
+ title: title || "Inspection Page",
1000
+ };
1001
+ }
919
1002
 
920
- /**
921
- * 4. Handles HITL_SESSION_ABORT:
922
- * Explicit client abort or session abandonment.
923
- */
924
- export async function handleInteractiveSessionAbort(payload) {
925
- const { sessionId } = payload;
926
- if (sessionId) {
927
- await purgeSession(sessionId);
928
- }
929
- return { success: true, aborted: true };
930
- }
1003
+ /**
1004
+ * Force capture snapshot of the active interactive session's page
1005
+ */
1006
+ export async function handleInteractiveSessionSnapshot(payload) {
1007
+ const { sessionId } = payload;
1008
+ const session = activeSessions.get(sessionId);
931
1009
 
932
- /**
933
- * 5. Handles HITL_SESSION_EXTRACT:
934
- * Extracts fields directly from an active interactive session's live page.
935
- */
936
- export async function handleInteractiveSessionExtract(payload) {
937
- const { sessionId, fieldMappings = [], collections = [] } = payload;
938
- const session = activeSessions.get(sessionId);
1010
+ if (!session || !session.page || session.page.isClosed()) {
1011
+ throw new Error("Interactive session not found or already expired");
1012
+ }
939
1013
 
940
- if (!session || !session.page || session.page.isClosed()) {
941
- throw new Error("Interactive session not found or already expired");
942
- }
1014
+ const { page } = session;
1015
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
1016
+ session.timeoutTimer = setTimeout(() => {
1017
+ purgeSession(sessionId).catch(() => { });
1018
+ }, 300 * 1000);
1019
+
1020
+ const snap = await captureInspectionSnapshot(page);
1021
+ return {
1022
+ success: true,
1023
+ sessionId,
1024
+ ...snap,
1025
+ };
1026
+ }
943
1027
 
944
- const { page } = session;
945
- const raw = {};
1028
+ /**
1029
+ * 4. Handles HITL_SESSION_ABORT:
1030
+ * Explicit client abort or session abandonment.
1031
+ */
1032
+ export async function handleInteractiveSessionAbort(payload) {
1033
+ const { sessionId } = payload;
1034
+ if (sessionId) {
1035
+ await purgeSession(sessionId);
1036
+ }
1037
+ return { success: true, aborted: true };
1038
+ }
946
1039
 
947
- for (const mapping of fieldMappings) {
948
- let val = null;
949
- const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
950
- for (const strat of strategies) {
951
- val = await extractBrowserField(page, strat);
952
- if (val !== null && val !== undefined && val !== "") break;
953
- }
1040
+ /**
1041
+ * 5. Handles HITL_SESSION_EXTRACT:
1042
+ * Extracts fields directly from an active interactive session's live page.
1043
+ */
1044
+ export async function handleInteractiveSessionExtract(payload) {
1045
+ const { sessionId, fieldMappings = [], collections = [] } = payload;
1046
+ const session = activeSessions.get(sessionId);
954
1047
 
955
- if (mapping.transforms && mapping.transforms.length > 0) {
956
- val = runTransformPipeline(val, mapping.transforms);
957
- }
958
- if (mapping.type) {
959
- val = castValueToType(val, mapping.type);
960
- }
961
- raw[mapping.field || mapping.key] = val;
962
- }
1048
+ if (!session || !session.page || session.page.isClosed()) {
1049
+ throw new Error("Interactive session not found or already expired");
1050
+ }
963
1051
 
964
- return {
965
- success: true,
966
- data: raw,
967
- };
968
- }
1052
+ const { page } = session;
1053
+ const raw = {};
1054
+
1055
+ for (const mapping of fieldMappings) {
1056
+ let val = null;
1057
+ const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
1058
+ for (const strat of strategies) {
1059
+ val = await extractBrowserField(page, strat);
1060
+ if (val !== null && val !== undefined && val !== "") break;
1061
+ }
1062
+
1063
+ if (mapping.transforms && mapping.transforms.length > 0) {
1064
+ val = runTransformPipeline(val, mapping.transforms);
1065
+ }
1066
+ if (mapping.type) {
1067
+ val = castValueToType(val, mapping.type);
1068
+ }
1069
+ raw[mapping.field || mapping.key] = val;
1070
+ }
1071
+
1072
+ return {
1073
+ success: true,
1074
+ data: raw,
1075
+ };
1076
+ }
969
1077