app-manager-edge-worker 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/previewHandler.js +181 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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": {
|
|
@@ -93,48 +93,210 @@ export async function handleInspectElement(payload) {
|
|
|
93
93
|
height: Math.round(rect.height),
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
+
// ─── Helper: count matching elements for a CSS selector ───
|
|
97
|
+
const countMatches = (sel) => {
|
|
98
|
+
try { return document.querySelectorAll(sel).length; } catch { return 999; }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ─── Helper: Full positional CSS path anchored at nearest ID ───
|
|
102
|
+
const buildCssPath = (element, maxDepth = 8) => {
|
|
103
|
+
const parts = [];
|
|
104
|
+
let curr = element;
|
|
105
|
+
while (curr && curr.nodeType === 1 && curr !== document.documentElement) {
|
|
106
|
+
if (curr === document.body) { parts.unshift("body"); break; }
|
|
107
|
+
let seg = curr.tagName.toLowerCase();
|
|
108
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
109
|
+
parts.unshift(`#${curr.id}`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (curr.parentElement) {
|
|
113
|
+
const pos = Array.from(curr.parentElement.children).indexOf(curr) + 1;
|
|
114
|
+
seg += `:nth-child(${pos})`;
|
|
115
|
+
}
|
|
116
|
+
parts.unshift(seg);
|
|
117
|
+
curr = curr.parentElement;
|
|
118
|
+
if (parts.length >= maxDepth) break;
|
|
119
|
+
}
|
|
120
|
+
return parts.join(" > ");
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ─── Helper: Shortest unique CSS path using class combos + nth-child ───
|
|
124
|
+
const buildShortUniqueSelector = (element) => {
|
|
125
|
+
const pathParts = [];
|
|
126
|
+
let curr = element;
|
|
127
|
+
while (curr && curr !== document.documentElement) {
|
|
128
|
+
if (curr === document.body) { pathParts.unshift("body"); break; }
|
|
129
|
+
let seg = curr.tagName.toLowerCase();
|
|
130
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
131
|
+
pathParts.unshift(`#${curr.id}`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const classes = (curr.className && typeof curr.className === "string")
|
|
135
|
+
? curr.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
136
|
+
: [];
|
|
137
|
+
let foundUnique = false;
|
|
138
|
+
for (let len = Math.min(classes.length, 5); len >= 1; len--) {
|
|
139
|
+
const combo = classes.slice(0, len);
|
|
140
|
+
const candidate = `${seg}.${combo.join(".")}`;
|
|
141
|
+
const full = [...pathParts, candidate].join(" > ");
|
|
142
|
+
if (countMatches(full) === 1) {
|
|
143
|
+
pathParts.unshift(candidate);
|
|
144
|
+
foundUnique = true;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!foundUnique) {
|
|
149
|
+
if (curr.parentElement) {
|
|
150
|
+
const pos = Array.from(curr.parentElement.children).indexOf(curr) + 1;
|
|
151
|
+
seg += `:nth-child(${pos})`;
|
|
152
|
+
}
|
|
153
|
+
pathParts.unshift(seg);
|
|
154
|
+
}
|
|
155
|
+
const full = pathParts.join(" > ");
|
|
156
|
+
if (countMatches(full) === 1) break;
|
|
157
|
+
curr = curr.parentElement;
|
|
158
|
+
}
|
|
159
|
+
return pathParts.join(" > ");
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ─── Helper: Robust absolute XPath ───
|
|
163
|
+
const buildXPath = (element) => {
|
|
164
|
+
if (!element || element === document) return "";
|
|
165
|
+
if (element.id && /^[a-zA-Z][\w-]*$/.test(element.id)) {
|
|
166
|
+
return `//*[@id="${element.id}"]`;
|
|
167
|
+
}
|
|
168
|
+
if (element === document.body) return "/html/body";
|
|
169
|
+
const parts = [];
|
|
170
|
+
let curr = element;
|
|
171
|
+
while (curr && curr.nodeType === 1 && curr !== document.documentElement) {
|
|
172
|
+
if (curr === document.body) { parts.unshift("body"); break; }
|
|
173
|
+
if (curr.id && /^[a-zA-Z][\w-]*$/.test(curr.id)) {
|
|
174
|
+
parts.unshift(`//*[@id="${curr.id}"]`);
|
|
175
|
+
return parts.join("/");
|
|
176
|
+
}
|
|
177
|
+
const tag = curr.tagName.toLowerCase();
|
|
178
|
+
let idx = 1;
|
|
179
|
+
let sib = curr.previousElementSibling;
|
|
180
|
+
while (sib) {
|
|
181
|
+
if (sib.tagName.toLowerCase() === tag) idx++;
|
|
182
|
+
sib = sib.previousElementSibling;
|
|
183
|
+
}
|
|
184
|
+
const total = curr.parentElement
|
|
185
|
+
? Array.from(curr.parentElement.children).filter(c => c.tagName.toLowerCase() === tag).length
|
|
186
|
+
: 1;
|
|
187
|
+
parts.unshift(total > 1 ? `${tag}[${idx}]` : tag);
|
|
188
|
+
curr = curr.parentElement;
|
|
189
|
+
}
|
|
190
|
+
return "/" + parts.join("/");
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ─── Build ranked candidates ───
|
|
96
194
|
const candidates = [];
|
|
97
|
-
|
|
98
|
-
|
|
195
|
+
const seen = new Set();
|
|
196
|
+
const addCandidate = (type, value, extra = {}) => {
|
|
197
|
+
if (!value || seen.has(value)) return;
|
|
198
|
+
seen.add(value);
|
|
199
|
+
const matchCount = type === "css" ? countMatches(value) : null;
|
|
200
|
+
candidates.push({ type, value, attribute: "text", matchCount, ...extra });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 1. ID (most reliable, unique)
|
|
204
|
+
if (el.id && /^[a-zA-Z][\w-]*$/.test(el.id)) {
|
|
205
|
+
addCandidate("css", `#${el.id}`, { specificity: "unique", label: "ID Selector" });
|
|
99
206
|
}
|
|
100
207
|
|
|
208
|
+
// 2. data-* and aria-* attributes
|
|
101
209
|
for (const attr of el.attributes) {
|
|
102
|
-
if (attr.name.startsWith("data-") && attr.value) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
value: `[${attr.name}='${attr.value}']`,
|
|
106
|
-
attribute: "text",
|
|
210
|
+
if ((attr.name.startsWith("data-") || attr.name.startsWith("aria-")) && attr.value && attr.value.length < 80) {
|
|
211
|
+
addCandidate("css", `${el.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`, {
|
|
212
|
+
specificity: "high", label: `Attribute: ${attr.name}`
|
|
107
213
|
});
|
|
108
214
|
}
|
|
109
215
|
}
|
|
110
216
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
// 3. Shortest unique CSS path
|
|
218
|
+
const shortUnique = buildShortUniqueSelector(el);
|
|
219
|
+
if (shortUnique) {
|
|
220
|
+
const mc = countMatches(shortUnique);
|
|
221
|
+
addCandidate("css", shortUnique, {
|
|
222
|
+
specificity: mc === 1 ? "unique" : "medium",
|
|
223
|
+
label: "Smart CSS Path",
|
|
224
|
+
matchCount: mc,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Class-based selectors (increasing combos until unique)
|
|
229
|
+
const classes = (el.className && typeof el.className === "string")
|
|
230
|
+
? el.className.split(/\s+/).filter(c => c && /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
231
|
+
: [];
|
|
232
|
+
for (let len = 1; len <= Math.min(classes.length, 5); len++) {
|
|
233
|
+
const sel = `${el.tagName.toLowerCase()}.${classes.slice(0, len).join(".")}`;
|
|
234
|
+
const mc = countMatches(sel);
|
|
235
|
+
addCandidate("css", sel, {
|
|
236
|
+
specificity: mc === 1 ? "unique" : mc <= 3 ? "medium" : "generic",
|
|
237
|
+
label: "Class Selector",
|
|
238
|
+
matchCount: mc,
|
|
239
|
+
});
|
|
240
|
+
if (mc === 1) break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 5. Table-specific selectors
|
|
244
|
+
const tr = el.closest("tr");
|
|
245
|
+
if (tr && tr.parentElement) {
|
|
246
|
+
const rowIdx = Array.from(tr.parentElement.children).indexOf(tr) + 1;
|
|
247
|
+
const td = el.closest("td, th");
|
|
248
|
+
if (td) {
|
|
249
|
+
const colIdx = Array.from(tr.children).indexOf(td) + 1;
|
|
250
|
+
addCandidate("css", `table tr:nth-child(${rowIdx}) td:nth-child(${colIdx})`, {
|
|
251
|
+
specificity: "medium", label: "Table Cell"
|
|
252
|
+
});
|
|
253
|
+
addCandidate("css", `table tbody tr:nth-child(${rowIdx}) td:nth-child(${colIdx})`, {
|
|
254
|
+
specificity: "medium", label: "Table Cell (tbody)"
|
|
120
255
|
});
|
|
121
256
|
}
|
|
122
257
|
}
|
|
123
258
|
|
|
124
|
-
|
|
125
|
-
|
|
259
|
+
// 6. Full structural CSS path
|
|
260
|
+
const fullCss = buildCssPath(el, 8);
|
|
261
|
+
addCandidate("css", fullCss, { specificity: "unique", label: "Full CSS Path (Structural)" });
|
|
262
|
+
|
|
263
|
+
// 7. XPath — most reliable for scrapers
|
|
264
|
+
const xpath = buildXPath(el);
|
|
265
|
+
addCandidate("xpath", xpath, { specificity: "high", label: "XPath" });
|
|
266
|
+
|
|
267
|
+
// Sort by specificity: unique > high > medium > generic
|
|
268
|
+
const rank = { unique: 0, high: 1, medium: 2, generic: 3 };
|
|
269
|
+
candidates.sort((a, b) => (rank[a.specificity] ?? 4) - (rank[b.specificity] ?? 4));
|
|
270
|
+
|
|
271
|
+
// ─── Breadcrumb hierarchy for UI ───
|
|
272
|
+
const hierarchy = [];
|
|
273
|
+
let curr = el;
|
|
274
|
+
while (curr && curr !== document.body && hierarchy.length < 7) {
|
|
275
|
+
let desc = curr.tagName.toLowerCase();
|
|
276
|
+
if (curr.id) {
|
|
277
|
+
desc += `#${curr.id}`;
|
|
278
|
+
} else if (curr.classList && curr.classList.length > 0) {
|
|
279
|
+
const cls = Array.from(curr.classList)
|
|
280
|
+
.filter(c => /^[a-zA-Z_-][\w-]*$/.test(c))
|
|
281
|
+
.slice(0, 2);
|
|
282
|
+
if (cls.length) desc += `.${cls.join(".")}`;
|
|
283
|
+
}
|
|
284
|
+
hierarchy.unshift(desc);
|
|
285
|
+
curr = curr.parentElement;
|
|
286
|
+
}
|
|
126
287
|
|
|
127
288
|
const text = el.innerText ? el.innerText.trim() : el.textContent?.trim() || "";
|
|
128
289
|
|
|
129
290
|
return {
|
|
130
291
|
found: true,
|
|
131
|
-
tagName,
|
|
292
|
+
tagName: el.tagName.toLowerCase(),
|
|
132
293
|
text,
|
|
133
294
|
attributes: Array.from(el.attributes).reduce((acc, a) => {
|
|
134
295
|
acc[a.name] = a.value;
|
|
135
296
|
return acc;
|
|
136
297
|
}, {}),
|
|
137
298
|
candidates,
|
|
299
|
+
hierarchy,
|
|
138
300
|
boundingRect,
|
|
139
301
|
};
|
|
140
302
|
}, x, y);
|