app-manager-edge-worker 1.3.0 → 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.
- 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 +841 -535
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import localBrowserManager from "../browser.js";
|
|
3
3
|
import { runTransformPipeline, castValueToType } from "./transformUtils.js";
|
|
4
|
+
import { logInfo, logWarn, logError } from "../banner.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* In-memory map of active interactive sessions on this Edge Worker.
|
|
@@ -86,13 +87,89 @@ async function purgeSession(sessionId) {
|
|
|
86
87
|
|
|
87
88
|
try {
|
|
88
89
|
if (session.page && !session.page.isClosed()) {
|
|
89
|
-
await session.page.close().catch(() => {});
|
|
90
|
+
await session.page.close().catch(() => { });
|
|
90
91
|
}
|
|
91
|
-
} catch (_) {}
|
|
92
|
+
} catch (_) { }
|
|
92
93
|
|
|
93
94
|
activeSessions.delete(sessionId);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Constructs a robust set of CSS selectors for an input field based on its configured selector and paramKey.
|
|
99
|
+
* Handles IDs, name attributes, ng-model attributes, placeholders, and comma-separated selectors.
|
|
100
|
+
*/
|
|
101
|
+
function buildRobustInputSelectors(selector, paramKey) {
|
|
102
|
+
const set = new Set();
|
|
103
|
+
if (selector && typeof selector === "string") {
|
|
104
|
+
selector.split(",").map((s) => s.trim()).filter(Boolean).forEach((s) => set.add(s));
|
|
105
|
+
|
|
106
|
+
// If selector contains [name="xxx"] or [name='xxx'], extract the name
|
|
107
|
+
const nameMatch = selector.match(/\[name=["']?([^"'\]\s]+)["']?\]/i);
|
|
108
|
+
if (nameMatch && nameMatch[1]) {
|
|
109
|
+
const name = nameMatch[1];
|
|
110
|
+
set.add(`#${name}`);
|
|
111
|
+
set.add(`input[name="${name}"]`);
|
|
112
|
+
set.add(`[name="${name}"]`);
|
|
113
|
+
set.add(`input#${name}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If selector contains #xxx or [id="xxx"], extract the id
|
|
117
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_\-]+)/) || selector.match(/\[id=["']?([^"'\]\s]+)["']?\]/i);
|
|
118
|
+
if (idMatch && idMatch[1]) {
|
|
119
|
+
const id = idMatch[1];
|
|
120
|
+
set.add(`#${id}`);
|
|
121
|
+
set.add(`input[name="${id}"]`);
|
|
122
|
+
set.add(`[name="${id}"]`);
|
|
123
|
+
set.add(`input#${id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (paramKey && typeof paramKey === "string") {
|
|
128
|
+
const pk = paramKey.trim();
|
|
129
|
+
set.add(`#${pk}`);
|
|
130
|
+
set.add(`input[name="${pk}"]`);
|
|
131
|
+
set.add(`[name="${pk}"]`);
|
|
132
|
+
set.add(`input[data-ng-model*="${pk}"]`);
|
|
133
|
+
set.add(`input[ng-model*="${pk}"]`);
|
|
134
|
+
set.add(`input[placeholder*="${pk}" i]`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Array.from(set);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Searches for an element in the main page or any child iframe, waiting up to timeoutMs.
|
|
142
|
+
* Returns { target: page | frame, element, matchedSelector } or null.
|
|
143
|
+
*/
|
|
144
|
+
async function locateElementInPageOrFrames(page, selectorList, timeoutMs = 15000) {
|
|
145
|
+
const combinedSelector = Array.isArray(selectorList) ? selectorList.join(", ") : String(selectorList);
|
|
146
|
+
|
|
147
|
+
// 1. Try waiting on the top page first
|
|
148
|
+
try {
|
|
149
|
+
const el = await page.waitForSelector(combinedSelector, { timeout: timeoutMs });
|
|
150
|
+
if (el) {
|
|
151
|
+
return { target: page, element: el, matchedSelector: combinedSelector };
|
|
152
|
+
}
|
|
153
|
+
} catch (_) {
|
|
154
|
+
// Top-level wait timed out or failed; inspect child frames next
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Scan child frames/iframes if not found on top-level page
|
|
158
|
+
try {
|
|
159
|
+
const frames = page.frames();
|
|
160
|
+
for (const frame of frames) {
|
|
161
|
+
try {
|
|
162
|
+
const fEl = await frame.$(combinedSelector);
|
|
163
|
+
if (fEl) {
|
|
164
|
+
return { target: frame, element: fEl, matchedSelector: combinedSelector };
|
|
165
|
+
}
|
|
166
|
+
} catch (_) { }
|
|
167
|
+
}
|
|
168
|
+
} catch (_) { }
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
96
173
|
/**
|
|
97
174
|
* 1. Handles HITL_SESSION_INIT:
|
|
98
175
|
* Launches page, pre-fills initial form inputs, captures captcha screenshot,
|
|
@@ -106,6 +183,7 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
106
183
|
captchaConfig = {},
|
|
107
184
|
preScrapeActions = [],
|
|
108
185
|
authentication = null,
|
|
186
|
+
navigation = {},
|
|
109
187
|
timeoutMs = 30000,
|
|
110
188
|
fieldMappings = [],
|
|
111
189
|
collections = [],
|
|
@@ -139,32 +217,36 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
139
217
|
path: c.path || "/",
|
|
140
218
|
}))
|
|
141
219
|
)
|
|
142
|
-
.catch(() => {});
|
|
220
|
+
.catch(() => { });
|
|
143
221
|
}
|
|
144
222
|
if (authentication.type === "HEADERS" && authentication.headers) {
|
|
145
|
-
await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
|
|
223
|
+
await page.setExtraHTTPHeaders(authentication.headers).catch(() => { });
|
|
146
224
|
}
|
|
147
225
|
}
|
|
148
226
|
|
|
149
|
-
// Navigate to target portal
|
|
150
|
-
|
|
227
|
+
// Navigate to target portal with flexible waitUntil and realistic timeout
|
|
228
|
+
const navTimeout = navigation.timeoutMs || timeoutMs || 30000;
|
|
229
|
+
const navWaitUntil = navigation.waitUntil || "domcontentloaded";
|
|
230
|
+
|
|
231
|
+
await page.goto(url, { waitUntil: navWaitUntil, timeout: navTimeout });
|
|
151
232
|
try {
|
|
152
|
-
await page.waitForNetworkIdle({ idleTime: 500, timeout:
|
|
153
|
-
} catch (_) {}
|
|
233
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 4000 }).catch(() => { });
|
|
234
|
+
} catch (_) { }
|
|
154
235
|
|
|
155
236
|
// Execute pre-scrape actions if present
|
|
156
237
|
if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
|
|
157
238
|
for (const action of preScrapeActions) {
|
|
158
239
|
try {
|
|
159
240
|
const actType = (action.type || action.actionType || "").toUpperCase();
|
|
241
|
+
const actionTimeout = Math.max(action.timeoutMs || 0, 10000);
|
|
160
242
|
if (actType === "CLICK" && action.selector) {
|
|
161
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
243
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
162
244
|
await page.click(action.selector);
|
|
163
245
|
} else if (actType === "TYPE" && action.selector) {
|
|
164
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
246
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
165
247
|
await page.type(action.selector, action.value || "", { delay: 30 });
|
|
166
248
|
} else if (actType === "WAIT_FOR" && action.selector) {
|
|
167
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
249
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
168
250
|
}
|
|
169
251
|
if (action.delayMs > 0) {
|
|
170
252
|
await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 4000)));
|
|
@@ -175,597 +257,821 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
175
257
|
}
|
|
176
258
|
}
|
|
177
259
|
|
|
178
|
-
// Fill Form Inputs (e.g. GSTIN, PAN, registration number)
|
|
260
|
+
// Fill Form Inputs (e.g. GSTIN, PAN, registration number) with robust selector fallbacks & frame support
|
|
179
261
|
if (Array.isArray(formInputs) && formInputs.length > 0) {
|
|
180
262
|
for (const input of formInputs) {
|
|
181
263
|
if (!input.selector || input.value === undefined || input.value === null) continue;
|
|
182
|
-
|
|
264
|
+
|
|
265
|
+
const inputTimeout = Math.max(input.timeoutMs || 0, timeoutMs || 30000, 20000);
|
|
266
|
+
const robustSelectors = buildRobustInputSelectors(input.selector, input.paramKey);
|
|
267
|
+
|
|
268
|
+
const located = await locateElementInPageOrFrames(page, robustSelectors, inputTimeout);
|
|
269
|
+
if (!located || !located.element) {
|
|
270
|
+
const currentUrl = page.url();
|
|
271
|
+
const pageTitle = (await page.title().catch(() => "")) || "Untitled";
|
|
272
|
+
logWarn(
|
|
273
|
+
`[HITL_SESSION_INIT] Input selector '${input.selector}' (or fallbacks: ${robustSelectors.join(", ")}) not found after ${inputTimeout}ms on page "${pageTitle}" (${currentUrl})`
|
|
274
|
+
);
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Waiting for selector '${input.selector}' failed on page "${pageTitle}" (${currentUrl}) after ${inputTimeout}ms. Ensure portal is reachable and selector is correct.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const { target, element, matchedSelector } = located;
|
|
183
281
|
|
|
184
282
|
if (input.clearFirst !== false) {
|
|
185
|
-
await
|
|
186
|
-
await page.keyboard.press("Backspace").catch(() => {});
|
|
283
|
+
await element.click({ clickCount: 3 }).catch(() => { });
|
|
284
|
+
await page.keyboard.press("Backspace").catch(() => { });
|
|
187
285
|
}
|
|
188
286
|
|
|
189
287
|
if (input.actionType === "SELECT") {
|
|
190
|
-
await
|
|
288
|
+
await target.select(matchedSelector, String(input.value));
|
|
191
289
|
} else {
|
|
192
|
-
await
|
|
290
|
+
await element.type(String(input.value), { delay: input.delayMs || 30 });
|
|
193
291
|
}
|
|
194
292
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}, matchedSelector).catch(() => { });
|
|
205
321
|
|
|
206
|
-
await
|
|
322
|
+
await page.keyboard.press("Tab").catch(() => { });
|
|
323
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
207
324
|
}
|
|
208
325
|
}
|
|
209
326
|
|
|
210
|
-
// Locate and Screenshot Captcha DOM Element
|
|
211
|
-
const
|
|
212
|
-
|
|
327
|
+
// Locate and Screenshot Captcha DOM Element with robust candidate fallbacks
|
|
328
|
+
const imageTimeout = Math.max(captchaConfig.timeoutMs || 0, timeoutMs || 30000, 15000);
|
|
329
|
+
const candidateImageSelectors = [
|
|
330
|
+
captchaConfig.imageSelector,
|
|
331
|
+
"#imgCaptcha",
|
|
332
|
+
"#captchaImg",
|
|
333
|
+
"img[src*='captcha' i]",
|
|
334
|
+
"img[id*='captcha' i]",
|
|
335
|
+
"img[alt*='captcha' i]",
|
|
336
|
+
"canvas[id*='captcha' i]",
|
|
337
|
+
".captcha-image img",
|
|
338
|
+
"#captcha_image",
|
|
339
|
+
].filter(Boolean);
|
|
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
|
+
}
|
|
213
349
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
});
|
|
218
395
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
encoding: "binary",
|
|
232
|
-
fullPage: false,
|
|
233
|
-
type: "jpeg",
|
|
234
|
-
quality: 85,
|
|
235
|
-
});
|
|
236
|
-
const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
|
|
237
|
-
const title = (await page.title().catch(() => "")) || "Interactive Portal Session";
|
|
238
|
-
const dimensions = await page
|
|
239
|
-
.evaluate(() => ({
|
|
240
|
-
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
241
|
-
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
242
|
-
}))
|
|
243
|
-
.catch(() => ({ width: 1366, height: 900 }));
|
|
244
|
-
|
|
245
|
-
const sessionTtlSeconds = captchaConfig.sessionTimeoutSeconds || 90;
|
|
246
|
-
|
|
247
|
-
// Set auto-purge timer to prevent resource leaks
|
|
248
|
-
const timeoutTimer = setTimeout(() => {
|
|
249
|
-
purgeSession(sessionId).catch(() => {});
|
|
250
|
-
}, sessionTtlSeconds * 1000);
|
|
251
|
-
|
|
252
|
-
// Store active session in memory
|
|
253
|
-
activeSessions.set(sessionId, {
|
|
254
|
-
page,
|
|
255
|
-
timeoutTimer,
|
|
256
|
-
createdAt: Date.now(),
|
|
257
|
-
url,
|
|
258
|
-
captchaConfig,
|
|
259
|
-
fieldMappings,
|
|
260
|
-
collections,
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
sessionId,
|
|
265
|
-
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
266
|
-
pageScreenshotBase64,
|
|
267
|
-
dimensions,
|
|
268
|
-
title,
|
|
269
|
-
expiresInSeconds: sessionTtlSeconds,
|
|
270
|
-
};
|
|
271
|
-
} catch (err) {
|
|
272
|
-
await page.close().catch(() => {});
|
|
273
|
-
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
|
+
}
|
|
274
408
|
}
|
|
275
|
-
}
|
|
276
409
|
|
|
277
410
|
/**
|
|
278
411
|
* 2. Handles HITL_SESSION_REFRESH:
|
|
279
412
|
* Clicks the refresh button and verifies the visual MD5 hash of the captcha changed.
|
|
280
413
|
*/
|
|
281
414
|
export async function handleInteractiveSessionRefresh(payload) {
|
|
282
|
-
|
|
283
|
-
|
|
415
|
+
const { sessionId, captchaConfig = {} } = payload;
|
|
416
|
+
const session = activeSessions.get(sessionId);
|
|
284
417
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
418
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
419
|
+
throw new Error("Interactive session not found or already expired");
|
|
420
|
+
}
|
|
288
421
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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;
|
|
293
442
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (!oldEl) {
|
|
297
|
-
throw new Error(`Captcha element not found with selector: ${imageSelector}`);
|
|
298
|
-
}
|
|
443
|
+
const oldBuffer = await oldEl.screenshot({ encoding: "binary" });
|
|
444
|
+
const oldHash = crypto.createHash("md5").update(oldBuffer).digest("hex");
|
|
299
445
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
} catch (_) {}
|
|
338
|
-
}
|
|
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
|
+
}
|
|
339
482
|
|
|
340
483
|
if (!clicked) {
|
|
341
|
-
|
|
342
|
-
|
|
484
|
+
throw new Error(`Captcha refresh button not found on page (tried: ${refreshSelector || "standard reload selectors"})`);
|
|
485
|
+
}
|
|
343
486
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
516
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
517
|
+
if (!newBuffer) {
|
|
518
|
+
newBuffer = await oldEl.screenshot({ encoding: "binary" });
|
|
519
|
+
}
|
|
377
520
|
|
|
378
|
-
|
|
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
|
+
}
|
|
379
538
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
}
|
|
388
559
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|
|
395
608
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
* If captcha is invalid, keeps session alive and snaps new captcha for retry.
|
|
400
|
-
* If success, extracts data and closes page.
|
|
401
|
-
*/
|
|
402
|
-
export async function handleInteractiveSessionSolve(payload) {
|
|
403
|
-
const {
|
|
404
|
-
sessionId,
|
|
405
|
-
captchaSolution,
|
|
406
|
-
captchaConfig = {},
|
|
407
|
-
fieldMappings = [],
|
|
408
|
-
collections = [],
|
|
409
|
-
preservePageForInspection = false,
|
|
410
|
-
} = payload;
|
|
411
|
-
const session = activeSessions.get(sessionId);
|
|
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 });
|
|
412
612
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 }));
|
|
416
622
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
".has-error, .text-danger, #error-msg, .error-message, .err-msg, " +
|
|
430
|
-
".ui-messages-error, [class*='error-msg'], [class*='alert-message'], " +
|
|
431
|
-
".invalid-feedback, .help-block.error, .form-error, [aria-invalid='true']";
|
|
432
|
-
|
|
433
|
-
const customRules = cfg.errorDetection?.rules || [];
|
|
434
|
-
const ruleSelectors = customRules.map((r) => r.selector).filter(Boolean);
|
|
435
|
-
const combinedErrorSelectors = [
|
|
436
|
-
cfg.errorDetection?.selector,
|
|
437
|
-
...ruleSelectors,
|
|
438
|
-
defaultBroadErrorSelector,
|
|
439
|
-
]
|
|
440
|
-
.filter(Boolean)
|
|
441
|
-
.join(", ");
|
|
442
|
-
|
|
443
|
-
const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
|
|
444
|
-
|
|
445
|
-
// 1. Type captcha solution
|
|
446
|
-
await page.waitForSelector(inputSelector, { timeout: 4000 });
|
|
447
|
-
await page.click(inputSelector, { clickCount: 3 }).catch(() => {});
|
|
448
|
-
await page.keyboard.press("Backspace").catch(() => {});
|
|
449
|
-
await page.type(inputSelector, String(captchaSolution).trim(), { delay: 35 });
|
|
450
|
-
|
|
451
|
-
// 2. Click submit button
|
|
452
|
-
await page.waitForSelector(submitSelector, { timeout: 4000 });
|
|
453
|
-
await page.click(submitSelector);
|
|
454
|
-
|
|
455
|
-
// 3. Post-Submit Race: Wait for Success Selector OR Error Selector
|
|
456
|
-
// In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
|
|
457
|
-
const safetyTimeoutMs = preservePageForInspection
|
|
458
|
-
? Math.min(cfg.waitForAfterSubmit?.timeoutMs || 8000, 6000)
|
|
459
|
-
: (cfg.waitForAfterSubmit?.timeoutMs || 15000);
|
|
460
|
-
|
|
461
|
-
const matchedOutcome = await page
|
|
462
|
-
.waitForFunction(
|
|
463
|
-
(succ, err) => {
|
|
464
|
-
if (document.querySelector(succ)) return "SUCCESS";
|
|
465
|
-
try {
|
|
466
|
-
const elements = document.querySelectorAll(err);
|
|
467
|
-
for (const el of elements) {
|
|
468
|
-
const txt = (el.innerText || el.textContent || "").trim();
|
|
469
|
-
if (txt.length > 0) {
|
|
470
|
-
const style = window.getComputedStyle(el);
|
|
471
|
-
if (style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0") {
|
|
472
|
-
return "ERROR";
|
|
473
|
-
}
|
|
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 (_) { }
|
|
474
635
|
}
|
|
475
636
|
}
|
|
476
|
-
} catch
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
val = await extractBrowserField(page, strat);
|
|
494
|
-
if (val !== null && val !== undefined && val !== "") break;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
498
|
-
val = runTransformPipeline(val, mapping.transforms);
|
|
499
|
-
}
|
|
500
|
-
if (mapping.type) {
|
|
501
|
-
val = castValueToType(val, mapping.type);
|
|
502
|
-
}
|
|
503
|
-
raw[mapping.field || mapping.key] = val;
|
|
504
|
-
}
|
|
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
|
+
}
|
|
505
654
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 (_) { }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
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
|
+
};
|
|
695
|
+
|
|
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 (_) { }
|
|
706
|
+
|
|
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 (_) { }
|
|
722
|
+
|
|
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 (_) { }
|
|
736
|
+
|
|
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
|
+
}
|
|
513
756
|
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
}
|
|
516
765
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
787
|
|
|
528
|
-
|
|
529
|
-
|
|
788
|
+
// Clean up session and close page
|
|
789
|
+
await purgeSession(sessionId);
|
|
530
790
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
791
|
+
return {
|
|
792
|
+
status: "COMPLETED",
|
|
793
|
+
success: true,
|
|
794
|
+
data: raw,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
537
797
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 = "";
|
|
541
801
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const check = await page.evaluate((sel, pat) => {
|
|
546
|
-
try {
|
|
547
|
-
const els = document.querySelectorAll(sel);
|
|
548
|
-
for (const el of els) {
|
|
549
|
-
const txt = (el.innerText || el.textContent || "").trim();
|
|
550
|
-
if (txt) {
|
|
802
|
+
const checkRulesAndText = await page.evaluate((rules, errorSelector) => {
|
|
803
|
+
const isVisible = (el) => {
|
|
804
|
+
if (!el) return false;
|
|
551
805
|
const style = window.getComputedStyle(el);
|
|
552
|
-
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
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 (_) { }
|
|
556
835
|
}
|
|
557
|
-
}
|
|
558
|
-
} catch (e) {}
|
|
559
|
-
return null;
|
|
560
|
-
}, rule.selector, rule.pattern);
|
|
561
|
-
|
|
562
|
-
if (check && check.matched) {
|
|
563
|
-
matchedRule = rule;
|
|
564
|
-
errorText = check.text;
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
836
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
580
864
|
}
|
|
581
865
|
}
|
|
582
|
-
}
|
|
583
|
-
} catch (e) {}
|
|
584
|
-
return "";
|
|
585
|
-
}, combinedErrorSelectors);
|
|
586
|
-
}
|
|
587
866
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
}
|
|
592
882
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (newCaptchaEl) {
|
|
599
|
-
const newBuffer = await newCaptchaEl.screenshot({ encoding: "binary", type: "png" });
|
|
600
|
-
newImageBase64 = `data:image/png;base64,${Buffer.from(newBuffer).toString("base64")}`;
|
|
601
|
-
}
|
|
602
|
-
} catch (e) {}
|
|
603
|
-
|
|
604
|
-
// CRITICAL FOR INSPECTOR: In inspection mode, NEVER purge the session on errors or timeout!
|
|
605
|
-
// Always snap the live DOM and return it so the user can inspect the red text and configure selectors
|
|
606
|
-
if (preservePageForInspection) {
|
|
607
|
-
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
608
|
-
const inspectTtl = 300;
|
|
609
|
-
session.timeoutTimer = setTimeout(() => {
|
|
610
|
-
purgeSession(sessionId).catch(() => {});
|
|
611
|
-
}, inspectTtl * 1000);
|
|
612
|
-
|
|
613
|
-
const snap = await captureInspectionSnapshot(page);
|
|
614
|
-
const effectiveStatus = isCaptchaError
|
|
615
|
-
? "INVALID_CAPTCHA"
|
|
616
|
-
: (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE");
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
status: effectiveStatus,
|
|
620
|
-
success: true, // Allows frontend to render the inspection screenshot
|
|
621
|
-
errorCode: (matchedRule && matchedRule.id) || (isCaptchaError ? "INVALID_CAPTCHA" : (errorText ? "BUSINESS_ERROR" : "INSPECTION_MODE")),
|
|
622
|
-
message: (matchedRule && matchedRule.customMessage) || errorText || (matchedOutcome === "TIMEOUT" ? "Page updated (no success table found). You can now inspect elements or error messages." : "Page outcome revealed for inspection."),
|
|
623
|
-
newImageBase64,
|
|
624
|
-
matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
|
|
625
|
-
sessionId,
|
|
626
|
-
isInspectionMode: true,
|
|
627
|
-
...snap,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
883
|
+
return {
|
|
884
|
+
matchedRuleId: fallbackRuleId,
|
|
885
|
+
text: foundText,
|
|
886
|
+
};
|
|
887
|
+
}, customRules, combinedErrorSelectors);
|
|
630
888
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
session.timeoutTimer = setTimeout(() => {
|
|
636
|
-
purgeSession(sessionId).catch(() => {});
|
|
637
|
-
}, retryTtlSeconds * 1000);
|
|
638
|
-
|
|
639
|
-
return {
|
|
640
|
-
status: "INVALID_CAPTCHA",
|
|
641
|
-
success: false,
|
|
642
|
-
message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
|
|
643
|
-
canRetry: true,
|
|
644
|
-
newImageBase64,
|
|
645
|
-
errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
|
|
646
|
-
};
|
|
647
|
-
}
|
|
889
|
+
if (checkRulesAndText && checkRulesAndText.matchedRuleId) {
|
|
890
|
+
matchedRule = customRules.find((r) => r.id === checkRulesAndText.matchedRuleId) || null;
|
|
891
|
+
}
|
|
892
|
+
errorText = (checkRulesAndText && checkRulesAndText.text) || "";
|
|
648
893
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
success: false,
|
|
654
|
-
errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
|
|
655
|
-
message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
|
|
656
|
-
canRetry: false,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
894
|
+
const regex = new RegExp(invalidPattern, "i");
|
|
895
|
+
const isCaptchaError = matchedRule
|
|
896
|
+
? matchedRule.action === "RETRY_CAPTCHA"
|
|
897
|
+
: (Boolean(errorText) && regex.test(errorText));
|
|
659
898
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
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
|
+
}
|
|
669
936
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
}))
|
|
688
|
-
.catch(() => ({ width: 1366, height: 900 }));
|
|
689
|
-
|
|
690
|
-
return {
|
|
691
|
-
screenshotBase64,
|
|
692
|
-
dimensions,
|
|
693
|
-
title: title || "Inspection Page",
|
|
694
|
-
};
|
|
695
|
-
}
|
|
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
|
+
}
|
|
696
954
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
}
|
|
703
965
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
+
}
|
|
707
975
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
+
}
|
|
713
1002
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
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);
|
|
721
1009
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
*/
|
|
726
|
-
export async function handleInteractiveSessionAbort(payload) {
|
|
727
|
-
const { sessionId } = payload;
|
|
728
|
-
if (sessionId) {
|
|
729
|
-
await purgeSession(sessionId);
|
|
730
|
-
}
|
|
731
|
-
return { success: true, aborted: true };
|
|
732
|
-
}
|
|
1010
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
1011
|
+
throw new Error("Interactive session not found or already expired");
|
|
1012
|
+
}
|
|
733
1013
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|
|
741
1027
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
}
|
|
745
1039
|
|
|
746
|
-
|
|
747
|
-
|
|
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);
|
|
748
1047
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
for (const strat of strategies) {
|
|
753
|
-
val = await extractBrowserField(page, strat);
|
|
754
|
-
if (val !== null && val !== undefined && val !== "") break;
|
|
755
|
-
}
|
|
1048
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
1049
|
+
throw new Error("Interactive session not found or already expired");
|
|
1050
|
+
}
|
|
756
1051
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
if (mapping.type) {
|
|
761
|
-
val = castValueToType(val, mapping.type);
|
|
762
|
-
}
|
|
763
|
-
raw[mapping.field || mapping.key] = val;
|
|
764
|
-
}
|
|
1052
|
+
const { page } = session;
|
|
1053
|
+
const raw = {};
|
|
765
1054
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
}
|
|
771
1077
|
|