app-manager-edge-worker 1.2.0 → 1.2.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 +14 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +1 -1
- package/src/handlers/interactiveHandler.js +12 -1
- package/src/handlers/previewHandler.js +355 -14
package/README.md
CHANGED
|
@@ -203,6 +203,20 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
203
203
|
|
|
204
204
|
## 📝 Changelog
|
|
205
205
|
|
|
206
|
+
### v1.2.2
|
|
207
|
+
- **Reactive Portal Form & Dynamic Captcha Reveal Compatibility**:
|
|
208
|
+
- Enhanced form input typing in `HITL_SESSION_INIT` and pre-scrape `TYPE` actions (`PREVIEW_PAGE`, `INSPECT_ELEMENT`) to dispatch DOM `input`, `change`, and `blur` events with a simulated `Tab` keypress.
|
|
209
|
+
- Guarantees immediate captcha generation on reactive frontend frameworks (AngularJS, React, Vue, jQuery) requiring input validation/blur before unhiding dynamic captcha elements.
|
|
210
|
+
|
|
211
|
+
### v1.2.1
|
|
212
|
+
- **Smart Form Field & Leaf Control Element Resolution**:
|
|
213
|
+
- Replaced single-surface `elementFromPoint` with full-stack `elementsFromPoint` coordinate sampling and multi-offset probing.
|
|
214
|
+
- Implemented intelligent priority scoring favoring form inputs (`<input>`, `<textarea>`, `<select>`), captcha images (`<img>`, `<canvas>`), and interactive action buttons over generic wrapper `<div>` tags.
|
|
215
|
+
- 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.
|
|
216
|
+
- Added automatic container drill-down to pick child form inputs when clicking inside input groups or labels.
|
|
217
|
+
- Extended candidate selector generation to include form `name`, `placeholder`, `type`, and image `alt`/`captcha` attributes.
|
|
218
|
+
- Added stacked `alternates` metadata return for rapid element switching in the Visual Element Inspector.
|
|
219
|
+
|
|
206
220
|
### v1.2.0
|
|
207
221
|
- **Human-In-The-Loop (HITL) Interactive Scraping Engine**:
|
|
208
222
|
- 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.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/banner.js
CHANGED
package/src/client.js
CHANGED
|
@@ -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.
|
|
21
|
+
let workerPkgVersion = "1.2.2";
|
|
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;
|
|
@@ -185,7 +185,18 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
185
185
|
await page.type(input.selector, String(input.value), { delay: input.delayMs || 30 });
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
// Explicitly dispatch input, change, and blur events to trigger AngularJS/React/portal validation listeners
|
|
189
|
+
await page.evaluate((sel) => {
|
|
190
|
+
const el = document.querySelector(sel);
|
|
191
|
+
if (el) {
|
|
192
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
193
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
194
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
195
|
+
}
|
|
196
|
+
}, input.selector).catch(() => {});
|
|
197
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
198
|
+
|
|
199
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
202
|
|
|
@@ -43,7 +43,16 @@ export async function handlePreviewPage(payload) {
|
|
|
43
43
|
await page.click(action.selector);
|
|
44
44
|
} else if (action.actionType === "TYPE" && action.selector && action.value) {
|
|
45
45
|
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
46
|
-
await page.type(action.selector, action.value, { delay: 30 });
|
|
46
|
+
await page.type(action.selector, action.value, { delay: action.delayMs || 30 });
|
|
47
|
+
await page.evaluate((sel) => {
|
|
48
|
+
const el = document.querySelector(sel);
|
|
49
|
+
if (el) {
|
|
50
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
51
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
52
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
53
|
+
}
|
|
54
|
+
}, action.selector).catch(() => {});
|
|
55
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
47
56
|
} else if (action.actionType === "SCROLL") {
|
|
48
57
|
const scrollPixels = parseInt(action.value, 10) || 500;
|
|
49
58
|
await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
|
|
@@ -111,14 +120,75 @@ export async function handlePreviewPage(payload) {
|
|
|
111
120
|
* Handles INSPECT_ELEMENT task: resolves DOM element coordinates or evaluates candidate selectors on the edge worker.
|
|
112
121
|
*/
|
|
113
122
|
export async function handleInspectElement(payload) {
|
|
114
|
-
const { url, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
|
|
123
|
+
const { url, preScrapeActions, authentication, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
|
|
115
124
|
const browser = await localBrowserManager.getBrowser();
|
|
116
125
|
const page = await browser.newPage();
|
|
117
126
|
|
|
118
127
|
try {
|
|
119
128
|
await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 });
|
|
129
|
+
|
|
130
|
+
// Apply authentication if provided
|
|
131
|
+
if (authentication) {
|
|
132
|
+
if (authentication.type === "COOKIES" && Array.isArray(authentication.cookies)) {
|
|
133
|
+
const parsedUrl = new URL(url);
|
|
134
|
+
await page.setCookie(...authentication.cookies.map(c => ({
|
|
135
|
+
name: c.name,
|
|
136
|
+
value: c.value,
|
|
137
|
+
domain: c.domain || parsedUrl.hostname,
|
|
138
|
+
path: c.path || "/",
|
|
139
|
+
}))).catch(() => {});
|
|
140
|
+
}
|
|
141
|
+
if (authentication.type === "HEADERS" && authentication.headers) {
|
|
142
|
+
await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
121
|
-
|
|
147
|
+
try {
|
|
148
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 3500 }).catch(() => {});
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
await page.evaluate(() => document.fonts?.ready).catch(() => {});
|
|
151
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
152
|
+
|
|
153
|
+
// Execute pre-scrape actions if present
|
|
154
|
+
if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
|
|
155
|
+
for (const action of preScrapeActions) {
|
|
156
|
+
try {
|
|
157
|
+
if (action.actionType === "CLICK" && action.selector) {
|
|
158
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
159
|
+
await page.click(action.selector);
|
|
160
|
+
} else if (action.actionType === "TYPE" && action.selector && action.value) {
|
|
161
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
162
|
+
await page.type(action.selector, action.value, { delay: action.delayMs || 30 });
|
|
163
|
+
await page.evaluate((sel) => {
|
|
164
|
+
const el = document.querySelector(sel);
|
|
165
|
+
if (el) {
|
|
166
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
167
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
168
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
169
|
+
}
|
|
170
|
+
}, action.selector).catch(() => {});
|
|
171
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
172
|
+
} else if (action.actionType === "SCROLL") {
|
|
173
|
+
const scrollPixels = parseInt(action.value, 10) || 500;
|
|
174
|
+
await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
|
|
175
|
+
} else if (action.actionType === "WAIT_FOR" && action.selector) {
|
|
176
|
+
await page.waitForSelector(action.selector, { timeout: 8000 });
|
|
177
|
+
} else if (action.actionType === "SELECT_OPTION" && action.selector && action.value) {
|
|
178
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
179
|
+
await page.select(action.selector, action.value);
|
|
180
|
+
} else if (action.actionType === "PRESS_KEY" && action.value) {
|
|
181
|
+
await page.keyboard.press(action.value);
|
|
182
|
+
}
|
|
183
|
+
if (action.delayMs && action.delayMs > 0) {
|
|
184
|
+
await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 5000)));
|
|
185
|
+
}
|
|
186
|
+
} catch (actErr) {
|
|
187
|
+
console.warn(`[WorkerInspect] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
191
|
+
}
|
|
122
192
|
|
|
123
193
|
if (x !== undefined && y !== undefined) {
|
|
124
194
|
const elementInfo = await page.evaluate((px, py) => {
|
|
@@ -129,8 +199,168 @@ export async function handleInspectElement(payload) {
|
|
|
129
199
|
const clientX = px - window.scrollX;
|
|
130
200
|
const clientY = py - window.scrollY;
|
|
131
201
|
|
|
132
|
-
|
|
133
|
-
|
|
202
|
+
// ─── Step 1: Collect candidates using elementsFromPoint + surrounding offsets ───
|
|
203
|
+
const candidateNodes = new Set();
|
|
204
|
+
const sampleOffsets = [
|
|
205
|
+
[0, 0],
|
|
206
|
+
[-4, 0], [4, 0], [0, -4], [0, 4],
|
|
207
|
+
[-8, 0], [8, 0], [0, -8], [0, 8],
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
for (const [dx, dy] of sampleOffsets) {
|
|
211
|
+
const cx = clientX + dx;
|
|
212
|
+
const cy = clientY + dy;
|
|
213
|
+
if (cx >= 0 && cy >= 0 && cx <= window.innerWidth && cy <= window.innerHeight) {
|
|
214
|
+
if (typeof document.elementsFromPoint === "function") {
|
|
215
|
+
const list = document.elementsFromPoint(cx, cy) || [];
|
|
216
|
+
for (const node of list) {
|
|
217
|
+
if (node && node.nodeType === 1) candidateNodes.add(node);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (candidateNodes.size === 0) {
|
|
224
|
+
const single = document.elementFromPoint(clientX, clientY);
|
|
225
|
+
if (single && single.nodeType === 1) candidateNodes.add(single);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rawElements = Array.from(candidateNodes);
|
|
229
|
+
if (rawElements.length === 0) return null;
|
|
230
|
+
|
|
231
|
+
// ─── Step 2: Intelligent priority scoring ───
|
|
232
|
+
const scoreElement = (node) => {
|
|
233
|
+
if (!node || node === document.body || node === document.documentElement) return -999999;
|
|
234
|
+
const tag = node.tagName.toLowerCase();
|
|
235
|
+
const cls = (typeof node.className === "string" ? node.className : "").toLowerCase();
|
|
236
|
+
const id = (node.id || "").toLowerCase();
|
|
237
|
+
const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
238
|
+
|
|
239
|
+
if (style) {
|
|
240
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
241
|
+
return -80000;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const rect = node.getBoundingClientRect();
|
|
246
|
+
const area = rect.width * rect.height;
|
|
247
|
+
|
|
248
|
+
// Severe penalty for dimmer, overlay, backdrop, modal blocker containers
|
|
249
|
+
const isDimmer =
|
|
250
|
+
cls.includes("dimmer") ||
|
|
251
|
+
cls.includes("backdrop") ||
|
|
252
|
+
cls.includes("overlay") ||
|
|
253
|
+
cls.includes("mask") ||
|
|
254
|
+
cls.includes("blocker") ||
|
|
255
|
+
id.includes("dimmer") ||
|
|
256
|
+
id.includes("backdrop") ||
|
|
257
|
+
id.includes("overlay");
|
|
258
|
+
|
|
259
|
+
if (isDimmer) {
|
|
260
|
+
return -50000;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Severe penalty for giant viewport-covering containers
|
|
264
|
+
if (rect.width >= window.innerWidth * 0.85 && rect.height >= window.innerHeight * 0.7) {
|
|
265
|
+
if (["div", "section", "main", "body", "form"].includes(tag)) {
|
|
266
|
+
return -40000;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let score = 0;
|
|
271
|
+
|
|
272
|
+
// Form controls (Inputs, Textareas, Selects) - Top Priority!
|
|
273
|
+
if (tag === "input") {
|
|
274
|
+
const inputType = (node.type || "text").toLowerCase();
|
|
275
|
+
if (inputType === "hidden") return -10000;
|
|
276
|
+
if (["submit", "button", "reset"].includes(inputType)) {
|
|
277
|
+
score += 18000;
|
|
278
|
+
} else {
|
|
279
|
+
score += 20000;
|
|
280
|
+
}
|
|
281
|
+
} else if (tag === "textarea" || tag === "select" || node.getAttribute("contenteditable") === "true") {
|
|
282
|
+
score += 20000;
|
|
283
|
+
}
|
|
284
|
+
// Captchas & Media
|
|
285
|
+
else if (tag === "img") {
|
|
286
|
+
const src = (node.src || "").toLowerCase();
|
|
287
|
+
const alt = (node.getAttribute("alt") || "").toLowerCase();
|
|
288
|
+
if (src.includes("captcha") || alt.includes("captcha") || id.includes("captcha") || cls.includes("captcha")) {
|
|
289
|
+
score += 22000;
|
|
290
|
+
} else {
|
|
291
|
+
score += 15000;
|
|
292
|
+
}
|
|
293
|
+
} else if (tag === "canvas") {
|
|
294
|
+
score += 16000;
|
|
295
|
+
}
|
|
296
|
+
// Interactive Buttons & Links
|
|
297
|
+
else if (tag === "button" || node.getAttribute("role") === "button") {
|
|
298
|
+
score += 17000;
|
|
299
|
+
} else if (tag === "a" && (node.hasAttribute("href") || node.getAttribute("role") === "button")) {
|
|
300
|
+
score += 16000;
|
|
301
|
+
}
|
|
302
|
+
// Labels
|
|
303
|
+
else if (tag === "label") {
|
|
304
|
+
score += 13000;
|
|
305
|
+
}
|
|
306
|
+
// Table cells & headers
|
|
307
|
+
else if (tag === "td" || tag === "th") {
|
|
308
|
+
score += 12000;
|
|
309
|
+
}
|
|
310
|
+
// Headings
|
|
311
|
+
else if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
|
|
312
|
+
score += 11000;
|
|
313
|
+
}
|
|
314
|
+
// Text leaves
|
|
315
|
+
else if (["span", "p", "strong", "b", "em", "i", "li", "small"].includes(tag)) {
|
|
316
|
+
const txt = (node.innerText || node.textContent || "").trim();
|
|
317
|
+
if (txt.length > 0 && node.children.length === 0) {
|
|
318
|
+
score += 10000;
|
|
319
|
+
} else {
|
|
320
|
+
score += 7000;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Generic wrappers
|
|
324
|
+
else if (["div", "form", "section", "article"].includes(tag)) {
|
|
325
|
+
score += 2000;
|
|
326
|
+
} else {
|
|
327
|
+
score += 1000;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Tightly-fitting area preference
|
|
331
|
+
if (area > 0) {
|
|
332
|
+
score -= Math.min(area / 100, 10000);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Exact coordinate hit bonus
|
|
336
|
+
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
|
337
|
+
score += 1500;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return score;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
rawElements.sort((a, b) => scoreElement(b) - scoreElement(a));
|
|
344
|
+
|
|
345
|
+
let el = rawElements[0];
|
|
346
|
+
if (!el || el === document.body || el === document.documentElement) {
|
|
347
|
+
el = rawElements.find(n => n !== document.body && n !== document.documentElement) || document.body;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Step 3: Drill-down inspection ───
|
|
351
|
+
// If top-scoring element is a container, check if it directly contains an interactive control near click
|
|
352
|
+
if (el && ["div", "span", "label", "p", "form", "section"].includes(el.tagName.toLowerCase())) {
|
|
353
|
+
const innerControls = Array.from(el.querySelectorAll('input:not([type="hidden"]), textarea, select, button, img, canvas'));
|
|
354
|
+
for (const ctrl of innerControls) {
|
|
355
|
+
const cRect = ctrl.getBoundingClientRect();
|
|
356
|
+
const pad = 25;
|
|
357
|
+
if (clientX >= cRect.left - pad && clientX <= cRect.right + pad &&
|
|
358
|
+
clientY >= cRect.top - pad && clientY <= cRect.bottom + pad) {
|
|
359
|
+
el = ctrl;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
134
364
|
|
|
135
365
|
const rect = el.getBoundingClientRect();
|
|
136
366
|
const boundingRect = {
|
|
@@ -237,6 +467,34 @@ export async function handleInspectElement(payload) {
|
|
|
237
467
|
return "/" + parts.join("/");
|
|
238
468
|
};
|
|
239
469
|
|
|
470
|
+
// ─── Collect Alternates for quick switching ───
|
|
471
|
+
const alternates = [];
|
|
472
|
+
const seenAltKeys = new Set();
|
|
473
|
+
for (const cand of rawElements) {
|
|
474
|
+
if (cand === el || cand === document.body || cand === document.documentElement) continue;
|
|
475
|
+
const cScore = scoreElement(cand);
|
|
476
|
+
if (cScore < 0) continue;
|
|
477
|
+
const cTag = cand.tagName.toLowerCase();
|
|
478
|
+
const cId = cand.id && /^[a-zA-Z][\w-]*$/.test(cand.id) ? `#${cand.id}` : "";
|
|
479
|
+
const cCls = (cand.className && typeof cand.className === "string")
|
|
480
|
+
? cand.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))[0]
|
|
481
|
+
: "";
|
|
482
|
+
const altKey = `${cTag}${cId}${cCls ? `.${cCls}` : ""}`;
|
|
483
|
+
if (seenAltKeys.has(altKey)) continue;
|
|
484
|
+
seenAltKeys.add(altKey);
|
|
485
|
+
|
|
486
|
+
const altSel = cId ? cId : (cCls ? `${cTag}.${cCls}` : cTag);
|
|
487
|
+
const altTxt = (cand.innerText || cand.textContent || "").trim().slice(0, 35);
|
|
488
|
+
alternates.push({
|
|
489
|
+
tagName: cTag,
|
|
490
|
+
selector: altSel,
|
|
491
|
+
id: cand.id || null,
|
|
492
|
+
text: altTxt,
|
|
493
|
+
label: `<${cTag}${cId || (cCls ? `.${cCls}` : "")}> ${altTxt ? `"${altTxt}"` : ""}`.trim()
|
|
494
|
+
});
|
|
495
|
+
if (alternates.length >= 4) break;
|
|
496
|
+
}
|
|
497
|
+
|
|
240
498
|
// ─── Build ranked candidates ───
|
|
241
499
|
const candidates = [];
|
|
242
500
|
const seen = new Set();
|
|
@@ -252,7 +510,64 @@ export async function handleInspectElement(payload) {
|
|
|
252
510
|
addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
|
|
253
511
|
}
|
|
254
512
|
|
|
255
|
-
// 2.
|
|
513
|
+
// 2. Form Name attribute (essential for inputs/forms)
|
|
514
|
+
const nameAttr = el.getAttribute("name");
|
|
515
|
+
if (nameAttr && /^[a-zA-Z0-9_\-\[\]:]+$/.test(nameAttr)) {
|
|
516
|
+
const selName = `${el.tagName.toLowerCase()}[name="${nameAttr}"]`;
|
|
517
|
+
const mc = countMatches(selName);
|
|
518
|
+
addCandidate("css", selName, {
|
|
519
|
+
specificity: mc === 1 ? "unique" : "high",
|
|
520
|
+
label: `Form Name: ${nameAttr}`,
|
|
521
|
+
matchCount: mc
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 3. Form Placeholder attribute
|
|
526
|
+
const phAttr = el.getAttribute("placeholder");
|
|
527
|
+
if (phAttr && phAttr.length < 80) {
|
|
528
|
+
const selPh = `${el.tagName.toLowerCase()}[placeholder="${phAttr.replace(/"/g, '\\"')}"]`;
|
|
529
|
+
const mc = countMatches(selPh);
|
|
530
|
+
addCandidate("css", selPh, {
|
|
531
|
+
specificity: mc === 1 ? "unique" : "high",
|
|
532
|
+
label: `Placeholder: ${phAttr.slice(0, 25)}`,
|
|
533
|
+
matchCount: mc
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 4. Input Type / Role attribute
|
|
538
|
+
const typeAttr = el.getAttribute("type");
|
|
539
|
+
if (typeAttr && typeAttr !== "text" && ["input", "button"].includes(el.tagName.toLowerCase())) {
|
|
540
|
+
const selType = `${el.tagName.toLowerCase()}[type="${typeAttr}"]`;
|
|
541
|
+
const mc = countMatches(selType);
|
|
542
|
+
addCandidate("css", selType, {
|
|
543
|
+
specificity: mc === 1 ? "unique" : "medium",
|
|
544
|
+
label: `Input Type: ${typeAttr}`,
|
|
545
|
+
matchCount: mc
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 5. Image Alt / Captcha attribute
|
|
550
|
+
if (el.tagName.toLowerCase() === "img") {
|
|
551
|
+
const altAttr = el.getAttribute("alt");
|
|
552
|
+
if (altAttr && altAttr.length < 60) {
|
|
553
|
+
const selAlt = `img[alt="${altAttr.replace(/"/g, '\\"')}"]`;
|
|
554
|
+
const mc = countMatches(selAlt);
|
|
555
|
+
addCandidate("css", selAlt, {
|
|
556
|
+
specificity: mc === 1 ? "unique" : "high",
|
|
557
|
+
label: `Image Alt: ${altAttr.slice(0, 25)}`,
|
|
558
|
+
matchCount: mc
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const srcAttr = el.getAttribute("src");
|
|
562
|
+
if (srcAttr && (srcAttr.toLowerCase().includes("captcha") || srcAttr.toLowerCase().includes("secur"))) {
|
|
563
|
+
addCandidate("css", `img[src*="captcha"]`, {
|
|
564
|
+
specificity: "high",
|
|
565
|
+
label: "Captcha Source Pattern"
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 6. data-* and aria-* attributes
|
|
256
571
|
for (const attr of el.attributes) {
|
|
257
572
|
if ((attr.name.startsWith("data-") || attr.name.startsWith("aria-")) && attr.value && attr.value.length < 80) {
|
|
258
573
|
addCandidate("css", `${el.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`, {
|
|
@@ -261,7 +576,7 @@ export async function handleInspectElement(payload) {
|
|
|
261
576
|
}
|
|
262
577
|
}
|
|
263
578
|
|
|
264
|
-
//
|
|
579
|
+
// 7. Shortest unique CSS path
|
|
265
580
|
const shortUnique = buildShortUniqueSelector(el);
|
|
266
581
|
if (shortUnique) {
|
|
267
582
|
const mc = countMatches(shortUnique);
|
|
@@ -272,7 +587,7 @@ export async function handleInspectElement(payload) {
|
|
|
272
587
|
});
|
|
273
588
|
}
|
|
274
589
|
|
|
275
|
-
//
|
|
590
|
+
// 8. Class-based selectors (increasing combos until unique)
|
|
276
591
|
const classes = (el.className && typeof el.className === "string")
|
|
277
592
|
? el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
278
593
|
: [];
|
|
@@ -287,7 +602,7 @@ export async function handleInspectElement(payload) {
|
|
|
287
602
|
if (mc === 1) break;
|
|
288
603
|
}
|
|
289
604
|
|
|
290
|
-
//
|
|
605
|
+
// 9. Table-specific selectors
|
|
291
606
|
const tr = el.closest("tr");
|
|
292
607
|
if (tr && tr.parentElement) {
|
|
293
608
|
const rowIdx = Array.from(tr.parentElement.children).indexOf(tr) + 1;
|
|
@@ -303,11 +618,11 @@ export async function handleInspectElement(payload) {
|
|
|
303
618
|
}
|
|
304
619
|
}
|
|
305
620
|
|
|
306
|
-
//
|
|
621
|
+
// 10. Full structural CSS path
|
|
307
622
|
const fullCss = buildCssPath(el, 8);
|
|
308
623
|
addCandidate("css", fullCss, { specificity: "unique", label: "Full CSS Path (Structural)" });
|
|
309
624
|
|
|
310
|
-
//
|
|
625
|
+
// 11. XPath — most reliable for scrapers
|
|
311
626
|
const xpath = buildXPath(el);
|
|
312
627
|
addCandidate("xpath", xpath, { specificity: "high", label: "XPath" });
|
|
313
628
|
|
|
@@ -481,6 +796,7 @@ export async function handleInspectElement(payload) {
|
|
|
481
796
|
candidates,
|
|
482
797
|
hierarchy,
|
|
483
798
|
boundingRect,
|
|
799
|
+
alternates,
|
|
484
800
|
tableDetection,
|
|
485
801
|
repeatingDetection,
|
|
486
802
|
};
|
|
@@ -608,9 +924,34 @@ export async function handleInspectElement(payload) {
|
|
|
608
924
|
currH = currH.parentElement;
|
|
609
925
|
}
|
|
610
926
|
|
|
611
|
-
const candidates = [
|
|
612
|
-
|
|
613
|
-
|
|
927
|
+
const candidates = [];
|
|
928
|
+
const seen = new Set();
|
|
929
|
+
const addCandidate = (type, value, extra = {}) => {
|
|
930
|
+
if (!value || seen.has(value)) return;
|
|
931
|
+
seen.add(value);
|
|
932
|
+
const matchCount = type === "css" ? countMatches(value) : null;
|
|
933
|
+
candidates.push({ type, value, attribute: "text", matchCount, ...extra });
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
addCandidate(strategyType, matchedSelector, { specificity: count === 1 ? "unique" : "medium", label: "Selected Query", matchCount: count });
|
|
937
|
+
|
|
938
|
+
if (el.id && /^[a-zA-Z][\w-]*$/.test(el.id)) {
|
|
939
|
+
addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
|
|
940
|
+
}
|
|
941
|
+
const nameAttr = el.getAttribute("name");
|
|
942
|
+
if (nameAttr && /^[a-zA-Z0-9_\-\[\]:]+$/.test(nameAttr)) {
|
|
943
|
+
addCandidate("css", `${el.tagName.toLowerCase()}[name="${nameAttr}"]`, { specificity: "unique", label: `Form Name: ${nameAttr}` });
|
|
944
|
+
}
|
|
945
|
+
const phAttr = el.getAttribute("placeholder");
|
|
946
|
+
if (phAttr && phAttr.length < 80) {
|
|
947
|
+
addCandidate("css", `${el.tagName.toLowerCase()}[placeholder="${phAttr.replace(/"/g, '\\"')}"]`, { specificity: "high", label: `Placeholder: ${phAttr.slice(0, 25)}` });
|
|
948
|
+
}
|
|
949
|
+
if (el.className && typeof el.className === "string") {
|
|
950
|
+
const cls = el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c));
|
|
951
|
+
if (cls.length > 0) {
|
|
952
|
+
addCandidate("css", `${el.tagName.toLowerCase()}.${cls[0]}`, { specificity: "medium", label: "Class Selector" });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
614
955
|
|
|
615
956
|
return {
|
|
616
957
|
found: true,
|