app-manager-edge-worker 1.3.1 → 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 +15 -0
- package/package.json +2 -2
- package/src/banner.js +1 -1
- package/src/client.js +5 -5
- package/src/handlers/interactiveHandler.js +788 -609
|
@@ -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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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,811 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
338
338
|
"#captcha_image",
|
|
339
339
|
].filter(Boolean);
|
|
340
340
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
349
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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;
|
|
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
|
-
|
|
416
|
-
|
|
415
|
+
const { sessionId, captchaConfig = {} } = payload;
|
|
416
|
+
const session = activeSessions.get(sessionId);
|
|
417
417
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
419
|
+
throw new Error("Interactive session not found or already expired");
|
|
420
|
+
}
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
515
|
-
}
|
|
440
|
+
const oldEl = locatedOldImg.element;
|
|
441
|
+
const effectiveImageSelector = locatedOldImg.matchedSelector;
|
|
516
442
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
517
|
+
if (!newBuffer) {
|
|
518
|
+
newBuffer = await oldEl.screenshot({ encoding: "binary" });
|
|
519
|
+
}
|
|
559
520
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
// 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
|
+
|
|
539
|
+
return {
|
|
540
|
+
sessionId,
|
|
541
|
+
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
542
|
+
pageScreenshotBase64,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
604
545
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
546
|
+
/**
|
|
547
|
+
* 3. Handles HITL_SESSION_SOLVE:
|
|
548
|
+
* Types the captcha solution, clicks submit, and performs a post-submit race.
|
|
549
|
+
* If captcha is invalid, keeps session alive and snaps new captcha for retry.
|
|
550
|
+
* If success, extracts data and closes page.
|
|
551
|
+
*/
|
|
552
|
+
export async function handleInteractiveSessionSolve(payload) {
|
|
553
|
+
const {
|
|
554
|
+
sessionId,
|
|
555
|
+
captchaSolution,
|
|
556
|
+
captchaConfig = {},
|
|
557
|
+
fieldMappings = [],
|
|
558
|
+
collections = [],
|
|
559
|
+
preservePageForInspection = false,
|
|
560
|
+
} = payload;
|
|
561
|
+
const session = activeSessions.get(sessionId);
|
|
562
|
+
|
|
563
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
564
|
+
throw new Error("Interactive session not found or already expired");
|
|
565
|
+
}
|
|
608
566
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
567
|
+
const { page } = session;
|
|
568
|
+
const cfg = { ...session.captchaConfig, ...captchaConfig };
|
|
569
|
+
const mappings = fieldMappings.length > 0 ? fieldMappings : session.fieldMappings || [];
|
|
570
|
+
const colls = collections.length > 0 ? collections : session.collections || [];
|
|
571
|
+
|
|
572
|
+
const inputSelector = cfg.inputSelector || "#captcha, input[name='captcha']";
|
|
573
|
+
const submitSelector = cfg.submitSelector || "button[type='submit']";
|
|
574
|
+
const successSelector = cfg.waitForAfterSubmit?.selector || ".table-responsive, table, .result-pane";
|
|
575
|
+
|
|
576
|
+
// Broad default error selectors covering Indian gov and modern SPA patterns
|
|
577
|
+
const defaultBroadErrorSelector =
|
|
578
|
+
"p#alertMsg, #alertMsg, .alertMsg, .alert-danger, .alert-warning, " +
|
|
579
|
+
".has-error, .text-danger, #error-msg, .error-message, .err-msg, " +
|
|
580
|
+
".ui-messages-error, [class*='error-msg'], [class*='alert-message'], " +
|
|
581
|
+
".invalid-feedback, .help-block.error, .form-error, [aria-invalid='true'], " +
|
|
582
|
+
".err, span.err, p.err, div.err, [class*='err-'], [class*='-error'], " +
|
|
583
|
+
"[data-ng-bind*='ERR'], [data-ng-bind*='err'], [data-ng-show*='err'], " +
|
|
584
|
+
"[data-ng-show*='w_gst'], [data-ng-if*='captcha'], [data-ng-show*='captcha'], [role='alert'], " +
|
|
585
|
+
"[style*='color:red'], [style*='color: red']";
|
|
586
|
+
|
|
587
|
+
const customRules = cfg.errorDetection?.rules || [];
|
|
588
|
+
const ruleSelectors = customRules.map((r) => r.selector).filter(Boolean);
|
|
589
|
+
const combinedErrorSelectors = [
|
|
590
|
+
cfg.errorDetection?.selector,
|
|
591
|
+
...ruleSelectors,
|
|
592
|
+
defaultBroadErrorSelector,
|
|
593
|
+
]
|
|
594
|
+
.filter(Boolean)
|
|
595
|
+
.join(", ");
|
|
596
|
+
|
|
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;
|
|
610
|
+
|
|
611
|
+
// 1. Type captcha solution with robust candidate fallbacks & frame search
|
|
612
|
+
const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
|
|
613
|
+
const candidateInputSelectors = [
|
|
614
|
+
inputSelector,
|
|
615
|
+
"#fo-captcha",
|
|
616
|
+
"#captcha",
|
|
617
|
+
"input[name='cap']",
|
|
618
|
+
"input[name*='captcha' i]",
|
|
619
|
+
"input[id*='captcha' i]",
|
|
620
|
+
"input[placeholder*='captcha' i]",
|
|
621
|
+
].filter(Boolean);
|
|
622
|
+
|
|
623
|
+
const locatedInput = await locateElementInPageOrFrames(page, candidateInputSelectors, solveTimeout);
|
|
624
|
+
if (!locatedInput || !locatedInput.element) {
|
|
625
|
+
throw new Error(`Captcha input field not found with selector: ${inputSelector}`);
|
|
626
|
+
}
|
|
618
627
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
}
|
|
628
|
+
await locatedInput.element.click({ clickCount: 3 }).catch(() => { });
|
|
629
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
630
|
+
await locatedInput.element.type(String(captchaSolution).trim(), { delay: 35 });
|
|
650
631
|
|
|
651
|
-
|
|
632
|
+
// Sync input events and AngularJS ngModel for captcha input
|
|
633
|
+
await locatedInput.target.evaluate((sel) => {
|
|
634
|
+
const el = document.querySelector(sel);
|
|
635
|
+
if (el) {
|
|
636
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
637
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
638
|
+
el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
|
|
639
|
+
el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
|
|
640
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
652
641
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
642
|
+
if (window.angular) {
|
|
643
|
+
try {
|
|
644
|
+
const ngEl = window.angular.element(el);
|
|
645
|
+
const ngModel = ngEl.controller("ngModel");
|
|
646
|
+
if (ngModel) {
|
|
647
|
+
ngModel.$setViewValue(el.value);
|
|
648
|
+
ngModel.$render();
|
|
649
|
+
ngModel.$commitViewValue();
|
|
650
|
+
}
|
|
651
|
+
const scope = ngEl.scope();
|
|
652
|
+
if (scope && !scope.$$phase) scope.$apply();
|
|
653
|
+
} catch (_) { }
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}, locatedInput.matchedSelector).catch(() => { });
|
|
657
|
+
|
|
658
|
+
// 2. Click submit button with candidate fallbacks & frame search
|
|
659
|
+
const candidateSubmitSelectors = [
|
|
660
|
+
submitSelector,
|
|
661
|
+
"#lotsearch",
|
|
662
|
+
"button[type='submit']",
|
|
663
|
+
"input[type='submit']",
|
|
664
|
+
"button[id*='search' i]",
|
|
665
|
+
"button[id*='submit' i]",
|
|
666
|
+
"button.btn-primary",
|
|
667
|
+
].filter(Boolean);
|
|
668
|
+
|
|
669
|
+
const locatedSubmit = await locateElementInPageOrFrames(page, candidateSubmitSelectors, solveTimeout);
|
|
670
|
+
if (!locatedSubmit || !locatedSubmit.element) {
|
|
671
|
+
throw new Error(`Submit button not found with selector: ${submitSelector}`);
|
|
672
|
+
}
|
|
658
673
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
|
|
670
|
-
return "ERROR";
|
|
671
|
-
}
|
|
674
|
+
await locatedSubmit.element.click().catch(() => { });
|
|
675
|
+
// Also dispatch native click & submit events to ensure AngularJS/React/SPA form listeners fire
|
|
676
|
+
await locatedSubmit.target.evaluate((sel) => {
|
|
677
|
+
const btn = document.querySelector(sel);
|
|
678
|
+
if (btn) {
|
|
679
|
+
try { btn.click(); } catch (_) { }
|
|
680
|
+
if (btn.form) {
|
|
681
|
+
try {
|
|
682
|
+
btn.form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
|
|
683
|
+
} catch (_) { }
|
|
672
684
|
}
|
|
673
685
|
}
|
|
674
|
-
} catch
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
686
|
+
}, locatedSubmit.matchedSelector).catch(() => { });
|
|
687
|
+
|
|
688
|
+
// 3. Post-Submit Race: Wait for Success Selector OR Error Selector
|
|
689
|
+
// In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
|
|
690
|
+
const safetyTimeoutMs = preservePageForInspection
|
|
691
|
+
? Math.min(cfg.waitForAfterSubmit?.timeoutMs || 8000, 6000)
|
|
692
|
+
: (cfg.waitForAfterSubmit?.timeoutMs || 15000);
|
|
693
|
+
|
|
694
|
+
const matchedOutcome = await page
|
|
695
|
+
.waitForFunction(
|
|
696
|
+
(succ, err) => {
|
|
697
|
+
const isVisible = (el) => {
|
|
698
|
+
if (!el) return false;
|
|
699
|
+
const style = window.getComputedStyle(el);
|
|
700
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
701
|
+
const rects = el.getClientRects();
|
|
702
|
+
if (rects.length === 0) return false;
|
|
703
|
+
return rects[0].width > 0 || rects[0].height > 0;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const isRealError = (txt) => {
|
|
707
|
+
if (!txt || txt.length < 3) return false;
|
|
708
|
+
const lower = txt.trim().toLowerCase();
|
|
709
|
+
if (lower.startsWith("loading") || lower.startsWith("please wait") || lower.startsWith("processing")) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
};
|
|
694
714
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
715
|
+
// 1. Error selectors check (Must run FIRST to prevent false positives from generic table selectors)
|
|
716
|
+
try {
|
|
717
|
+
const elements = document.querySelectorAll(err);
|
|
718
|
+
for (const el of elements) {
|
|
719
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
720
|
+
if (isRealError(txt) && isVisible(el)) {
|
|
721
|
+
return "ERROR";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} catch (_) { }
|
|
703
725
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
726
|
+
// 2. Semantic error text scan across visible leaf elements
|
|
727
|
+
try {
|
|
728
|
+
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;
|
|
729
|
+
const candidates = document.querySelectorAll("span, p, div, label, font, b, strong");
|
|
730
|
+
for (const c of candidates) {
|
|
731
|
+
if (c.children.length === 0 || c.tagName === "P" || c.tagName === "SPAN") {
|
|
732
|
+
const txt = (c.innerText || c.textContent || "").trim();
|
|
733
|
+
if (txt.length >= 5 && txt.length < 250 && errorRegex.test(txt) && isRealError(txt)) {
|
|
734
|
+
if (isVisible(c)) {
|
|
735
|
+
return "ERROR";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} catch (_) { }
|
|
711
741
|
|
|
712
|
-
|
|
713
|
-
|
|
742
|
+
// 3. Success check: must be genuinely visible and contain substantive data
|
|
743
|
+
try {
|
|
744
|
+
const successElements = document.querySelectorAll(succ);
|
|
745
|
+
for (const el of successElements) {
|
|
746
|
+
if (isVisible(el)) {
|
|
747
|
+
// Ignore empty 1-cell layout tables like captcha wrapper tables
|
|
748
|
+
if (el.tagName === "TABLE" && (el.innerText || "").trim().length < 15) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
return "SUCCESS";
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} catch (_) { }
|
|
714
755
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
756
|
+
return null;
|
|
757
|
+
},
|
|
758
|
+
{ timeout: safetyTimeoutMs },
|
|
759
|
+
successSelector,
|
|
760
|
+
combinedErrorSelectors
|
|
761
|
+
)
|
|
762
|
+
.then((handle) => handle.jsonValue())
|
|
763
|
+
.catch(() => "TIMEOUT");
|
|
764
|
+
|
|
765
|
+
// Case A: Success Result Page
|
|
766
|
+
if (matchedOutcome === "SUCCESS") {
|
|
767
|
+
const raw = {};
|
|
768
|
+
for (const mapping of mappings) {
|
|
769
|
+
let val = null;
|
|
770
|
+
const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
|
|
771
|
+
for (const strat of strategies) {
|
|
772
|
+
val = await extractBrowserField(page, strat);
|
|
773
|
+
if (val !== null && val !== undefined && val !== "") break;
|
|
774
|
+
}
|
|
725
775
|
|
|
726
|
-
|
|
727
|
-
|
|
776
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
777
|
+
val = runTransformPipeline(val, mapping.transforms);
|
|
778
|
+
}
|
|
779
|
+
if (mapping.type) {
|
|
780
|
+
val = castValueToType(val, mapping.type);
|
|
781
|
+
}
|
|
782
|
+
raw[mapping.field || mapping.key] = val;
|
|
783
|
+
}
|
|
728
784
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
785
|
+
if (preservePageForInspection) {
|
|
786
|
+
// Keep the page alive for visual inspection & testing
|
|
787
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
788
|
+
const inspectTtl = 300; // 5 minutes for interactive element picking
|
|
789
|
+
session.timeoutTimer = setTimeout(() => {
|
|
790
|
+
purgeSession(sessionId).catch(() => { });
|
|
791
|
+
}, inspectTtl * 1000);
|
|
792
|
+
|
|
793
|
+
// Settle time for tables & styles
|
|
794
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
795
|
+
|
|
796
|
+
const snap = await captureInspectionSnapshot(page);
|
|
797
|
+
return {
|
|
798
|
+
status: "COMPLETED",
|
|
799
|
+
success: true,
|
|
800
|
+
data: raw,
|
|
801
|
+
...snap,
|
|
802
|
+
sessionId,
|
|
803
|
+
isInspectionMode: true,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
735
806
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
let errorText = "";
|
|
807
|
+
// Clean up session and close page
|
|
808
|
+
await purgeSession(sessionId);
|
|
739
809
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
}
|
|
810
|
+
return {
|
|
811
|
+
status: "COMPLETED",
|
|
812
|
+
success: true,
|
|
813
|
+
data: raw,
|
|
814
|
+
};
|
|
755
815
|
}
|
|
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
816
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (txt) {
|
|
817
|
+
// Evaluate error rules and messages if matchedOutcome === "ERROR" or if we are inspecting or if timed out
|
|
818
|
+
let matchedRule = null;
|
|
819
|
+
let errorText = "";
|
|
820
|
+
|
|
821
|
+
const checkRulesAndText = await page.evaluate((rules, errorSelector) => {
|
|
822
|
+
const isVisible = (el) => {
|
|
823
|
+
if (!el) return false;
|
|
775
824
|
const style = window.getComputedStyle(el);
|
|
776
|
-
if (style.display
|
|
777
|
-
|
|
825
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
826
|
+
const rects = el.getClientRects();
|
|
827
|
+
if (rects.length === 0) return false;
|
|
828
|
+
return rects[0].width > 0 || rects[0].height > 0;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const isRealError = (txt) => {
|
|
832
|
+
if (!txt || txt.length < 3) return false;
|
|
833
|
+
const lower = txt.trim().toLowerCase();
|
|
834
|
+
if (lower.startsWith("loading") || lower.startsWith("please wait") || lower.startsWith("processing")) {
|
|
835
|
+
return false;
|
|
778
836
|
}
|
|
837
|
+
return true;
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// 1. Check custom rules with rule.selector
|
|
841
|
+
for (const rule of rules) {
|
|
842
|
+
if (!rule.selector) continue;
|
|
843
|
+
try {
|
|
844
|
+
const els = document.querySelectorAll(rule.selector);
|
|
845
|
+
for (const el of els) {
|
|
846
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
847
|
+
if (isRealError(txt) && isVisible(el)) {
|
|
848
|
+
if (!rule.pattern) return { matchedRuleId: rule.id, text: txt };
|
|
849
|
+
const re = new RegExp(rule.pattern, "i");
|
|
850
|
+
if (re.test(txt)) return { matchedRuleId: rule.id, text: txt };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch (_) { }
|
|
779
854
|
}
|
|
780
|
-
}
|
|
781
|
-
} catch (e) {}
|
|
782
|
-
return "";
|
|
783
|
-
}, combinedErrorSelectors);
|
|
784
|
-
}
|
|
785
855
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
856
|
+
// 2. Extract visible text from combinedErrorSelectors
|
|
857
|
+
let foundText = "";
|
|
858
|
+
try {
|
|
859
|
+
const els = document.querySelectorAll(errorSelector);
|
|
860
|
+
for (const el of els) {
|
|
861
|
+
const txt = (el.innerText || el.textContent || "").trim();
|
|
862
|
+
if (isRealError(txt) && isVisible(el)) {
|
|
863
|
+
foundText = txt;
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch (_) { }
|
|
868
|
+
|
|
869
|
+
// 3. Semantic fallback if foundText is still empty
|
|
870
|
+
if (!foundText) {
|
|
871
|
+
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;
|
|
872
|
+
const candidates = document.querySelectorAll("span, p, div, label, font, b, strong");
|
|
873
|
+
for (const c of candidates) {
|
|
874
|
+
if (c.children.length === 0 || c.tagName === "P" || c.tagName === "SPAN") {
|
|
875
|
+
const txt = (c.innerText || c.textContent || "").trim();
|
|
876
|
+
if (txt.length >= 5 && txt.length < 250 && errorRegex.test(txt) && isRealError(txt)) {
|
|
877
|
+
if (isVisible(c)) {
|
|
878
|
+
foundText = txt;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
790
885
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
}
|
|
886
|
+
// 4. If we found text, check if any custom rule matches this text by pattern (even if rule.selector didn't match!)
|
|
887
|
+
let fallbackRuleId = null;
|
|
888
|
+
if (foundText) {
|
|
889
|
+
for (const rule of rules) {
|
|
890
|
+
if (rule.pattern) {
|
|
891
|
+
try {
|
|
892
|
+
const re = new RegExp(rule.pattern, "i");
|
|
893
|
+
if (re.test(foundText)) {
|
|
894
|
+
fallbackRuleId = rule.id;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
} catch (_) { }
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
828
901
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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 (_) { }
|
|
846
929
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
|
|
854
|
-
canRetry: false,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
930
|
+
return {
|
|
931
|
+
matchedRuleId: fallbackRuleId,
|
|
932
|
+
text: foundText,
|
|
933
|
+
hasDomCaptchaError,
|
|
934
|
+
};
|
|
935
|
+
}, customRules, combinedErrorSelectors);
|
|
857
936
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
937
|
+
if (checkRulesAndText && checkRulesAndText.matchedRuleId) {
|
|
938
|
+
matchedRule = customRules.find((r) => r.id === checkRulesAndText.matchedRuleId) || null;
|
|
939
|
+
}
|
|
940
|
+
errorText = (checkRulesAndText && checkRulesAndText.text) || "";
|
|
941
|
+
|
|
942
|
+
const regex = new RegExp(invalidPattern, "i");
|
|
943
|
+
const isCaptchaError =
|
|
944
|
+
Boolean(checkRulesAndText?.hasDomCaptchaError) ||
|
|
945
|
+
(matchedRule ? matchedRule.action === "RETRY_CAPTCHA" : false) ||
|
|
946
|
+
(Boolean(errorText) && regex.test(errorText));
|
|
947
|
+
|
|
948
|
+
// If captcha error occurred, give target portal a brief window to load the refreshed captcha image
|
|
949
|
+
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
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
|
+
}
|
|
867
968
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
969
|
+
try {
|
|
970
|
+
const newCaptchaEl = await page.$(imageSelector);
|
|
971
|
+
if (newCaptchaEl) {
|
|
972
|
+
const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
|
|
973
|
+
newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
|
|
974
|
+
}
|
|
975
|
+
} catch (e) { }
|
|
976
|
+
|
|
977
|
+
// CRITICAL FOR INSPECTOR: In inspection mode, NEVER purge the session on errors or timeout!
|
|
978
|
+
// Always snap the live DOM and return it so the user can inspect the red text and configure selectors
|
|
979
|
+
if (preservePageForInspection) {
|
|
980
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
981
|
+
const inspectTtl = 300;
|
|
982
|
+
session.timeoutTimer = setTimeout(() => {
|
|
983
|
+
purgeSession(sessionId).catch(() => { });
|
|
984
|
+
}, inspectTtl * 1000);
|
|
985
|
+
|
|
986
|
+
const snap = await captureInspectionSnapshot(page);
|
|
987
|
+
const effectiveStatus = isCaptchaError
|
|
988
|
+
? "INVALID_CAPTCHA"
|
|
989
|
+
: (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
status: effectiveStatus,
|
|
993
|
+
success: true, // Allows frontend to render the inspection screenshot
|
|
994
|
+
errorCode: (matchedRule && matchedRule.id) || (isCaptchaError ? "INVALID_CAPTCHA" : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE")),
|
|
995
|
+
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."),
|
|
996
|
+
newImageBase64,
|
|
997
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
998
|
+
sessionId,
|
|
999
|
+
isInspectionMode: true,
|
|
1000
|
+
...snap,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
894
1003
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1004
|
+
// Runtime non-inspection mode:
|
|
1005
|
+
if (isCaptchaError) {
|
|
1006
|
+
// For captcha errors: NEVER abort session! Keep the browser session alive and reset TTL timer
|
|
1007
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
1008
|
+
const retryTtlSeconds = cfg.sessionTimeoutSeconds || 180;
|
|
1009
|
+
session.timeoutTimer = setTimeout(() => {
|
|
1010
|
+
purgeSession(sessionId).catch(() => { });
|
|
1011
|
+
}, retryTtlSeconds * 1000);
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
status: "INVALID_CAPTCHA",
|
|
1015
|
+
success: false,
|
|
1016
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect. Please retry with the updated image.",
|
|
1017
|
+
canRetry: true,
|
|
1018
|
+
newImageBase64,
|
|
1019
|
+
errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
|
|
1020
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
901
1023
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1024
|
+
if (errorText || matchedRule) {
|
|
1025
|
+
// For business errors (e.g. invalid GSTIN, not found): abort and purge session immediately
|
|
1026
|
+
await purgeSession(sessionId);
|
|
1027
|
+
return {
|
|
1028
|
+
status: "BUSINESS_ERROR",
|
|
1029
|
+
success: false,
|
|
1030
|
+
errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
|
|
1031
|
+
message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
|
|
1032
|
+
canRetry: false,
|
|
1033
|
+
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
905
1036
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1037
|
+
// Runtime Timeout
|
|
1038
|
+
await purgeSession(sessionId);
|
|
1039
|
+
return {
|
|
1040
|
+
status: "TIMEOUT",
|
|
1041
|
+
success: false,
|
|
1042
|
+
message: "Target portal did not respond with results within the expected time window.",
|
|
1043
|
+
canRetry: false,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
911
1046
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* Capture full inspection snapshot helper
|
|
1049
|
+
*/
|
|
1050
|
+
async function captureInspectionSnapshot(page) {
|
|
1051
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1052
|
+
const screenshotBuffer = await page.screenshot({
|
|
1053
|
+
encoding: "binary",
|
|
1054
|
+
fullPage: false,
|
|
1055
|
+
type: "jpeg",
|
|
1056
|
+
quality: 85,
|
|
1057
|
+
});
|
|
1058
|
+
const screenshotBase64 = `data:image/jpeg;base64,${Buffer.from(screenshotBuffer).toString("base64")}`;
|
|
1059
|
+
const title = await page.title().catch(() => "");
|
|
1060
|
+
const dimensions = await page
|
|
1061
|
+
.evaluate(() => ({
|
|
1062
|
+
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
1063
|
+
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
1064
|
+
}))
|
|
1065
|
+
.catch(() => ({ width: 1366, height: 900 }));
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
screenshotBase64,
|
|
1069
|
+
dimensions,
|
|
1070
|
+
title: title || "Inspection Page",
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
919
1073
|
|
|
920
|
-
/**
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (sessionId) {
|
|
927
|
-
await purgeSession(sessionId);
|
|
928
|
-
}
|
|
929
|
-
return { success: true, aborted: true };
|
|
930
|
-
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Force capture snapshot of the active interactive session's page
|
|
1076
|
+
*/
|
|
1077
|
+
export async function handleInteractiveSessionSnapshot(payload) {
|
|
1078
|
+
const { sessionId } = payload;
|
|
1079
|
+
const session = activeSessions.get(sessionId);
|
|
931
1080
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
*/
|
|
936
|
-
export async function handleInteractiveSessionExtract(payload) {
|
|
937
|
-
const { sessionId, fieldMappings = [], collections = [] } = payload;
|
|
938
|
-
const session = activeSessions.get(sessionId);
|
|
1081
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
1082
|
+
throw new Error("Interactive session not found or already expired");
|
|
1083
|
+
}
|
|
939
1084
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1085
|
+
const { page } = session;
|
|
1086
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
1087
|
+
session.timeoutTimer = setTimeout(() => {
|
|
1088
|
+
purgeSession(sessionId).catch(() => { });
|
|
1089
|
+
}, 300 * 1000);
|
|
1090
|
+
|
|
1091
|
+
const snap = await captureInspectionSnapshot(page);
|
|
1092
|
+
return {
|
|
1093
|
+
success: true,
|
|
1094
|
+
sessionId,
|
|
1095
|
+
...snap,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
943
1098
|
|
|
944
|
-
|
|
945
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* 4. Handles HITL_SESSION_ABORT:
|
|
1101
|
+
* Explicit client abort or session abandonment.
|
|
1102
|
+
*/
|
|
1103
|
+
export async function handleInteractiveSessionAbort(payload) {
|
|
1104
|
+
const { sessionId } = payload;
|
|
1105
|
+
if (sessionId) {
|
|
1106
|
+
await purgeSession(sessionId);
|
|
1107
|
+
}
|
|
1108
|
+
return { success: true, aborted: true };
|
|
1109
|
+
}
|
|
946
1110
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1111
|
+
/**
|
|
1112
|
+
* 5. Handles HITL_SESSION_EXTRACT:
|
|
1113
|
+
* Extracts fields directly from an active interactive session's live page.
|
|
1114
|
+
*/
|
|
1115
|
+
export async function handleInteractiveSessionExtract(payload) {
|
|
1116
|
+
const { sessionId, fieldMappings = [], collections = [] } = payload;
|
|
1117
|
+
const session = activeSessions.get(sessionId);
|
|
954
1118
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (mapping.type) {
|
|
959
|
-
val = castValueToType(val, mapping.type);
|
|
960
|
-
}
|
|
961
|
-
raw[mapping.field || mapping.key] = val;
|
|
962
|
-
}
|
|
1119
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
1120
|
+
throw new Error("Interactive session not found or already expired");
|
|
1121
|
+
}
|
|
963
1122
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1123
|
+
const { page } = session;
|
|
1124
|
+
const raw = {};
|
|
1125
|
+
|
|
1126
|
+
for (const mapping of fieldMappings) {
|
|
1127
|
+
let val = null;
|
|
1128
|
+
const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
|
|
1129
|
+
for (const strat of strategies) {
|
|
1130
|
+
val = await extractBrowserField(page, strat);
|
|
1131
|
+
if (val !== null && val !== undefined && val !== "") break;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
1135
|
+
val = runTransformPipeline(val, mapping.transforms);
|
|
1136
|
+
}
|
|
1137
|
+
if (mapping.type) {
|
|
1138
|
+
val = castValueToType(val, mapping.type);
|
|
1139
|
+
}
|
|
1140
|
+
raw[mapping.field || mapping.key] = val;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return {
|
|
1144
|
+
success: true,
|
|
1145
|
+
data: raw,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
969
1148
|
|