app-manager-edge-worker 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -203,6 +203,15 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
203
203
 
204
204
  ## 📝 Changelog
205
205
 
206
+ ### v1.2.1
207
+ - **Smart Form Field & Leaf Control Element Resolution**:
208
+ - Replaced single-surface `elementFromPoint` with full-stack `elementsFromPoint` coordinate sampling and multi-offset probing.
209
+ - Implemented intelligent priority scoring favoring form inputs (`<input>`, `<textarea>`, `<select>`), captcha images (`<img>`, `<canvas>`), and interactive action buttons over generic wrapper `<div>` tags.
210
+ - Added severe scoring penalties (-50,000) for full-screen dimmer overlays, modal backdrops (`div.dimmer-holder`, `div.ui.dimmer`, `div.backdrop`), and transparent blocker masks.
211
+ - Added automatic container drill-down to pick child form inputs when clicking inside input groups or labels.
212
+ - Extended candidate selector generation to include form `name`, `placeholder`, `type`, and image `alt`/`captcha` attributes.
213
+ - Added stacked `alternates` metadata return for rapid element switching in the Visual Element Inspector.
214
+
206
215
  ### v1.2.0
207
216
  - **Human-In-The-Loop (HITL) Interactive Scraping Engine**:
208
217
  - Added stateful interactive session handlers: `HITL_SESSION_INIT`, `HITL_SESSION_REFRESH`, `HITL_SESSION_SOLVE`, and `HITL_SESSION_ABORT`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-manager-edge-worker",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/banner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "module";
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
- let workerVersion = "1.2.0";
4
+ let workerVersion = "1.2.1";
5
5
  try {
6
6
  const pkg = require("../package.json");
7
7
  if (pkg?.version) workerVersion = pkg.version;
package/src/client.js CHANGED
@@ -18,7 +18,7 @@ import fs from "fs";
18
18
  import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
19
19
  import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
20
20
 
21
- let workerPkgVersion = "1.2.0";
21
+ let workerPkgVersion = "1.2.1";
22
22
  try {
23
23
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
24
24
  if (pkg?.version) workerPkgVersion = pkg.version;
@@ -111,14 +111,66 @@ export async function handlePreviewPage(payload) {
111
111
  * Handles INSPECT_ELEMENT task: resolves DOM element coordinates or evaluates candidate selectors on the edge worker.
112
112
  */
113
113
  export async function handleInspectElement(payload) {
114
- const { url, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
114
+ const { url, preScrapeActions, authentication, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
115
115
  const browser = await localBrowserManager.getBrowser();
116
116
  const page = await browser.newPage();
117
117
 
118
118
  try {
119
119
  await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 });
120
+
121
+ // Apply authentication if provided
122
+ if (authentication) {
123
+ if (authentication.type === "COOKIES" && Array.isArray(authentication.cookies)) {
124
+ const parsedUrl = new URL(url);
125
+ await page.setCookie(...authentication.cookies.map(c => ({
126
+ name: c.name,
127
+ value: c.value,
128
+ domain: c.domain || parsedUrl.hostname,
129
+ path: c.path || "/",
130
+ }))).catch(() => {});
131
+ }
132
+ if (authentication.type === "HEADERS" && authentication.headers) {
133
+ await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
134
+ }
135
+ }
136
+
120
137
  await page.goto(url, { waitUntil: "domcontentloaded", timeout });
121
- await new Promise((r) => setTimeout(r, 1000));
138
+ try {
139
+ await page.waitForNetworkIdle({ idleTime: 500, timeout: 3500 }).catch(() => {});
140
+ } catch (_) {}
141
+ await page.evaluate(() => document.fonts?.ready).catch(() => {});
142
+ await new Promise((r) => setTimeout(r, 800));
143
+
144
+ // Execute pre-scrape actions if present
145
+ if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
146
+ for (const action of preScrapeActions) {
147
+ try {
148
+ if (action.actionType === "CLICK" && action.selector) {
149
+ await page.waitForSelector(action.selector, { timeout: 4000 });
150
+ await page.click(action.selector);
151
+ } else if (action.actionType === "TYPE" && action.selector && action.value) {
152
+ await page.waitForSelector(action.selector, { timeout: 4000 });
153
+ await page.type(action.selector, action.value, { delay: 30 });
154
+ } else if (action.actionType === "SCROLL") {
155
+ const scrollPixels = parseInt(action.value, 10) || 500;
156
+ await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
157
+ } else if (action.actionType === "WAIT_FOR" && action.selector) {
158
+ await page.waitForSelector(action.selector, { timeout: 8000 });
159
+ } else if (action.actionType === "SELECT_OPTION" && action.selector && action.value) {
160
+ await page.waitForSelector(action.selector, { timeout: 4000 });
161
+ await page.select(action.selector, action.value);
162
+ } else if (action.actionType === "PRESS_KEY" && action.value) {
163
+ await page.keyboard.press(action.value);
164
+ }
165
+ if (action.delayMs && action.delayMs > 0) {
166
+ await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 5000)));
167
+ }
168
+ } catch (actErr) {
169
+ console.warn(`[WorkerInspect] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
170
+ }
171
+ }
172
+ await new Promise((r) => setTimeout(r, 600));
173
+ }
122
174
 
123
175
  if (x !== undefined && y !== undefined) {
124
176
  const elementInfo = await page.evaluate((px, py) => {
@@ -129,8 +181,168 @@ export async function handleInspectElement(payload) {
129
181
  const clientX = px - window.scrollX;
130
182
  const clientY = py - window.scrollY;
131
183
 
132
- const el = document.elementFromPoint(clientX, clientY);
133
- if (!el) return null;
184
+ // ─── Step 1: Collect candidates using elementsFromPoint + surrounding offsets ───
185
+ const candidateNodes = new Set();
186
+ const sampleOffsets = [
187
+ [0, 0],
188
+ [-4, 0], [4, 0], [0, -4], [0, 4],
189
+ [-8, 0], [8, 0], [0, -8], [0, 8],
190
+ ];
191
+
192
+ for (const [dx, dy] of sampleOffsets) {
193
+ const cx = clientX + dx;
194
+ const cy = clientY + dy;
195
+ if (cx >= 0 && cy >= 0 && cx <= window.innerWidth && cy <= window.innerHeight) {
196
+ if (typeof document.elementsFromPoint === "function") {
197
+ const list = document.elementsFromPoint(cx, cy) || [];
198
+ for (const node of list) {
199
+ if (node && node.nodeType === 1) candidateNodes.add(node);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ if (candidateNodes.size === 0) {
206
+ const single = document.elementFromPoint(clientX, clientY);
207
+ if (single && single.nodeType === 1) candidateNodes.add(single);
208
+ }
209
+
210
+ const rawElements = Array.from(candidateNodes);
211
+ if (rawElements.length === 0) return null;
212
+
213
+ // ─── Step 2: Intelligent priority scoring ───
214
+ const scoreElement = (node) => {
215
+ if (!node || node === document.body || node === document.documentElement) return -999999;
216
+ const tag = node.tagName.toLowerCase();
217
+ const cls = (typeof node.className === "string" ? node.className : "").toLowerCase();
218
+ const id = (node.id || "").toLowerCase();
219
+ const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
220
+
221
+ if (style) {
222
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
223
+ return -80000;
224
+ }
225
+ }
226
+
227
+ const rect = node.getBoundingClientRect();
228
+ const area = rect.width * rect.height;
229
+
230
+ // Severe penalty for dimmer, overlay, backdrop, modal blocker containers
231
+ const isDimmer =
232
+ cls.includes("dimmer") ||
233
+ cls.includes("backdrop") ||
234
+ cls.includes("overlay") ||
235
+ cls.includes("mask") ||
236
+ cls.includes("blocker") ||
237
+ id.includes("dimmer") ||
238
+ id.includes("backdrop") ||
239
+ id.includes("overlay");
240
+
241
+ if (isDimmer) {
242
+ return -50000;
243
+ }
244
+
245
+ // Severe penalty for giant viewport-covering containers
246
+ if (rect.width >= window.innerWidth * 0.85 && rect.height >= window.innerHeight * 0.7) {
247
+ if (["div", "section", "main", "body", "form"].includes(tag)) {
248
+ return -40000;
249
+ }
250
+ }
251
+
252
+ let score = 0;
253
+
254
+ // Form controls (Inputs, Textareas, Selects) - Top Priority!
255
+ if (tag === "input") {
256
+ const inputType = (node.type || "text").toLowerCase();
257
+ if (inputType === "hidden") return -10000;
258
+ if (["submit", "button", "reset"].includes(inputType)) {
259
+ score += 18000;
260
+ } else {
261
+ score += 20000;
262
+ }
263
+ } else if (tag === "textarea" || tag === "select" || node.getAttribute("contenteditable") === "true") {
264
+ score += 20000;
265
+ }
266
+ // Captchas & Media
267
+ else if (tag === "img") {
268
+ const src = (node.src || "").toLowerCase();
269
+ const alt = (node.getAttribute("alt") || "").toLowerCase();
270
+ if (src.includes("captcha") || alt.includes("captcha") || id.includes("captcha") || cls.includes("captcha")) {
271
+ score += 22000;
272
+ } else {
273
+ score += 15000;
274
+ }
275
+ } else if (tag === "canvas") {
276
+ score += 16000;
277
+ }
278
+ // Interactive Buttons & Links
279
+ else if (tag === "button" || node.getAttribute("role") === "button") {
280
+ score += 17000;
281
+ } else if (tag === "a" && (node.hasAttribute("href") || node.getAttribute("role") === "button")) {
282
+ score += 16000;
283
+ }
284
+ // Labels
285
+ else if (tag === "label") {
286
+ score += 13000;
287
+ }
288
+ // Table cells & headers
289
+ else if (tag === "td" || tag === "th") {
290
+ score += 12000;
291
+ }
292
+ // Headings
293
+ else if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
294
+ score += 11000;
295
+ }
296
+ // Text leaves
297
+ else if (["span", "p", "strong", "b", "em", "i", "li", "small"].includes(tag)) {
298
+ const txt = (node.innerText || node.textContent || "").trim();
299
+ if (txt.length > 0 && node.children.length === 0) {
300
+ score += 10000;
301
+ } else {
302
+ score += 7000;
303
+ }
304
+ }
305
+ // Generic wrappers
306
+ else if (["div", "form", "section", "article"].includes(tag)) {
307
+ score += 2000;
308
+ } else {
309
+ score += 1000;
310
+ }
311
+
312
+ // Tightly-fitting area preference
313
+ if (area > 0) {
314
+ score -= Math.min(area / 100, 10000);
315
+ }
316
+
317
+ // Exact coordinate hit bonus
318
+ if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
319
+ score += 1500;
320
+ }
321
+
322
+ return score;
323
+ };
324
+
325
+ rawElements.sort((a, b) => scoreElement(b) - scoreElement(a));
326
+
327
+ let el = rawElements[0];
328
+ if (!el || el === document.body || el === document.documentElement) {
329
+ el = rawElements.find(n => n !== document.body && n !== document.documentElement) || document.body;
330
+ }
331
+
332
+ // ─── Step 3: Drill-down inspection ───
333
+ // If top-scoring element is a container, check if it directly contains an interactive control near click
334
+ if (el && ["div", "span", "label", "p", "form", "section"].includes(el.tagName.toLowerCase())) {
335
+ const innerControls = Array.from(el.querySelectorAll('input:not([type="hidden"]), textarea, select, button, img, canvas'));
336
+ for (const ctrl of innerControls) {
337
+ const cRect = ctrl.getBoundingClientRect();
338
+ const pad = 25;
339
+ if (clientX >= cRect.left - pad && clientX <= cRect.right + pad &&
340
+ clientY >= cRect.top - pad && clientY <= cRect.bottom + pad) {
341
+ el = ctrl;
342
+ break;
343
+ }
344
+ }
345
+ }
134
346
 
135
347
  const rect = el.getBoundingClientRect();
136
348
  const boundingRect = {
@@ -237,6 +449,34 @@ export async function handleInspectElement(payload) {
237
449
  return "/" + parts.join("/");
238
450
  };
239
451
 
452
+ // ─── Collect Alternates for quick switching ───
453
+ const alternates = [];
454
+ const seenAltKeys = new Set();
455
+ for (const cand of rawElements) {
456
+ if (cand === el || cand === document.body || cand === document.documentElement) continue;
457
+ const cScore = scoreElement(cand);
458
+ if (cScore < 0) continue;
459
+ const cTag = cand.tagName.toLowerCase();
460
+ const cId = cand.id && /^[a-zA-Z][\w-]*$/.test(cand.id) ? `#${cand.id}` : "";
461
+ const cCls = (cand.className && typeof cand.className === "string")
462
+ ? cand.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))[0]
463
+ : "";
464
+ const altKey = `${cTag}${cId}${cCls ? `.${cCls}` : ""}`;
465
+ if (seenAltKeys.has(altKey)) continue;
466
+ seenAltKeys.add(altKey);
467
+
468
+ const altSel = cId ? cId : (cCls ? `${cTag}.${cCls}` : cTag);
469
+ const altTxt = (cand.innerText || cand.textContent || "").trim().slice(0, 35);
470
+ alternates.push({
471
+ tagName: cTag,
472
+ selector: altSel,
473
+ id: cand.id || null,
474
+ text: altTxt,
475
+ label: `<${cTag}${cId || (cCls ? `.${cCls}` : "")}> ${altTxt ? `"${altTxt}"` : ""}`.trim()
476
+ });
477
+ if (alternates.length >= 4) break;
478
+ }
479
+
240
480
  // ─── Build ranked candidates ───
241
481
  const candidates = [];
242
482
  const seen = new Set();
@@ -252,7 +492,64 @@ export async function handleInspectElement(payload) {
252
492
  addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
253
493
  }
254
494
 
255
- // 2. data-* and aria-* attributes
495
+ // 2. Form Name attribute (essential for inputs/forms)
496
+ const nameAttr = el.getAttribute("name");
497
+ if (nameAttr && /^[a-zA-Z0-9_\-\[\]:]+$/.test(nameAttr)) {
498
+ const selName = `${el.tagName.toLowerCase()}[name="${nameAttr}"]`;
499
+ const mc = countMatches(selName);
500
+ addCandidate("css", selName, {
501
+ specificity: mc === 1 ? "unique" : "high",
502
+ label: `Form Name: ${nameAttr}`,
503
+ matchCount: mc
504
+ });
505
+ }
506
+
507
+ // 3. Form Placeholder attribute
508
+ const phAttr = el.getAttribute("placeholder");
509
+ if (phAttr && phAttr.length < 80) {
510
+ const selPh = `${el.tagName.toLowerCase()}[placeholder="${phAttr.replace(/"/g, '\\"')}"]`;
511
+ const mc = countMatches(selPh);
512
+ addCandidate("css", selPh, {
513
+ specificity: mc === 1 ? "unique" : "high",
514
+ label: `Placeholder: ${phAttr.slice(0, 25)}`,
515
+ matchCount: mc
516
+ });
517
+ }
518
+
519
+ // 4. Input Type / Role attribute
520
+ const typeAttr = el.getAttribute("type");
521
+ if (typeAttr && typeAttr !== "text" && ["input", "button"].includes(el.tagName.toLowerCase())) {
522
+ const selType = `${el.tagName.toLowerCase()}[type="${typeAttr}"]`;
523
+ const mc = countMatches(selType);
524
+ addCandidate("css", selType, {
525
+ specificity: mc === 1 ? "unique" : "medium",
526
+ label: `Input Type: ${typeAttr}`,
527
+ matchCount: mc
528
+ });
529
+ }
530
+
531
+ // 5. Image Alt / Captcha attribute
532
+ if (el.tagName.toLowerCase() === "img") {
533
+ const altAttr = el.getAttribute("alt");
534
+ if (altAttr && altAttr.length < 60) {
535
+ const selAlt = `img[alt="${altAttr.replace(/"/g, '\\"')}"]`;
536
+ const mc = countMatches(selAlt);
537
+ addCandidate("css", selAlt, {
538
+ specificity: mc === 1 ? "unique" : "high",
539
+ label: `Image Alt: ${altAttr.slice(0, 25)}`,
540
+ matchCount: mc
541
+ });
542
+ }
543
+ const srcAttr = el.getAttribute("src");
544
+ if (srcAttr && (srcAttr.toLowerCase().includes("captcha") || srcAttr.toLowerCase().includes("secur"))) {
545
+ addCandidate("css", `img[src*="captcha"]`, {
546
+ specificity: "high",
547
+ label: "Captcha Source Pattern"
548
+ });
549
+ }
550
+ }
551
+
552
+ // 6. data-* and aria-* attributes
256
553
  for (const attr of el.attributes) {
257
554
  if ((attr.name.startsWith("data-") || attr.name.startsWith("aria-")) && attr.value && attr.value.length < 80) {
258
555
  addCandidate("css", `${el.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`, {
@@ -261,7 +558,7 @@ export async function handleInspectElement(payload) {
261
558
  }
262
559
  }
263
560
 
264
- // 3. Shortest unique CSS path
561
+ // 7. Shortest unique CSS path
265
562
  const shortUnique = buildShortUniqueSelector(el);
266
563
  if (shortUnique) {
267
564
  const mc = countMatches(shortUnique);
@@ -272,7 +569,7 @@ export async function handleInspectElement(payload) {
272
569
  });
273
570
  }
274
571
 
275
- // 4. Class-based selectors (increasing combos until unique)
572
+ // 8. Class-based selectors (increasing combos until unique)
276
573
  const classes = (el.className && typeof el.className === "string")
277
574
  ? el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
278
575
  : [];
@@ -287,7 +584,7 @@ export async function handleInspectElement(payload) {
287
584
  if (mc === 1) break;
288
585
  }
289
586
 
290
- // 5. Table-specific selectors
587
+ // 9. Table-specific selectors
291
588
  const tr = el.closest("tr");
292
589
  if (tr && tr.parentElement) {
293
590
  const rowIdx = Array.from(tr.parentElement.children).indexOf(tr) + 1;
@@ -303,11 +600,11 @@ export async function handleInspectElement(payload) {
303
600
  }
304
601
  }
305
602
 
306
- // 6. Full structural CSS path
603
+ // 10. Full structural CSS path
307
604
  const fullCss = buildCssPath(el, 8);
308
605
  addCandidate("css", fullCss, { specificity: "unique", label: "Full CSS Path (Structural)" });
309
606
 
310
- // 7. XPath — most reliable for scrapers
607
+ // 11. XPath — most reliable for scrapers
311
608
  const xpath = buildXPath(el);
312
609
  addCandidate("xpath", xpath, { specificity: "high", label: "XPath" });
313
610
 
@@ -481,6 +778,7 @@ export async function handleInspectElement(payload) {
481
778
  candidates,
482
779
  hierarchy,
483
780
  boundingRect,
781
+ alternates,
484
782
  tableDetection,
485
783
  repeatingDetection,
486
784
  };
@@ -608,9 +906,34 @@ export async function handleInspectElement(payload) {
608
906
  currH = currH.parentElement;
609
907
  }
610
908
 
611
- const candidates = [
612
- { type: strategyType, value: matchedSelector, attribute: "text", matchCount: count }
613
- ];
909
+ const candidates = [];
910
+ const seen = new Set();
911
+ const addCandidate = (type, value, extra = {}) => {
912
+ if (!value || seen.has(value)) return;
913
+ seen.add(value);
914
+ const matchCount = type === "css" ? countMatches(value) : null;
915
+ candidates.push({ type, value, attribute: "text", matchCount, ...extra });
916
+ };
917
+
918
+ addCandidate(strategyType, matchedSelector, { specificity: count === 1 ? "unique" : "medium", label: "Selected Query", matchCount: count });
919
+
920
+ if (el.id && /^[a-zA-Z][\w-]*$/.test(el.id)) {
921
+ addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
922
+ }
923
+ const nameAttr = el.getAttribute("name");
924
+ if (nameAttr && /^[a-zA-Z0-9_\-\[\]:]+$/.test(nameAttr)) {
925
+ addCandidate("css", `${el.tagName.toLowerCase()}[name="${nameAttr}"]`, { specificity: "unique", label: `Form Name: ${nameAttr}` });
926
+ }
927
+ const phAttr = el.getAttribute("placeholder");
928
+ if (phAttr && phAttr.length < 80) {
929
+ addCandidate("css", `${el.tagName.toLowerCase()}[placeholder="${phAttr.replace(/"/g, '\\"')}"]`, { specificity: "high", label: `Placeholder: ${phAttr.slice(0, 25)}` });
930
+ }
931
+ if (el.className && typeof el.className === "string") {
932
+ const cls = el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c));
933
+ if (cls.length > 0) {
934
+ addCandidate("css", `${el.tagName.toLowerCase()}.${cls[0]}`, { specificity: "medium", label: "Class Selector" });
935
+ }
936
+ }
614
937
 
615
938
  return {
616
939
  found: true,