app-manager-edge-worker 1.2.1 → 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 +16 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +6 -2
- package/src/handlers/interactiveHandler.js +104 -2
- package/src/handlers/previewHandler.js +90 -54
- package/src/handlers/whatsappHandler.js +23 -2
package/README.md
CHANGED
|
@@ -203,6 +203,22 @@ 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
|
+
|
|
217
|
+
### v1.2.2
|
|
218
|
+
- **Reactive Portal Form & Dynamic Captcha Reveal Compatibility**:
|
|
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.
|
|
220
|
+
- Guarantees immediate captcha generation on reactive frontend frameworks (AngularJS, React, Vue, jQuery) requiring input validation/blur before unhiding dynamic captcha elements.
|
|
221
|
+
|
|
206
222
|
### v1.2.1
|
|
207
223
|
- **Smart Form Field & Leaf Control Element Resolution**:
|
|
208
224
|
- Replaced single-surface `elementFromPoint` with full-stack `elementsFromPoint` coordinate sampling and multi-offset probing.
|
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
|
*/
|
|
@@ -185,7 +192,18 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
185
192
|
await page.type(input.selector, String(input.value), { delay: input.delayMs || 30 });
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
|
|
195
|
+
// Explicitly dispatch input, change, and blur events to trigger AngularJS/React/portal validation listeners
|
|
196
|
+
await page.evaluate((sel) => {
|
|
197
|
+
const el = document.querySelector(sel);
|
|
198
|
+
if (el) {
|
|
199
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
200
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
201
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
202
|
+
}
|
|
203
|
+
}, input.selector).catch(() => {});
|
|
204
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
205
|
+
|
|
206
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
189
207
|
}
|
|
190
208
|
}
|
|
191
209
|
|
|
@@ -321,7 +339,14 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
321
339
|
* If success, extracts data and closes page.
|
|
322
340
|
*/
|
|
323
341
|
export async function handleInteractiveSessionSolve(payload) {
|
|
324
|
-
const {
|
|
342
|
+
const {
|
|
343
|
+
sessionId,
|
|
344
|
+
captchaSolution,
|
|
345
|
+
captchaConfig = {},
|
|
346
|
+
fieldMappings = [],
|
|
347
|
+
collections = [],
|
|
348
|
+
preservePageForInspection = false,
|
|
349
|
+
} = payload;
|
|
325
350
|
const session = activeSessions.get(sessionId);
|
|
326
351
|
|
|
327
352
|
if (!session || !session.page || session.page.isClosed()) {
|
|
@@ -385,6 +410,44 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
385
410
|
raw[mapping.field || mapping.key] = val;
|
|
386
411
|
}
|
|
387
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
|
+
|
|
388
451
|
// Clean up session and close page
|
|
389
452
|
await purgeSession(sessionId);
|
|
390
453
|
|
|
@@ -464,3 +527,42 @@ export async function handleInteractiveSessionAbort(payload) {
|
|
|
464
527
|
}
|
|
465
528
|
return { success: true, aborted: true };
|
|
466
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.
|
|
@@ -43,7 +44,16 @@ export async function handlePreviewPage(payload) {
|
|
|
43
44
|
await page.click(action.selector);
|
|
44
45
|
} else if (action.actionType === "TYPE" && action.selector && action.value) {
|
|
45
46
|
await page.waitForSelector(action.selector, { timeout: 4000 });
|
|
46
|
-
await page.type(action.selector, action.value, { delay: 30 });
|
|
47
|
+
await page.type(action.selector, action.value, { delay: action.delayMs || 30 });
|
|
48
|
+
await page.evaluate((sel) => {
|
|
49
|
+
const el = document.querySelector(sel);
|
|
50
|
+
if (el) {
|
|
51
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
52
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
53
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
54
|
+
}
|
|
55
|
+
}, action.selector).catch(() => {});
|
|
56
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
47
57
|
} else if (action.actionType === "SCROLL") {
|
|
48
58
|
const scrollPixels = parseInt(action.value, 10) || 500;
|
|
49
59
|
await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
|
|
@@ -111,65 +121,89 @@ export async function handlePreviewPage(payload) {
|
|
|
111
121
|
* Handles INSPECT_ELEMENT task: resolves DOM element coordinates or evaluates candidate selectors on the edge worker.
|
|
112
122
|
*/
|
|
113
123
|
export async function handleInspectElement(payload) {
|
|
114
|
-
const { url, preScrapeActions, authentication, selector, strategyType, strategies, fingerprint, x, y, timeout = 20000 } = payload;
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
await
|
|
136
|
+
if (!page) {
|
|
137
|
+
const browser = await localBrowserManager.getBrowser();
|
|
138
|
+
page = await browser.newPage();
|
|
139
|
+
}
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
159
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
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
160
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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}`);
|
|
167
203
|
}
|
|
168
|
-
} catch (actErr) {
|
|
169
|
-
console.warn(`[WorkerInspect] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
|
|
170
204
|
}
|
|
205
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
171
206
|
}
|
|
172
|
-
await new Promise((r) => setTimeout(r, 600));
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
if (x !== undefined && y !== undefined) {
|
|
@@ -965,6 +999,8 @@ export async function handleInspectElement(payload) {
|
|
|
965
999
|
|
|
966
1000
|
return { message: "Specify coordinates (x, y) or selector to inspect." };
|
|
967
1001
|
} finally {
|
|
968
|
-
|
|
1002
|
+
if (!isLiveSession && page) {
|
|
1003
|
+
await page.close().catch(() => {});
|
|
1004
|
+
}
|
|
969
1005
|
}
|
|
970
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
|
});
|