app-manager-edge-worker 1.0.6 → 1.0.8

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/src/config.js CHANGED
@@ -69,6 +69,12 @@ export function parseCliArgs() {
69
69
  parsed.keepAlive = parseInt(args[++i], 10);
70
70
  } else if (arg === "--no-keep-alive") {
71
71
  parsed.keepAlive = false;
72
+ } else if (arg === "--ai-endpoint") {
73
+ parsed.aiEndpoint = args[++i];
74
+ } else if (arg === "--ai-model") {
75
+ parsed.aiModel = args[++i];
76
+ } else if (arg === "--ai-key") {
77
+ parsed.aiApiKey = args[++i];
72
78
  } else if (arg === "--reset") {
73
79
  parsed.reset = true;
74
80
  } else if (arg === "--help" || arg === "-h") {
@@ -105,8 +111,14 @@ export async function resolveWorkerConfig() {
105
111
  const cli = parseCliArgs();
106
112
 
107
113
  if (cli.help) {
114
+ let pkgVer = "1.0.8";
115
+ try {
116
+ const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
117
+ if (pkg?.version) pkgVer = pkg.version;
118
+ } catch (_) {}
119
+
108
120
  console.log(`
109
- Apps Manager Scraper Edge Worker CLI v1.0.4
121
+ Apps Manager Scraper Edge Worker CLI v${pkgVer}
110
122
 
111
123
  Usage:
112
124
  npx app-manager-edge-worker [options]
@@ -118,6 +130,9 @@ Options:
118
130
  -n, --name <name> Custom display name for this worker device
119
131
  -p, --keep-alive <minutes> Interval in minutes to ping server and prevent Render free-tier sleep (default: 7)
120
132
  --no-keep-alive Disable keep-alive ping
133
+ --ai-endpoint <url> Local/remote AI endpoint (default: http://127.0.0.1:11434 for Ollama)
134
+ --ai-model <model> Model name for Tier 4 AI Smart Repair (e.g. llama3, mistral)
135
+ --ai-key <apiKey> Optional API key for AI endpoint
121
136
  --reset Clear stored credentials and prompt again
122
137
  -h, --help Display this help message
123
138
  `);
@@ -208,11 +223,33 @@ Options:
208
223
  keepAliveMinutes = parseInt(process.env.KEEP_ALIVE_MINUTES, 10) || 7;
209
224
  }
210
225
 
226
+ const aiEndpoint =
227
+ cli.aiEndpoint ||
228
+ process.env.AI_ENDPOINT ||
229
+ process.env.OLLAMA_ENDPOINT ||
230
+ stored.aiEndpoint ||
231
+ "http://localhost:11434/v1";
232
+
233
+ const aiModel =
234
+ cli.aiModel ||
235
+ process.env.AI_MODEL ||
236
+ stored.aiModel ||
237
+ "qwen2.5-coder:7b";
238
+
239
+ const aiApiKey =
240
+ cli.aiApiKey ||
241
+ process.env.AI_API_KEY ||
242
+ stored.aiApiKey ||
243
+ "ollama";
244
+
211
245
  return {
212
246
  workerKey: workerKey.trim(),
213
247
  serverUrl,
214
248
  concurrency,
215
249
  workerName,
216
250
  keepAliveMinutes,
251
+ aiEndpoint,
252
+ aiModel,
253
+ aiApiKey,
217
254
  };
218
255
  }
@@ -0,0 +1,154 @@
1
+ import axios from "axios";
2
+ import * as cheerio from "cheerio";
3
+
4
+ /**
5
+ * Handles AI_SMART_REPAIR jobs on the Edge Worker.
6
+ * Communicates with local Ollama, LocalAI, LM Studio, or OpenAI-compatible endpoints.
7
+ * This offloads all AI computation from the cloud server (Render free tier) to the local worker.
8
+ *
9
+ * @param {Object} payload - { field, expectedType, previousSelector, previousValue, regexPattern, htmlSnippet, aiConfig }
10
+ * @param {Object} workerConfig - Worker global config with default AI endpoint/model
11
+ * @returns {Promise<Object>} { success, field, repairedSelector, extractedValue, confidence, explanation, modelUsed }
12
+ */
13
+ export async function handleAiSmartRepair(payload, workerConfig = {}) {
14
+ const {
15
+ field = "field",
16
+ expectedType = "string",
17
+ previousSelector = "",
18
+ previousValue = "",
19
+ regexPattern = "",
20
+ htmlSnippet = "",
21
+ aiConfig = {},
22
+ } = payload;
23
+
24
+ const endpoint =
25
+ aiConfig.endpoint ||
26
+ workerConfig.aiEndpoint ||
27
+ process.env.AI_ENDPOINT ||
28
+ "http://localhost:11434/v1";
29
+
30
+ const model =
31
+ aiConfig.model ||
32
+ workerConfig.aiModel ||
33
+ process.env.AI_MODEL ||
34
+ "qwen2.5-coder:7b";
35
+
36
+ const apiKey =
37
+ aiConfig.apiKey ||
38
+ workerConfig.aiApiKey ||
39
+ process.env.AI_API_KEY ||
40
+ "ollama";
41
+
42
+ if (!htmlSnippet) {
43
+ throw new Error("Cannot perform AI smart repair: HTML snippet is empty.");
44
+ }
45
+
46
+ const systemPrompt = `You are an automated web scraping repair assistant.
47
+ A website redesign has broken an existing selector. Your job is to analyze the updated HTML snippet and identify the relocated element for the target field.
48
+ You MUST output ONLY a valid JSON object matching the requested schema with no surrounding Markdown or explanation.`;
49
+
50
+ const userPrompt = `FIELD REPAIR TARGET:
51
+ - Field Name: "${field}"
52
+ - Expected Data Type: "${expectedType}"
53
+ - Broken Previous Selector: "${previousSelector}"
54
+ - Last Known Sample Value: "${previousValue}"
55
+ ${regexPattern ? `- Expected Value Format (Regex): "${regexPattern}"` : ""}
56
+
57
+ UPDATED HTML SNIPPET:
58
+ \`\`\`html
59
+ ${htmlSnippet.slice(0, 8000)}
60
+ \`\`\`
61
+
62
+ TASK:
63
+ 1. Locate the exact HTML element in the snippet that represents "${field}".
64
+ 2. Construct a robust, resilient CSS selector (avoid dynamic or hashed classes like '_1a2b3c', prefer semantic tags, ids, data-* attributes, or structural paths).
65
+ 3. Return ONLY a JSON object formatted as follows:
66
+ {
67
+ "repairedSelector": "css_selector_here",
68
+ "extractedValue": "exact_text_or_attribute_value",
69
+ "confidence": 0.95,
70
+ "explanation": "Brief explanation of why this element was chosen"
71
+ }`;
72
+
73
+ const cleanEndpoint = endpoint.replace(/\/+$/, "");
74
+ const chatCompletionsUrl = cleanEndpoint.endsWith("/chat/completions")
75
+ ? cleanEndpoint
76
+ : `${cleanEndpoint}/chat/completions`;
77
+
78
+ try {
79
+ const response = await axios.post(
80
+ chatCompletionsUrl,
81
+ {
82
+ model,
83
+ messages: [
84
+ { role: "system", content: systemPrompt },
85
+ { role: "user", content: userPrompt },
86
+ ],
87
+ temperature: 0.1,
88
+ response_format: { type: "json_object" },
89
+ },
90
+ {
91
+ headers: {
92
+ Authorization: `Bearer ${apiKey}`,
93
+ "Content-Type": "application/json",
94
+ },
95
+ timeout: 45000,
96
+ }
97
+ );
98
+
99
+ let content = response.data?.choices?.[0]?.message?.content || "{}";
100
+
101
+ // Strip any markdown code fences if model returned ```json ... ```
102
+ content = content.replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/```$/i, "").trim();
103
+
104
+ let parsedResult = {};
105
+ try {
106
+ parsedResult = JSON.parse(content);
107
+ } catch {
108
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
109
+ if (jsonMatch) {
110
+ parsedResult = JSON.parse(jsonMatch[0]);
111
+ } else {
112
+ throw new Error("Failed to parse JSON response from local AI model.");
113
+ }
114
+ }
115
+
116
+ const candidateSelector = parsedResult.repairedSelector?.trim() || "";
117
+ if (!candidateSelector) {
118
+ return {
119
+ success: false,
120
+ field,
121
+ error: "AI model could not determine a candidate selector.",
122
+ confidence: 0,
123
+ };
124
+ }
125
+
126
+ // Verify the candidate selector locally in the HTML snippet using Cheerio
127
+ const $ = cheerio.load(htmlSnippet);
128
+ let matchEl = null;
129
+ try {
130
+ matchEl = $(candidateSelector);
131
+ } catch {
132
+ // In case model returned an invalid CSS syntax
133
+ matchEl = null;
134
+ }
135
+
136
+ const isVerified = matchEl && matchEl.length > 0;
137
+ const verifiedValue = isVerified ? matchEl.first().text().trim() : parsedResult.extractedValue || "";
138
+
139
+ return {
140
+ success: isVerified,
141
+ field,
142
+ repairedSelector: candidateSelector,
143
+ extractedValue: verifiedValue,
144
+ confidence: isVerified ? Math.min(1.0, Math.max(0.6, parsedResult.confidence || 0.85)) : 0.3,
145
+ explanation: parsedResult.explanation || "Selector inferred via Edge Worker AI model.",
146
+ modelUsed: model,
147
+ endpointUsed: endpoint,
148
+ };
149
+ } catch (apiErr) {
150
+ throw new Error(
151
+ `Edge Worker AI repair request failed on '${endpoint}' (${model}): ${apiErr.message}`
152
+ );
153
+ }
154
+ }
@@ -0,0 +1,74 @@
1
+ import nodemailer from "nodemailer";
2
+
3
+ /**
4
+ * Handles incoming SEND_EMAIL job from the cluster server.
5
+ *
6
+ * Security & Reliability Guarantees:
7
+ * 1. Zero credential retention: Credentials are received in-memory and destroyed after sending.
8
+ * 2. Fail-fast timeouts: Prevents socket hanging with strict 10s connection & socket timeouts.
9
+ * 3. Structured result payload: Returns delivery metadata (messageId, accepted, response).
10
+ *
11
+ * @param {Object} payload
12
+ * @param {Object} payload.mailOptions - { to, subject, html, text, fromName, fromEmail }
13
+ * @param {Object} payload.smtpConfig - { host, port, secure, auth: { user, pass } }
14
+ * @returns {Promise<Object>}
15
+ */
16
+ export async function handleSendEmail(payload = {}) {
17
+ const { mailOptions, smtpConfig } = payload;
18
+
19
+ if (!mailOptions || !mailOptions.to || !mailOptions.subject) {
20
+ throw new Error("Invalid mailOptions: 'to' and 'subject' are required.");
21
+ }
22
+ if (!smtpConfig || !smtpConfig.auth?.user || !smtpConfig.auth?.pass) {
23
+ throw new Error("Invalid smtpConfig: credentials missing.");
24
+ }
25
+
26
+ const host = smtpConfig.host || "smtp.titan.email";
27
+ const port = Number(smtpConfig.port) || 465;
28
+ const secure = smtpConfig.secure !== undefined ? Boolean(smtpConfig.secure) : port === 465;
29
+
30
+ // Ephemeral Nodemailer transporter created strictly for this in-memory dispatch
31
+ const transporter = nodemailer.createTransport({
32
+ host,
33
+ port,
34
+ secure,
35
+ auth: {
36
+ user: smtpConfig.auth.user,
37
+ pass: smtpConfig.auth.pass,
38
+ },
39
+ connectionTimeout: 10000,
40
+ greetingTimeout: 10000,
41
+ socketTimeout: 15000,
42
+ tls: {
43
+ rejectUnauthorized: false, // Prevents certificate mismatches on custom domain mail
44
+ },
45
+ });
46
+
47
+ try {
48
+ const fromName = mailOptions.fromName || "Apps Manager";
49
+ const fromAddress = mailOptions.fromEmail || smtpConfig.auth.user;
50
+
51
+ const info = await transporter.sendMail({
52
+ from: `"${fromName}" <${fromAddress}>`,
53
+ to: mailOptions.to,
54
+ subject: mailOptions.subject,
55
+ text: mailOptions.text,
56
+ html: mailOptions.html,
57
+ });
58
+
59
+ return {
60
+ success: true,
61
+ messageId: info.messageId,
62
+ accepted: info.accepted,
63
+ rejected: info.rejected,
64
+ response: info.response,
65
+ };
66
+ } finally {
67
+ // Explicitly close transporter socket to prevent lingering connections
68
+ try {
69
+ transporter.close();
70
+ } catch (_) {
71
+ // Best effort cleanup
72
+ }
73
+ }
74
+ }
@@ -4,13 +4,29 @@ import localBrowserManager from "../browser.js";
4
4
  * Handles PREVIEW_PAGE task: captures full-page screenshot and dimensions on the edge worker.
5
5
  */
6
6
  export async function handlePreviewPage(payload) {
7
- const { url, timeout = 25000 } = payload;
7
+ const { url, timeout = 25000, preScrapeActions, authentication } = payload;
8
8
  const browser = await localBrowserManager.getBrowser();
9
9
  const page = await browser.newPage();
10
10
 
11
11
  try {
12
12
  await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 });
13
13
 
14
+ // Apply authentication cookies & headers if provided
15
+ if (authentication) {
16
+ if (authentication.type === "COOKIES" && Array.isArray(authentication.cookies)) {
17
+ const parsedUrl = new URL(url);
18
+ await page.setCookie(...authentication.cookies.map(c => ({
19
+ name: c.name,
20
+ value: c.value,
21
+ domain: c.domain || parsedUrl.hostname,
22
+ path: c.path || "/",
23
+ }))).catch(() => {});
24
+ }
25
+ if (authentication.type === "HEADERS" && authentication.headers) {
26
+ await page.setExtraHTTPHeaders(authentication.headers).catch(() => {});
27
+ }
28
+ }
29
+
14
30
  await page.goto(url, { waitUntil: "domcontentloaded", timeout });
15
31
  try {
16
32
  await page.waitForNetworkIdle({ idleTime: 500, timeout: 3500 }).catch(() => {});
@@ -18,6 +34,37 @@ export async function handlePreviewPage(payload) {
18
34
  await page.evaluate(() => document.fonts?.ready).catch(() => {});
19
35
  await new Promise((r) => setTimeout(r, 800));
20
36
 
37
+ // Execute pre-scrape actions if present
38
+ if (Array.isArray(preScrapeActions) && preScrapeActions.length > 0) {
39
+ for (const action of preScrapeActions) {
40
+ try {
41
+ if (action.actionType === "CLICK" && action.selector) {
42
+ await page.waitForSelector(action.selector, { timeout: 4000 });
43
+ await page.click(action.selector);
44
+ } else if (action.actionType === "TYPE" && action.selector && action.value) {
45
+ await page.waitForSelector(action.selector, { timeout: 4000 });
46
+ await page.type(action.selector, action.value, { delay: 30 });
47
+ } else if (action.actionType === "SCROLL") {
48
+ const scrollPixels = parseInt(action.value, 10) || 500;
49
+ await page.evaluate((px) => window.scrollBy(0, px), scrollPixels);
50
+ } else if (action.actionType === "WAIT_FOR" && action.selector) {
51
+ await page.waitForSelector(action.selector, { timeout: 8000 });
52
+ } else if (action.actionType === "SELECT_OPTION" && action.selector && action.value) {
53
+ await page.waitForSelector(action.selector, { timeout: 4000 });
54
+ await page.select(action.selector, action.value);
55
+ } else if (action.actionType === "PRESS_KEY" && action.value) {
56
+ await page.keyboard.press(action.value);
57
+ }
58
+ if (action.delayMs && action.delayMs > 0) {
59
+ await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 5000)));
60
+ }
61
+ } catch (actErr) {
62
+ console.warn(`[WorkerPreview] Pre-scrape action ${action.actionType} (${action.selector}) skipped: ${actErr.message}`);
63
+ }
64
+ }
65
+ await new Promise((r) => setTimeout(r, 600));
66
+ }
67
+
21
68
  const title = (await page.title()) || new URL(url).hostname;
22
69
  const currentUrl = page.url();
23
70
 
@@ -458,16 +505,42 @@ export async function handleInspectElement(payload) {
458
505
 
459
506
  const count = document.querySelectorAll(sel).length;
460
507
 
508
+ const rawText = el.innerText ? el.innerText.trim() : el.textContent?.trim() || "";
509
+ const attrs = Array.from(el.attributes).reduce((acc, a) => {
510
+ acc[a.name] = a.value;
511
+ return acc;
512
+ }, {});
513
+
514
+ const hierarchy = [];
515
+ let currH = el;
516
+ while (currH && currH !== document.body && hierarchy.length < 6) {
517
+ let desc = currH.tagName.toLowerCase();
518
+ if (currH.id) desc += `#${currH.id}`;
519
+ else if (currH.className && typeof currH.className === "string") {
520
+ const cls = currH.className.split(/\s+/).filter(c => /^[a-zA-Z_-][\w-]*$/.test(c)).slice(0, 2);
521
+ if (cls.length) desc += `.${cls.join(".")}`;
522
+ }
523
+ hierarchy.unshift(desc);
524
+ currH = currH.parentElement;
525
+ }
526
+
527
+ const fingerprint = {
528
+ tagName: el.tagName.toLowerCase(),
529
+ domHierarchy: hierarchy,
530
+ attributes: attrs,
531
+ textAnchors: [rawText.slice(0, 80)].filter(Boolean),
532
+ dataPattern: "",
533
+ sampleValues: [rawText.slice(0, 100)].filter(Boolean),
534
+ };
535
+
461
536
  return {
462
537
  found: true,
463
538
  count,
464
539
  tagName: el.tagName.toLowerCase(),
465
- text: el.innerText ? el.innerText.trim() : el.textContent?.trim() || "",
466
- attributes: Array.from(el.attributes).reduce((acc, a) => {
467
- acc[a.name] = a.value;
468
- return acc;
469
- }, {}),
540
+ text: rawText,
541
+ attributes: attrs,
470
542
  boundingRect,
543
+ fingerprint,
471
544
  };
472
545
  } catch (e) {
473
546
  return { found: false, error: e.message };
@@ -349,15 +349,57 @@ export async function handleChromiumScrape(payload) {
349
349
  deviceScaleFactor: 1,
350
350
  });
351
351
 
352
- if (Object.keys(headers).length > 0) {
353
- await page.setExtraHTTPHeaders(headers);
352
+ // Merge auth headers & cookies
353
+ const effectiveHeaders = {
354
+ ...(headers || {}),
355
+ ...(payload.authentication?.headers || {}),
356
+ };
357
+ const effectiveCookies = [
358
+ ...(Array.isArray(cookies) ? cookies : []),
359
+ ...(Array.isArray(payload.authentication?.cookies) ? payload.authentication.cookies : []),
360
+ ];
361
+
362
+ if (Object.keys(effectiveHeaders).length > 0) {
363
+ await page.setExtraHTTPHeaders(effectiveHeaders);
354
364
  }
355
365
 
356
- if (Array.isArray(cookies) && cookies.length > 0) {
357
- await page.setCookie(...cookies);
366
+ if (effectiveCookies.length > 0) {
367
+ await page.setCookie(...effectiveCookies);
368
+ }
369
+
370
+ // Automated Login Flow if configured
371
+ const auth = payload.authentication || {};
372
+ if (auth.type === "LOGIN_FLOW" && auth.loginFlow?.loginUrl) {
373
+ try {
374
+ const lf = auth.loginFlow;
375
+ await page.goto(lf.loginUrl, { waitUntil: "domcontentloaded", timeout: 20000 });
376
+ if (lf.usernameSelector && lf.usernameValue) {
377
+ await page.waitForSelector(lf.usernameSelector, { timeout: 6000 });
378
+ await page.click(lf.usernameSelector);
379
+ await page.type(lf.usernameSelector, lf.usernameValue, { delay: 30 });
380
+ }
381
+ if (lf.passwordSelector && lf.passwordValue) {
382
+ await page.waitForSelector(lf.passwordSelector, { timeout: 6000 });
383
+ await page.click(lf.passwordSelector);
384
+ await page.type(lf.passwordSelector, lf.passwordValue, { delay: 30 });
385
+ }
386
+ if (lf.submitSelector) {
387
+ await page.waitForSelector(lf.submitSelector, { timeout: 6000 });
388
+ await Promise.all([
389
+ page.click(lf.submitSelector),
390
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 15000 }).catch(() => {}),
391
+ ]);
392
+ }
393
+ if (lf.successIndicatorSelector) {
394
+ await page.waitForSelector(lf.successIndicatorSelector, { timeout: 8000 }).catch(() => {});
395
+ }
396
+ await new Promise((r) => setTimeout(r, 1000));
397
+ } catch (loginErr) {
398
+ console.warn(`[ScrapeHandler] Automated login flow error: ${loginErr.message}. Continuing with scrape...`);
399
+ }
358
400
  }
359
401
 
360
- // Navigate
402
+ // Navigate to target URL
361
403
  const response = await page.goto(url, {
362
404
  waitUntil: navWaitUntil,
363
405
  timeout: navTimeout,
@@ -392,21 +434,53 @@ export async function handleChromiumScrape(payload) {
392
434
  await new Promise((r) => setTimeout(r, Math.min(waitForTimeout, 10000)));
393
435
  }
394
436
 
395
- // Execute sequential interactive actions if configured
396
- if (Array.isArray(actions) && actions.length > 0) {
397
- for (const action of actions) {
437
+ // Execute sequential interactive actions (preScrapeActions or actions)
438
+ const combinedActions = [
439
+ ...(Array.isArray(payload.preScrapeActions) ? payload.preScrapeActions : []),
440
+ ...(Array.isArray(actions) ? actions : []),
441
+ ];
442
+
443
+ if (combinedActions.length > 0) {
444
+ for (let aIdx = 0; aIdx < combinedActions.length; aIdx++) {
445
+ const action = combinedActions[aIdx];
446
+ const actType = (action.type || "").toUpperCase();
447
+ const opt = action.optional !== false;
398
448
  try {
399
- if (action.type === "click" && action.selector) {
449
+ if ((actType === "CLICK" || action.type === "click") && action.selector) {
450
+ await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 4000 });
400
451
  await page.click(action.selector);
401
- } else if (action.type === "type" && action.selector && action.text) {
402
- await page.type(action.selector, action.text);
403
- } else if (action.type === "scroll") {
404
- await page.evaluate((y) => window.scrollBy(0, y || 500), action.distance);
405
- } else if (action.type === "wait") {
406
- await new Promise((r) => setTimeout(r, action.duration || 1000));
452
+ } else if ((actType === "TYPE" || action.type === "type") && action.selector) {
453
+ const txt = action.value !== undefined ? action.value : action.text || "";
454
+ await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 4000 });
455
+ await page.click(action.selector);
456
+ await page.type(action.selector, txt, { delay: 30 });
457
+ } else if (actType === "SELECT_OPTION" && action.selector && action.value) {
458
+ await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 4000 });
459
+ await page.select(action.selector, action.value);
460
+ } else if (actType === "PRESS_KEY") {
461
+ await page.keyboard.press(action.key || "Enter");
462
+ } else if (actType === "SCROLL" || action.type === "scroll") {
463
+ const dist = action.scrollDistance || action.distance || 500;
464
+ await page.evaluate((y) => window.scrollBy(0, y), dist);
465
+ } else if (actType === "WAIT_FOR" && action.selector) {
466
+ await page.waitForSelector(action.selector, {
467
+ visible: true,
468
+ timeout: action.timeoutMs || 6000,
469
+ });
470
+ } else if (actType === "DELAY" || action.type === "wait") {
471
+ const dur = action.delayMs || action.duration || 1000;
472
+ await new Promise((r) => setTimeout(r, dur));
473
+ }
474
+
475
+ if (action.delayMs > 0) {
476
+ await new Promise((r) => setTimeout(r, action.delayMs));
407
477
  }
408
478
  } catch (actionErr) {
409
- console.warn(`[ScrapeHandler] Action ${action.type} failed:`, actionErr.message);
479
+ if (opt) {
480
+ console.warn(`[ScrapeHandler] Optional action #${aIdx + 1} (${action.type} '${action.selector || ""}') skipped: ${actionErr.message}`);
481
+ } else {
482
+ throw actionErr;
483
+ }
410
484
  }
411
485
  }
412
486
  }
