app-manager-edge-worker 1.2.2 → 1.2.4
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 +11 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +6 -2
- package/src/handlers/interactiveHandler.js +92 -1
- package/src/handlers/previewHandler.js +80 -62
- package/src/handlers/whatsappHandler.js +23 -2
package/README.md
CHANGED
|
@@ -203,6 +203,17 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
203
203
|
|
|
204
204
|
## 📝 Changelog
|
|
205
205
|
|
|
206
|
+
### v1.2.4
|
|
207
|
+
- **Preserved Live Inspection & Field Extraction on Results Pages**:
|
|
208
|
+
- Added `preservePageForInspection` capability to `HITL_SESSION_SOLVE`: keeps Puppeteer browser page alive upon successful captcha solve and returns full-page Results Page screenshot with dimensions and metadata.
|
|
209
|
+
- Linked active interactive sessions directly to `INSPECT_ELEMENT`: enables clicking and resolving exact selectors on the live Results Page without reloading or losing post-submit DOM state.
|
|
210
|
+
- Added `HITL_SESSION_EXTRACT` job handler to test and evaluate field mappings and transforms directly against the active Results Page in real-time.
|
|
211
|
+
|
|
212
|
+
### v1.2.3
|
|
213
|
+
- **Resilient Dynamic Attachment Streaming**:
|
|
214
|
+
- Implemented automatic loopback host and port reconciliation for WhatsApp document streaming (`WHATSAPP_SEND_DOCUMENT`).
|
|
215
|
+
- Automatically redirects loopback/localhost requests to the worker's active connected cluster address, preventing `ECONNREFUSED ::1:3000` errors when cluster port or host differs from server defaults or when worker runs remotely on mobile/VPS.
|
|
216
|
+
|
|
206
217
|
### v1.2.2
|
|
207
218
|
- **Reactive Portal Form & Dynamic Captcha Reveal Compatibility**:
|
|
208
219
|
- 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.
|
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.4",
|
|
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
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
handleInteractiveSessionRefresh,
|
|
14
14
|
handleInteractiveSessionSolve,
|
|
15
15
|
handleInteractiveSessionAbort,
|
|
16
|
+
handleInteractiveSessionExtract,
|
|
16
17
|
} from "./handlers/interactiveHandler.js";
|
|
17
18
|
import fs from "fs";
|
|
18
19
|
import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
|
|
19
20
|
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
20
21
|
|
|
21
|
-
let workerPkgVersion = "1.2.
|
|
22
|
+
let workerPkgVersion = "1.2.4";
|
|
22
23
|
try {
|
|
23
24
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
24
25
|
if (pkg?.version) workerPkgVersion = pkg.version;
|
|
@@ -122,6 +123,7 @@ export async function startWorkerClient(config) {
|
|
|
122
123
|
|
|
123
124
|
// Initialize WhatsApp Companion Device Engine
|
|
124
125
|
const waHandler = getWhatsAppHandler({
|
|
126
|
+
serverUrl: config.serverUrl || config.server,
|
|
125
127
|
onStatusChange: (statusData) => {
|
|
126
128
|
socket.emit("whatsapp:status_update", statusData);
|
|
127
129
|
},
|
|
@@ -252,7 +254,7 @@ export async function startWorkerClient(config) {
|
|
|
252
254
|
} else if (jobType === "WHATSAPP_SEND_MESSAGE") {
|
|
253
255
|
resultData = await waHandler.sendMessage(payload);
|
|
254
256
|
} else if (jobType === "WHATSAPP_SEND_DOCUMENT") {
|
|
255
|
-
resultData = await waHandler.sendDocument(payload);
|
|
257
|
+
resultData = await waHandler.sendDocument({ ...payload, serverUrl: config.serverUrl || config.server });
|
|
256
258
|
} else if (jobType === "WHATSAPP_RECONCILE") {
|
|
257
259
|
resultData = await waHandler.reconcileDelivery(payload.recipientPhone, payload.idempotencyKey);
|
|
258
260
|
} else if (jobType === "HITL_SESSION_INIT") {
|
|
@@ -261,6 +263,8 @@ export async function startWorkerClient(config) {
|
|
|
261
263
|
resultData = await handleInteractiveSessionRefresh(payload);
|
|
262
264
|
} else if (jobType === "HITL_SESSION_SOLVE") {
|
|
263
265
|
resultData = await handleInteractiveSessionSolve(payload);
|
|
266
|
+
} else if (jobType === "HITL_SESSION_EXTRACT") {
|
|
267
|
+
resultData = await handleInteractiveSessionExtract(payload);
|
|
264
268
|
} else if (jobType === "HITL_SESSION_ABORT") {
|
|
265
269
|
resultData = await handleInteractiveSessionAbort(payload);
|
|
266
270
|
} else {
|
|
@@ -8,6 +8,13 @@ import { runTransformPipeline, castValueToType } from "./transformUtils.js";
|
|
|
8
8
|
*/
|
|
9
9
|
const activeSessions = new Map();
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Returns an active interactive session by ID if alive.
|
|
13
|
+
*/
|
|
14
|
+
export function getActiveInteractiveSession(sessionId) {
|
|
15
|
+
return activeSessions.get(sessionId) || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Extracts a single field from a Puppeteer page given a strategy.
|
|
13
20
|
*/
|
|
@@ -332,7 +339,14 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
332
339
|
* If success, extracts data and closes page.
|
|
333
340
|
*/
|
|
334
341
|
export async function handleInteractiveSessionSolve(payload) {
|
|
335
|
-
const {
|
|
342
|
+
const {
|
|
343
|
+
sessionId,
|
|
344
|
+
captchaSolution,
|
|
345
|
+
captchaConfig = {},
|
|
346
|
+
fieldMappings = [],
|
|
347
|
+
collections = [],
|
|
348
|
+
preservePageForInspection = false,
|
|
349
|
+
} = payload;
|
|
336
350
|
const session = activeSessions.get(sessionId);
|
|
337
351
|
|
|
338
352
|
if (!session || !session.page || session.page.isClosed()) {
|
|
@@ -396,6 +410,44 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
396
410
|
raw[mapping.field || mapping.key] = val;
|
|
397
411
|
}
|
|
398
412
|
|
|
413
|
+
if (preservePageForInspection) {
|
|
414
|
+
// Keep the page alive for visual inspection & testing
|
|
415
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
416
|
+
const inspectTtl = 300; // 5 minutes for interactive element picking
|
|
417
|
+
session.timeoutTimer = setTimeout(() => {
|
|
418
|
+
purgeSession(sessionId).catch(() => {});
|
|
419
|
+
}, inspectTtl * 1000);
|
|
420
|
+
|
|
421
|
+
// Settle time for tables & styles
|
|
422
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
423
|
+
|
|
424
|
+
const screenshotBuffer = await page.screenshot({
|
|
425
|
+
encoding: "binary",
|
|
426
|
+
fullPage: false,
|
|
427
|
+
type: "jpeg",
|
|
428
|
+
quality: 85,
|
|
429
|
+
});
|
|
430
|
+
const screenshotBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
431
|
+
const title = await page.title().catch(() => "");
|
|
432
|
+
const dimensions = await page
|
|
433
|
+
.evaluate(() => ({
|
|
434
|
+
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
435
|
+
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
436
|
+
}))
|
|
437
|
+
.catch(() => ({ width: 1366, height: 900 }));
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
status: "COMPLETED",
|
|
441
|
+
success: true,
|
|
442
|
+
data: raw,
|
|
443
|
+
screenshotBase64: `data:image/jpeg;base64,${screenshotBase64}`,
|
|
444
|
+
dimensions,
|
|
445
|
+
title: title || "Results Page",
|
|
446
|
+
sessionId,
|
|
447
|
+
isInspectionMode: true,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
399
451
|
// Clean up session and close page
|
|
400
452
|
await purgeSession(sessionId);
|
|
401
453
|
|
|
@@ -475,3 +527,42 @@ export async function handleInteractiveSessionAbort(payload) {
|
|
|
475
527
|
}
|
|
476
528
|
return { success: true, aborted: true };
|
|
477
529
|
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 5. Handles HITL_SESSION_EXTRACT:
|
|
533
|
+
* Extracts fields directly from an active interactive session's live page.
|
|
534
|
+
*/
|
|
535
|
+
export async function handleInteractiveSessionExtract(payload) {
|
|
536
|
+
const { sessionId, fieldMappings = [], collections = [] } = payload;
|
|
537
|
+
const session = activeSessions.get(sessionId);
|
|
538
|
+
|
|
539
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
540
|
+
throw new Error("Interactive session not found or already expired");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const { page } = session;
|
|
544
|
+
const raw = {};
|
|
545
|
+
|
|
546
|
+
for (const mapping of fieldMappings) {
|
|
547
|
+
let val = null;
|
|
548
|
+
const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
|
|
549
|
+
for (const strat of strategies) {
|
|
550
|
+
val = await extractBrowserField(page, strat);
|
|
551
|
+
if (val !== null && val !== undefined && val !== "") break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
555
|
+
val = runTransformPipeline(val, mapping.transforms);
|
|
556
|
+
}
|
|
557
|
+
if (mapping.type) {
|
|
558
|
+
val = castValueToType(val, mapping.type);
|
|
559
|
+
}
|
|
560
|
+
raw[mapping.field || mapping.key] = val;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
data: raw,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import localBrowserManager from "../browser.js";
|
|
2
|
+
import { getActiveInteractiveSession } from "./interactiveHandler.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handles PREVIEW_PAGE task: captures full-page screenshot and dimensions on the edge worker.
|
|
@@ -120,74 +121,89 @@ export async function handlePreviewPage(payload) {
|
|
|
120
121
|
* Handles INSPECT_ELEMENT task: resolves DOM element coordinates or evaluates candidate selectors on the edge worker.
|
|
121
122
|
*/
|
|
122
123
|
export async function handleInspectElement(payload) {
|
|
123
|
-
const { url, preScrapeActions, authentication, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const { sessionId, url, preScrapeActions, authentication, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
|
|
125
|
+
let page = null;
|
|
126
|
+
let isLiveSession = false;
|
|
127
|
+
|
|
128
|
+
if (sessionId) {
|
|
129
|
+
const liveSession = getActiveInteractiveSession(sessionId);
|
|
130
|
+
if (liveSession && liveSession.page && !liveSession.page.isClosed()) {
|
|
131
|
+
page = liveSession.page;
|
|
132
|
+
isLiveSession = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
await
|
|
136
|
+
if (!page) {
|
|
137
|
+
const browser = await localBrowserManager.getBrowser();
|
|
138
|
+
page = await browser.newPage();
|
|
139
|
+
}
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
try {
|
|
142
|
+
if (!isLiveSession) {
|
|
143
|
+
await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 });
|
|
144
|
+
|
|
145
|
+
// Apply authentication if provided
|
|
146
|
+
if (authentication) {
|
|
147
|
+
if (authentication.type === "COOKIES" && Array.isArray(authentication.cookies)) {
|
|
148
|
+
const parsedUrl = new URL(url);
|
|
149
|
+
await page.setCookie(...authentication.cookies.map(c => ({
|
|
150
|
+
name: c.name,
|
|
151
|
+
value: c.value,
|
|
152
|
+
domain: c.domain || parsedUrl.hostname,
|
|
153
|
+
path: c.path || "/",
|
|
154
|
+
}))).catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
if (authentication.type === "HEADERS" && authentication.headers) {
|
|
157
|
+
await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
|
-
}
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
161
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
162
|
+
try {
|
|
163
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 3500 }).catch(() => {});
|
|
164
|
+
} catch (_) {}
|
|
165
|
+
await page.evaluate(() => document.fonts?.ready).catch(() => {});
|
|
166
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
167
|
+
|
|
168
|
+
// Execute pre-scrape actions if present
|
|
169
|
+
if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
|
|
170
|
+
for (const action of preScrapeActions) {
|
|
171
|
+
try {
|
|
172
|
+
if (action.actionType === "CLICK" && action.selector) {
|
|
173
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
174
|
+
await page.click(action.selector);
|
|
175
|
+
} else if (action.actionType === "TYPE" && action.selector && action.value) {
|
|
176
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
177
|
+
await page.type(action.selector, action.value, { delay: action.delayMs || 30 });
|
|
178
|
+
await page.evaluate((sel) => {
|
|
179
|
+
const el = document.querySelector(sel);
|
|
180
|
+
if (el) {
|
|
181
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
182
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
183
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
184
|
+
}
|
|
185
|
+
}, action.selector).catch(() => {});
|
|
186
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
187
|
+
} else if (action.actionType === "SCROLL") {
|
|
188
|
+
const scrollPixels = parseInt(action.value, 10) || 500;
|
|
189
|
+
await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
|
|
190
|
+
} else if (action.actionType === "WAIT_FOR" && action.selector) {
|
|
191
|
+
await page.waitForSelector(action.selector, { timeout: 8000 });
|
|
192
|
+
} else if (action.actionType === "SELECT_OPTION" && action.selector && action.value) {
|
|
193
|
+
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
194
|
+
await page.select(action.selector, action.value);
|
|
195
|
+
} else if (action.actionType === "PRESS_KEY" && action.value) {
|
|
196
|
+
await page.keyboard.press(action.value);
|
|
197
|
+
}
|
|
198
|
+
if (action.delayMs && action.delayMs > 0) {
|
|
199
|
+
await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 5000)));
|
|
200
|
+
}
|
|
201
|
+
} catch (actErr) {
|
|
202
|
+
console.warn(`[WorkerInspect] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
|
|
185
203
|
}
|
|
186
|
-
} catch (actErr) {
|
|
187
|
-
console.warn(`[WorkerInspect] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
|
|
188
204
|
}
|
|
205
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
189
206
|
}
|
|
190
|
-
await new Promise((r) => setTimeout(r, 600));
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
if (x !== undefined && y !== undefined) {
|
|
@@ -983,6 +999,8 @@ export async function handleInspectElement(payload) {
|
|
|
983
999
|
|
|
984
1000
|
return { message: "Specify coordinates (x, y) or selector to inspect." };
|
|
985
1001
|
} finally {
|
|
986
|
-
|
|
1002
|
+
if (!isLiveSession && page) {
|
|
1003
|
+
await page.close().catch(() => {});
|
|
1004
|
+
}
|
|
987
1005
|
}
|
|
988
1006
|
}
|
|
@@ -29,6 +29,7 @@ export class WhatsAppWorkerHandler {
|
|
|
29
29
|
const homeDir = os.homedir();
|
|
30
30
|
const baseDir = options.authDir || path.join(homeDir, ".app-manager-edge-worker", "wa-session");
|
|
31
31
|
this.authDir = baseDir;
|
|
32
|
+
this.serverUrl = options.serverUrl || null;
|
|
32
33
|
this.sock = null;
|
|
33
34
|
this.status = "DISCONNECTED"; // DISCONNECTED | PAIRING | CONNECTED | STANDBY
|
|
34
35
|
this.user = null; // { id, name, phone }
|
|
@@ -533,6 +534,7 @@ export class WhatsAppWorkerHandler {
|
|
|
533
534
|
mimetype = "application/pdf",
|
|
534
535
|
caption = "",
|
|
535
536
|
idempotencyKey = null,
|
|
537
|
+
serverUrl = null,
|
|
536
538
|
}) {
|
|
537
539
|
logInfo(`[WhatsApp] Incoming document task for ${recipientPhone} (${fileName})`);
|
|
538
540
|
await this.initialize(false);
|
|
@@ -565,8 +567,27 @@ export class WhatsAppWorkerHandler {
|
|
|
565
567
|
const targetJid = verified.jid || jid;
|
|
566
568
|
|
|
567
569
|
// 3. Stream PDF from authenticated URL (Zero buffer in central WebSocket)
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
+
let resolvedDownloadUrl = downloadUrl;
|
|
571
|
+
const activeServerUrl = serverUrl || this.serverUrl;
|
|
572
|
+
if (resolvedDownloadUrl && activeServerUrl) {
|
|
573
|
+
try {
|
|
574
|
+
const targetObj = new URL(resolvedDownloadUrl);
|
|
575
|
+
const srvObj = new URL(activeServerUrl);
|
|
576
|
+
const isTargetLoopback = ["localhost", "127.0.0.1", "::1"].includes(targetObj.hostname);
|
|
577
|
+
const isSrvLoopback = ["localhost", "127.0.0.1", "::1"].includes(srvObj.hostname);
|
|
578
|
+
|
|
579
|
+
// If target is loopback/localhost with mismatching port, or if worker connects to a remote server
|
|
580
|
+
if (isTargetLoopback && (!isSrvLoopback || targetObj.port !== srvObj.port)) {
|
|
581
|
+
targetObj.protocol = srvObj.protocol;
|
|
582
|
+
targetObj.hostname = srvObj.hostname;
|
|
583
|
+
targetObj.port = srvObj.port;
|
|
584
|
+
resolvedDownloadUrl = targetObj.toString();
|
|
585
|
+
}
|
|
586
|
+
} catch (_) {}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
logInfo(`[WhatsApp] Streaming document attachment (${fileName}) from ${resolvedDownloadUrl}...`);
|
|
590
|
+
const response = await axios.get(resolvedDownloadUrl, {
|
|
570
591
|
responseType: "arraybuffer",
|
|
571
592
|
timeout: 25000,
|
|
572
593
|
});
|