app-manager-edge-worker 1.2.2 → 1.2.5
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 +16 -1
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +6 -2
- package/src/handlers/interactiveHandler.js +160 -8
- package/src/handlers/previewHandler.js +80 -62
- package/src/handlers/whatsappHandler.js +23 -2
package/README.md
CHANGED
|
@@ -201,7 +201,22 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
### v1.2.5
|
|
205
|
+
- **Interactive Session Visual Inspector Sync & Robust Captcha Refresh**:
|
|
206
|
+
- `HITL_SESSION_INIT` now captures and returns full-page `pageScreenshotBase64`, dimensions, and title after typing initial form inputs, keeping the Visual Element Inspector synchronized with the active session.
|
|
207
|
+
- Enhanced `HITL_SESSION_REFRESH` with multi-selector fallback candidates (`button[ng-click*="refreshCaptcha"]`, `button[ng-click*="captcha"]`, `#reloadCaptcha`, etc.) ensuring reliable refresh on AngularJS and custom portal architectures.
|
|
208
|
+
- Captures and returns updated `pageScreenshotBase64` upon captcha refresh to keep inspect canvas up to date.
|
|
209
|
+
|
|
210
|
+
### v1.2.4
|
|
211
|
+
- **Preserved Live Inspection & Field Extraction on Results Pages**:
|
|
212
|
+
- 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.
|
|
213
|
+
- 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.
|
|
214
|
+
- Added `HITL_SESSION_EXTRACT` job handler to test and evaluate field mappings and transforms directly against the active Results Page in real-time.
|
|
215
|
+
|
|
216
|
+
### v1.2.3
|
|
217
|
+
- **Resilient Dynamic Attachment Streaming**:
|
|
218
|
+
- Implemented automatic loopback host and port reconciliation for WhatsApp document streaming (`WHATSAPP_SEND_DOCUMENT`).
|
|
219
|
+
- 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.
|
|
205
220
|
|
|
206
221
|
### v1.2.2
|
|
207
222
|
- **Reactive Portal Form & Dynamic Captcha Reveal Compatibility**:
|
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.5",
|
|
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.5";
|
|
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
|
*/
|
|
@@ -216,6 +223,25 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
216
223
|
});
|
|
217
224
|
const imageBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
218
225
|
|
|
226
|
+
// Settle briefly for form state & portal validation UI
|
|
227
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
228
|
+
|
|
229
|
+
// Capture full-page screenshot showing the filled inputs and revealed captcha
|
|
230
|
+
const pageScreenshotBuffer = await page.screenshot({
|
|
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
|
+
|
|
219
245
|
const sessionTtlSeconds = captchaConfig.sessionTimeoutSeconds || 90;
|
|
220
246
|
|
|
221
247
|
// Set auto-purge timer to prevent resource leaks
|
|
@@ -237,6 +263,9 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
237
263
|
return {
|
|
238
264
|
sessionId,
|
|
239
265
|
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
266
|
+
pageScreenshotBase64,
|
|
267
|
+
dimensions,
|
|
268
|
+
title,
|
|
240
269
|
expiresInSeconds: sessionTtlSeconds,
|
|
241
270
|
};
|
|
242
271
|
} catch (err) {
|
|
@@ -262,10 +291,6 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
262
291
|
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
263
292
|
const refreshSelector = cfg.refreshSelector;
|
|
264
293
|
|
|
265
|
-
if (!refreshSelector) {
|
|
266
|
-
throw new Error("refreshSelector is not configured for this captcha");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
294
|
// 1. Snapshot current image hash and src attribute
|
|
270
295
|
const oldEl = await page.$(imageSelector);
|
|
271
296
|
if (!oldEl) {
|
|
@@ -280,9 +305,41 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
280
305
|
return el ? el.getAttribute("src") : null;
|
|
281
306
|
}, imageSelector);
|
|
282
307
|
|
|
283
|
-
// 2. Click the reload button
|
|
284
|
-
|
|
285
|
-
|
|
308
|
+
// 2. Click the reload button with robust candidate fallbacks
|
|
309
|
+
const candidateSelectors = [
|
|
310
|
+
refreshSelector,
|
|
311
|
+
"button[ng-click*='refreshCaptcha' i]",
|
|
312
|
+
"button[ng-click*='captcha' i]",
|
|
313
|
+
"*[ng-click*='refreshCaptcha' i]",
|
|
314
|
+
"*[ng-click*='captcha' i]",
|
|
315
|
+
"#reloadCaptcha",
|
|
316
|
+
"#refreshCaptcha",
|
|
317
|
+
"a[title*='refresh' i]",
|
|
318
|
+
"button[title*='refresh' i]",
|
|
319
|
+
"a[title*='reload' i]",
|
|
320
|
+
"button[title*='reload' i]",
|
|
321
|
+
".captcha-refresh",
|
|
322
|
+
".reload-captcha",
|
|
323
|
+
".glyphicon-refresh",
|
|
324
|
+
"i.fa-refresh",
|
|
325
|
+
"i.fa-arrows-rotate",
|
|
326
|
+
].filter(Boolean);
|
|
327
|
+
|
|
328
|
+
let clicked = false;
|
|
329
|
+
for (const sel of candidateSelectors) {
|
|
330
|
+
try {
|
|
331
|
+
const btn = await page.$(sel);
|
|
332
|
+
if (btn) {
|
|
333
|
+
await btn.click();
|
|
334
|
+
clicked = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
} catch (_) {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!clicked) {
|
|
341
|
+
throw new Error(`Captcha refresh button not found on page (tried: ${refreshSelector || "standard reload selectors"})`);
|
|
342
|
+
}
|
|
286
343
|
|
|
287
344
|
// 3. Wait for src attribute change first
|
|
288
345
|
await page
|
|
@@ -319,9 +376,20 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
319
376
|
}
|
|
320
377
|
|
|
321
378
|
const imageBase64 = Buffer.from(newBuffer).toString("base64");
|
|
379
|
+
|
|
380
|
+
// Capture updated full page screenshot showing new captcha
|
|
381
|
+
const pageScreenshotBuffer = await page.screenshot({
|
|
382
|
+
encoding: "binary",
|
|
383
|
+
fullPage: false,
|
|
384
|
+
type: "jpeg",
|
|
385
|
+
quality: 85,
|
|
386
|
+
});
|
|
387
|
+
const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
|
|
388
|
+
|
|
322
389
|
return {
|
|
323
390
|
sessionId,
|
|
324
391
|
imageBase64: `data:image/png;base64,${imageBase64}`,
|
|
392
|
+
pageScreenshotBase64,
|
|
325
393
|
};
|
|
326
394
|
}
|
|
327
395
|
|
|
@@ -332,7 +400,14 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
332
400
|
* If success, extracts data and closes page.
|
|
333
401
|
*/
|
|
334
402
|
export async function handleInteractiveSessionSolve(payload) {
|
|
335
|
-
const {
|
|
403
|
+
const {
|
|
404
|
+
sessionId,
|
|
405
|
+
captchaSolution,
|
|
406
|
+
captchaConfig = {},
|
|
407
|
+
fieldMappings = [],
|
|
408
|
+
collections = [],
|
|
409
|
+
preservePageForInspection = false,
|
|
410
|
+
} = payload;
|
|
336
411
|
const session = activeSessions.get(sessionId);
|
|
337
412
|
|
|
338
413
|
if (!session || !session.page || session.page.isClosed()) {
|
|
@@ -396,6 +471,44 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
396
471
|
raw[mapping.field || mapping.key] = val;
|
|
397
472
|
}
|
|
398
473
|
|
|
474
|
+
if (preservePageForInspection) {
|
|
475
|
+
// Keep the page alive for visual inspection & testing
|
|
476
|
+
if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
|
|
477
|
+
const inspectTtl = 300; // 5 minutes for interactive element picking
|
|
478
|
+
session.timeoutTimer = setTimeout(() => {
|
|
479
|
+
purgeSession(sessionId).catch(() => {});
|
|
480
|
+
}, inspectTtl * 1000);
|
|
481
|
+
|
|
482
|
+
// Settle time for tables & styles
|
|
483
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
484
|
+
|
|
485
|
+
const screenshotBuffer = await page.screenshot({
|
|
486
|
+
encoding: "binary",
|
|
487
|
+
fullPage: false,
|
|
488
|
+
type: "jpeg",
|
|
489
|
+
quality: 85,
|
|
490
|
+
});
|
|
491
|
+
const screenshotBase64 = Buffer.from(screenshotBuffer).toString("base64");
|
|
492
|
+
const title = await page.title().catch(() => "");
|
|
493
|
+
const dimensions = await page
|
|
494
|
+
.evaluate(() => ({
|
|
495
|
+
width: document.documentElement.scrollWidth || window.innerWidth,
|
|
496
|
+
height: document.documentElement.scrollHeight || window.innerHeight,
|
|
497
|
+
}))
|
|
498
|
+
.catch(() => ({ width: 1366, height: 900 }));
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
status: "COMPLETED",
|
|
502
|
+
success: true,
|
|
503
|
+
data: raw,
|
|
504
|
+
screenshotBase64: `data:image/jpeg;base64,${screenshotBase64}`,
|
|
505
|
+
dimensions,
|
|
506
|
+
title: title || "Results Page",
|
|
507
|
+
sessionId,
|
|
508
|
+
isInspectionMode: true,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
399
512
|
// Clean up session and close page
|
|
400
513
|
await purgeSession(sessionId);
|
|
401
514
|
|
|
@@ -475,3 +588,42 @@ export async function handleInteractiveSessionAbort(payload) {
|
|
|
475
588
|
}
|
|
476
589
|
return { success: true, aborted: true };
|
|
477
590
|
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 5. Handles HITL_SESSION_EXTRACT:
|
|
594
|
+
* Extracts fields directly from an active interactive session's live page.
|
|
595
|
+
*/
|
|
596
|
+
export async function handleInteractiveSessionExtract(payload) {
|
|
597
|
+
const { sessionId, fieldMappings = [], collections = [] } = payload;
|
|
598
|
+
const session = activeSessions.get(sessionId);
|
|
599
|
+
|
|
600
|
+
if (!session || !session.page || session.page.isClosed()) {
|
|
601
|
+
throw new Error("Interactive session not found or already expired");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const { page } = session;
|
|
605
|
+
const raw = {};
|
|
606
|
+
|
|
607
|
+
for (const mapping of fieldMappings) {
|
|
608
|
+
let val = null;
|
|
609
|
+
const strategies = mapping.strategies || (mapping.strategy ? [mapping.strategy] : []);
|
|
610
|
+
for (const strat of strategies) {
|
|
611
|
+
val = await extractBrowserField(page, strat);
|
|
612
|
+
if (val !== null && val !== undefined && val !== "") break;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
616
|
+
val = runTransformPipeline(val, mapping.transforms);
|
|
617
|
+
}
|
|
618
|
+
if (mapping.type) {
|
|
619
|
+
val = castValueToType(val, mapping.type);
|
|
620
|
+
}
|
|
621
|
+
raw[mapping.field || mapping.key] = val;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
success: true,
|
|
626
|
+
data: raw,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
@@ -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
|
});
|