@@ -685,15 +759,27 @@ export async function handleHttpScrape(payload) {
685
759
  includeHtml = false,
686
760
  } = payload;
687
761
 
762
+ const auth = payload.authentication || {};
763
+ const effectiveHeaders = {
764
+ "User-Agent":
765
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
766
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
767
+ "Accept-Language": "en-US,en;q=0.9",
768
+ ...(headers || {}),
769
+ ...(auth.headers || {}),
770
+ };
771
+
772
+ const effectiveCookies = [
773
+ ...(Array.isArray(payload.cookies) ? payload.cookies : []),
774
+ ...(Array.isArray(auth.cookies) ? auth.cookies : []),
775
+ ];
776
+ if (effectiveCookies.length > 0 && !effectiveHeaders.Cookie) {
777
+ effectiveHeaders.Cookie = effectiveCookies.map((c) => `${c.name}=${c.value}`).join("; ");
778
+ }
779
+
688
780
  const response = await axios.get(url, {
689
781
  timeout,
690
- headers: {
691
- "User-Agent":
692
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
693
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
694
- "Accept-Language": "en-US,en;q=0.9",
695
- ...headers,
696
- },
782
+ headers: effectiveHeaders,
697
783
  });
698
784
 
699
785
  let pageHtml = typeof response.data === "string" ? response.data : Buffer.isBuffer(response.data) ? response.data.toString("utf-8") : "";