@doppelgangerdev/doppelganger 0.4.3 → 0.5.2
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/agent.js +415 -218
- package/dist/assets/index-BXRKDZ1_.css +1 -0
- package/dist/assets/index-Deb2QMGx.js +19 -0
- package/dist/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
- package/dist/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
- package/dist/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
- package/dist/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
- package/dist/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
- package/dist/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
- package/dist/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
- package/dist/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
- package/dist/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
- package/dist/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
- package/dist/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
- package/dist/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
- package/dist/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
- package/dist/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/agent_1769037343598.png +0 -0
- package/dist/screenshots/agent_1769037357541.png +0 -0
- package/dist/screenshots/scrape_1769037382254.png +0 -0
- package/dist/screenshots/scrape_1769037413189.png +0 -0
- package/dist/screenshots/scrape_1769037449707.png +0 -0
- package/dist/screenshots/scrape_1769037461756.png +0 -0
- package/dist/screenshots/scrape_1769037490581.png +0 -0
- package/dist/screenshots/scrape_1769038242368.png +0 -0
- package/headful.js +76 -21
- package/package.json +3 -1
- package/proxy-rotation.js +133 -90
- package/public/captures/run_1769042882403_589_agent_1769042884446_initial.png +0 -0
- package/public/captures/run_1769042882403_589_agent_1769042887058.png +0 -0
- package/public/captures/run_1769042882403_589_agent_1769042888468.webm +0 -0
- package/public/captures/run_1769043202318_943_agent_1769043206237.png +0 -0
- package/public/captures/run_1769043202318_943_agent_1769043207415.webm +0 -0
- package/public/captures/run_1769043449517_97_agent_1769043451350_initial.png +0 -0
- package/public/captures/run_1769043449517_97_agent_1769043455038.png +0 -0
- package/public/captures/run_1769043449517_97_agent_1769043456476.webm +0 -0
- package/public/captures/run_1769043471164_239_agent_1769043472720_initial.png +0 -0
- package/public/captures/run_1769043471164_239_agent_1769043474022.png +0 -0
- package/public/captures/run_1769043471164_239_agent_1769043476419.png +0 -0
- package/public/captures/run_1769043471164_239_agent_1769043477795.webm +0 -0
- package/public/captures/run_1769080585290_151_agent_1769080595110.png +0 -0
- package/public/captures/run_1769080585290_151_agent_1769080596335.webm +0 -0
- package/public/screenshots/agent_1769037343598.png +0 -0
- package/public/screenshots/agent_1769037357541.png +0 -0
- package/public/screenshots/scrape_1769037382254.png +0 -0
- package/public/screenshots/scrape_1769037413189.png +0 -0
- package/public/screenshots/scrape_1769037449707.png +0 -0
- package/public/screenshots/scrape_1769037461756.png +0 -0
- package/public/screenshots/scrape_1769037490581.png +0 -0
- package/public/screenshots/scrape_1769038242368.png +0 -0
- package/scrape.js +163 -66
- package/server.js +127 -72
- package/dist/assets/index-D68YZVOp.js +0 -19
- package/dist/assets/index-WbwoTnJa.css +0 -1
package/agent.js
CHANGED
|
@@ -2,7 +2,8 @@ const { chromium } = require('playwright');
|
|
|
2
2
|
const { JSDOM } = require('jsdom');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { getProxySelection } = require('./proxy-rotation');
|
|
5
|
+
const { getProxySelection } = require('./proxy-rotation');
|
|
6
|
+
const { selectUserAgent } = require('./user-agent-settings');
|
|
6
7
|
|
|
7
8
|
const STORAGE_STATE_PATH = path.join(__dirname, 'storage_state.json');
|
|
8
9
|
const STORAGE_STATE_FILE = (() => {
|
|
@@ -17,12 +18,6 @@ const STORAGE_STATE_FILE = (() => {
|
|
|
17
18
|
return STORAGE_STATE_PATH;
|
|
18
19
|
})();
|
|
19
20
|
|
|
20
|
-
const userAgents = [
|
|
21
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
22
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
|
23
|
-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
|
|
24
|
-
];
|
|
25
|
-
|
|
26
21
|
const API_KEY_FILE = path.join(__dirname, 'data', 'api_key.json');
|
|
27
22
|
|
|
28
23
|
const loadApiKey = () => {
|
|
@@ -124,8 +119,8 @@ async function humanType(page, selector, text, options = {}) {
|
|
|
124
119
|
if (selector) await page.focus(selector);
|
|
125
120
|
const chars = text.split('');
|
|
126
121
|
let burstCounter = 0;
|
|
127
|
-
const burstLimit = naturalTyping ? Math.floor(randomBetween(
|
|
128
|
-
const baseDelay = naturalTyping ? randomBetween(
|
|
122
|
+
const burstLimit = naturalTyping ? Math.floor(randomBetween(6, 16)) : 999;
|
|
123
|
+
const baseDelay = naturalTyping ? randomBetween(12, 55) : randomBetween(25, 80);
|
|
129
124
|
const typeChar = async (char, delay) => {
|
|
130
125
|
try {
|
|
131
126
|
await page.keyboard.press(char, { delay });
|
|
@@ -137,7 +132,7 @@ async function humanType(page, selector, text, options = {}) {
|
|
|
137
132
|
|
|
138
133
|
for (const char of chars) {
|
|
139
134
|
if (naturalTyping && burstCounter >= burstLimit) {
|
|
140
|
-
await page.waitForTimeout(randomBetween(
|
|
135
|
+
await page.waitForTimeout(randomBetween(60, 180));
|
|
141
136
|
burstCounter = 0;
|
|
142
137
|
}
|
|
143
138
|
|
|
@@ -145,31 +140,32 @@ async function humanType(page, selector, text, options = {}) {
|
|
|
145
140
|
const keys = 'qwertyuiopasdfghjklzxcvbnm';
|
|
146
141
|
const typo = keys[Math.floor(Math.random() * keys.length)];
|
|
147
142
|
await page.keyboard.press(typo, { delay: 40 + Math.random() * 120 });
|
|
148
|
-
if (Math.random() < 0.5) {
|
|
149
|
-
await page.waitForTimeout(
|
|
150
|
-
}
|
|
151
|
-
await page.keyboard.press('Backspace', { delay: 40 + Math.random() * 120 });
|
|
152
|
-
if (Math.random() < 0.3) {
|
|
153
|
-
await page.keyboard.press(typo, { delay: 40 + Math.random() * 120 });
|
|
154
|
-
await page.keyboard.press('Backspace', { delay: 40 + Math.random() * 120 });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const extra = punctuationPause.test(char) ? randomBetween(
|
|
159
|
-
const fatiguePause = fatigue && Math.random() < 0.06 ? randomBetween(
|
|
160
|
-
await typeChar(char, baseDelay + extra + fatiguePause);
|
|
161
|
-
burstCounter += 1;
|
|
162
|
-
|
|
163
|
-
if (naturalTyping && char === ' ') {
|
|
164
|
-
await page.waitForTimeout(randomBetween(
|
|
165
|
-
}
|
|
143
|
+
if (Math.random() < 0.5) {
|
|
144
|
+
await page.waitForTimeout(60 + Math.random() * 120);
|
|
145
|
+
}
|
|
146
|
+
await page.keyboard.press('Backspace', { delay: 40 + Math.random() * 120 });
|
|
147
|
+
if (Math.random() < 0.3) {
|
|
148
|
+
await page.keyboard.press(typo, { delay: 40 + Math.random() * 120 });
|
|
149
|
+
await page.keyboard.press('Backspace', { delay: 40 + Math.random() * 120 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const extra = punctuationPause.test(char) ? randomBetween(60, 150) : randomBetween(0, 40);
|
|
154
|
+
const fatiguePause = fatigue && Math.random() < 0.06 ? randomBetween(90, 200) : 0;
|
|
155
|
+
await typeChar(char, baseDelay + extra + fatiguePause);
|
|
156
|
+
burstCounter += 1;
|
|
157
|
+
|
|
158
|
+
if (naturalTyping && char === ' ') {
|
|
159
|
+
await page.waitForTimeout(randomBetween(20, 80));
|
|
160
|
+
}
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
async function handleAgent(req, res) {
|
|
170
|
-
const data = (req.method === 'POST') ? req.body : req.query;
|
|
171
|
-
let { url, actions, wait: globalWait, rotateUserAgents, rotateProxies, humanTyping, stealth = {} } = data;
|
|
172
|
-
const runId = data.runId ? String(data.runId) : null;
|
|
164
|
+
async function handleAgent(req, res) {
|
|
165
|
+
const data = (req.method === 'POST') ? req.body : req.query;
|
|
166
|
+
let { url, actions, wait: globalWait, rotateUserAgents, rotateProxies, humanTyping, stealth = {} } = data;
|
|
167
|
+
const runId = data.runId ? String(data.runId) : null;
|
|
168
|
+
const captureRunId = runId || `run_${Date.now()}_unknown`;
|
|
173
169
|
const includeShadowDomRaw = data.includeShadowDom ?? req.query.includeShadowDom;
|
|
174
170
|
const includeShadowDom = includeShadowDomRaw === undefined
|
|
175
171
|
? true
|
|
@@ -227,10 +223,20 @@ async function handleAgent(req, res) {
|
|
|
227
223
|
});
|
|
228
224
|
};
|
|
229
225
|
|
|
230
|
-
const resolveMaybe = (value) => {
|
|
231
|
-
if (typeof value !== 'string') return value;
|
|
232
|
-
return resolveTemplate(value);
|
|
233
|
-
};
|
|
226
|
+
const resolveMaybe = (value) => {
|
|
227
|
+
if (typeof value !== 'string') return value;
|
|
228
|
+
return resolveTemplate(value);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const parseCoords = (input) => {
|
|
232
|
+
if (!input || typeof input !== 'string') return null;
|
|
233
|
+
const match = input.trim().match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/);
|
|
234
|
+
if (!match) return null;
|
|
235
|
+
const x = Number(match[1]);
|
|
236
|
+
const y = Number(match[2]);
|
|
237
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
238
|
+
return { x, y };
|
|
239
|
+
};
|
|
234
240
|
|
|
235
241
|
const parseValue = (value) => {
|
|
236
242
|
if (typeof value !== 'string') return value;
|
|
@@ -391,23 +397,23 @@ async function handleAgent(req, res) {
|
|
|
391
397
|
return { startToEnd, startToElse, elseToEnd, endToStart };
|
|
392
398
|
};
|
|
393
399
|
|
|
394
|
-
|
|
395
|
-
const selectedUA = rotateUserAgents
|
|
396
|
-
? userAgents[Math.floor(Math.random() * userAgents.length)]
|
|
397
|
-
: userAgents[0];
|
|
400
|
+
const selectedUA = selectUserAgent(rotateUserAgents);
|
|
398
401
|
|
|
399
|
-
let browser;
|
|
400
|
-
|
|
402
|
+
let browser;
|
|
403
|
+
let context;
|
|
404
|
+
let page;
|
|
405
|
+
try {
|
|
401
406
|
const launchOptions = {
|
|
402
407
|
headless: true,
|
|
403
408
|
channel: 'chrome',
|
|
404
|
-
args: [
|
|
405
|
-
'--no-sandbox',
|
|
406
|
-
'--disable-setuid-sandbox',
|
|
407
|
-
'--disable-
|
|
408
|
-
'--
|
|
409
|
-
'--
|
|
410
|
-
|
|
409
|
+
args: [
|
|
410
|
+
'--no-sandbox',
|
|
411
|
+
'--disable-setuid-sandbox',
|
|
412
|
+
'--disable-dev-shm-usage',
|
|
413
|
+
'--disable-blink-features=AutomationControlled',
|
|
414
|
+
'--hide-scrollbars',
|
|
415
|
+
'--mute-audio'
|
|
416
|
+
]
|
|
411
417
|
};
|
|
412
418
|
const useRotateProxies = String(rotateProxies).toLowerCase() === 'true' || rotateProxies === true;
|
|
413
419
|
const selection = getProxySelection(useRotateProxies);
|
|
@@ -417,25 +423,97 @@ async function handleAgent(req, res) {
|
|
|
417
423
|
console.log(`[PROXY] Mode: ${selection.mode}; Target: ${selection.proxy ? selection.proxy.server : 'host_ip'}`);
|
|
418
424
|
browser = await chromium.launch(launchOptions);
|
|
419
425
|
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
const recordingsDir = path.join(__dirname, 'data', 'recordings');
|
|
427
|
+
if (!fs.existsSync(recordingsDir)) {
|
|
428
|
+
fs.mkdirSync(recordingsDir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const rotateViewport = String(data.rotateViewport).toLowerCase() === 'true' || data.rotateViewport === true;
|
|
432
|
+
const viewport = rotateViewport
|
|
433
|
+
? { width: 1280 + Math.floor(Math.random() * 640), height: 720 + Math.floor(Math.random() * 360) }
|
|
434
|
+
: { width: 1366, height: 768 };
|
|
435
|
+
|
|
436
|
+
const contextOptions = {
|
|
437
|
+
userAgent: selectedUA,
|
|
438
|
+
viewport,
|
|
439
|
+
deviceScaleFactor: 1,
|
|
440
|
+
locale: 'en-US',
|
|
441
|
+
timezoneId: 'America/New_York',
|
|
442
|
+
colorScheme: 'dark',
|
|
443
|
+
permissions: ['geolocation'],
|
|
444
|
+
recordVideo: { dir: recordingsDir, size: viewport }
|
|
445
|
+
};
|
|
429
446
|
|
|
430
447
|
if (fs.existsSync(STORAGE_STATE_FILE)) {
|
|
431
448
|
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
432
449
|
}
|
|
433
450
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
await context.addInitScript(() => {
|
|
437
|
-
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
438
|
-
});
|
|
451
|
+
context = await browser.newContext(contextOptions);
|
|
452
|
+
|
|
453
|
+
await context.addInitScript(() => {
|
|
454
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
455
|
+
});
|
|
456
|
+
await context.addInitScript(() => {
|
|
457
|
+
const cursorId = 'dg-cursor-overlay';
|
|
458
|
+
const dotId = 'dg-click-dot';
|
|
459
|
+
if (document.getElementById(cursorId)) return;
|
|
460
|
+
const cursor = document.createElement('div');
|
|
461
|
+
cursor.id = cursorId;
|
|
462
|
+
cursor.style.cssText = [
|
|
463
|
+
'position:fixed',
|
|
464
|
+
'top:0',
|
|
465
|
+
'left:0',
|
|
466
|
+
'width:18px',
|
|
467
|
+
'height:18px',
|
|
468
|
+
'margin-left:-9px',
|
|
469
|
+
'margin-top:-9px',
|
|
470
|
+
'border:2px solid rgba(56,189,248,0.7)',
|
|
471
|
+
'background:rgba(56,189,248,0.25)',
|
|
472
|
+
'border-radius:50%',
|
|
473
|
+
'box-shadow:0 0 10px rgba(56,189,248,0.6)',
|
|
474
|
+
'pointer-events:none',
|
|
475
|
+
'z-index:2147483647',
|
|
476
|
+
'transform:translate3d(0,0,0)',
|
|
477
|
+
'transition:transform 60ms ease-out'
|
|
478
|
+
].join(';');
|
|
479
|
+
const dot = document.createElement('div');
|
|
480
|
+
dot.id = dotId;
|
|
481
|
+
dot.style.cssText = [
|
|
482
|
+
'position:fixed',
|
|
483
|
+
'top:0',
|
|
484
|
+
'left:0',
|
|
485
|
+
'width:10px',
|
|
486
|
+
'height:10px',
|
|
487
|
+
'margin-left:-5px',
|
|
488
|
+
'margin-top:-5px',
|
|
489
|
+
'background:rgba(239,68,68,0.9)',
|
|
490
|
+
'border-radius:50%',
|
|
491
|
+
'box-shadow:0 0 12px rgba(239,68,68,0.8)',
|
|
492
|
+
'pointer-events:none',
|
|
493
|
+
'z-index:2147483647',
|
|
494
|
+
'opacity:0',
|
|
495
|
+
'transform:translate3d(0,0,0) scale(0.6)',
|
|
496
|
+
'transition:opacity 120ms ease, transform 120ms ease'
|
|
497
|
+
].join(';');
|
|
498
|
+
document.documentElement.appendChild(cursor);
|
|
499
|
+
document.documentElement.appendChild(dot);
|
|
500
|
+
const move = (x, y) => {
|
|
501
|
+
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
|
502
|
+
};
|
|
503
|
+
window.addEventListener('mousemove', (e) => move(e.clientX, e.clientY), { passive: true });
|
|
504
|
+
window.addEventListener('click', (e) => {
|
|
505
|
+
dot.style.left = `${e.clientX}px`;
|
|
506
|
+
dot.style.top = `${e.clientY}px`;
|
|
507
|
+
dot.style.opacity = '1';
|
|
508
|
+
dot.style.transform = 'translate3d(0,0,0) scale(1)';
|
|
509
|
+
cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) scale(0.65)`;
|
|
510
|
+
setTimeout(() => {
|
|
511
|
+
dot.style.opacity = '0';
|
|
512
|
+
dot.style.transform = 'translate3d(0,0,0) scale(0.6)';
|
|
513
|
+
cursor.style.transform = `translate3d(${e.clientX}px, ${e.clientY}px, 0) scale(1)`;
|
|
514
|
+
}, 180);
|
|
515
|
+
}, true);
|
|
516
|
+
});
|
|
439
517
|
if (includeShadowDom) {
|
|
440
518
|
await context.addInitScript(() => {
|
|
441
519
|
if (!Element.prototype.attachShadow) return;
|
|
@@ -447,7 +525,7 @@ async function handleAgent(req, res) {
|
|
|
447
525
|
});
|
|
448
526
|
}
|
|
449
527
|
|
|
450
|
-
|
|
528
|
+
page = await context.newPage();
|
|
451
529
|
|
|
452
530
|
if (url) {
|
|
453
531
|
await page.goto(resolveTemplate(url), { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
@@ -648,67 +726,105 @@ async function handleAgent(req, res) {
|
|
|
648
726
|
return merged;
|
|
649
727
|
};
|
|
650
728
|
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
729
|
+
const ensureCapturesDir = () => {
|
|
730
|
+
const capturesDir = path.join(__dirname, 'public', 'captures');
|
|
731
|
+
if (!fs.existsSync(capturesDir)) {
|
|
732
|
+
fs.mkdirSync(capturesDir, { recursive: true });
|
|
733
|
+
}
|
|
734
|
+
return capturesDir;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const captureScreenshot = async (label) => {
|
|
738
|
+
const capturesDir = ensureCapturesDir();
|
|
739
|
+
const safeLabel = label ? String(label).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24) : '';
|
|
740
|
+
const nameSuffix = safeLabel ? `_${safeLabel}` : '';
|
|
741
|
+
const screenshotName = `${captureRunId}_agent_${Date.now()}${nameSuffix}.png`;
|
|
742
|
+
const screenshotPath = path.join(capturesDir, screenshotName);
|
|
743
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
744
|
+
return `/captures/${screenshotName}`;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const executeAction = async (act) => {
|
|
748
|
+
const { type, timeout } = act;
|
|
749
|
+
const actionTimeout = timeout || 10000;
|
|
750
|
+
let result = null;
|
|
751
|
+
|
|
752
|
+
switch (type) {
|
|
753
|
+
case 'navigate':
|
|
754
|
+
case 'goto':
|
|
755
|
+
logs.push(`Navigating to: ${resolveMaybe(act.value)}`);
|
|
756
|
+
await page.goto(resolveMaybe(act.value), { waitUntil: 'domcontentloaded' });
|
|
757
|
+
result = page.url();
|
|
758
|
+
break;
|
|
759
|
+
case 'click': {
|
|
760
|
+
const selectorValue = resolveMaybe(act.selector);
|
|
761
|
+
const coords = parseCoords(String(selectorValue || ''));
|
|
762
|
+
logs.push(`Clicking: ${selectorValue}`);
|
|
763
|
+
if (coords) {
|
|
764
|
+
await page.mouse.click(coords.x, coords.y, { delay: baseDelay(50) });
|
|
765
|
+
result = true;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
await page.waitForSelector(selectorValue, { timeout: actionTimeout });
|
|
769
|
+
|
|
770
|
+
// Neutral Dead Click
|
|
771
|
+
if (deadClicks && Math.random() < 0.4) {
|
|
772
|
+
logs.push('Performing neutral dead-click...');
|
|
773
|
+
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
|
774
|
+
await page.mouse.click(
|
|
775
|
+
10 + Math.random() * (viewport.width * 0.2),
|
|
776
|
+
10 + Math.random() * (viewport.height * 0.2)
|
|
777
|
+
);
|
|
778
|
+
await page.waitForTimeout(baseDelay(200));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Get element point for human-like movement
|
|
782
|
+
const handle = await page.$(selectorValue);
|
|
783
|
+
const box = await handle.boundingBox();
|
|
784
|
+
if (box) {
|
|
785
|
+
const centerX = box.x + box.width / 2 + (Math.random() - 0.5) * 5;
|
|
786
|
+
const centerY = box.y + box.height / 2 + (Math.random() - 0.5) * 5;
|
|
787
|
+
await moveMouseHumanlike(page, centerX, centerY);
|
|
788
|
+
if (deadClicks && Math.random() < 0.25) {
|
|
789
|
+
const offsetX = (Math.random() - 0.5) * Math.min(20, box.width / 3);
|
|
790
|
+
const offsetY = (Math.random() - 0.5) * Math.min(20, box.height / 3);
|
|
791
|
+
await page.mouse.click(centerX + offsetX, centerY + offsetY, { delay: baseDelay(30) });
|
|
792
|
+
await page.waitForTimeout(baseDelay(120));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
await page.waitForTimeout(baseDelay(50));
|
|
797
|
+
await page.click(selectorValue, {
|
|
798
|
+
delay: baseDelay(50)
|
|
799
|
+
});
|
|
800
|
+
result = true;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case 'type':
|
|
804
|
+
case 'fill':
|
|
805
|
+
if (act.selector) {
|
|
806
|
+
const selectorValue = resolveMaybe(act.selector);
|
|
807
|
+
const coords = parseCoords(String(selectorValue || ''));
|
|
808
|
+
logs.push(`Typing into ${selectorValue}: ${resolveMaybe(act.value)}`);
|
|
809
|
+
if (coords) {
|
|
810
|
+
await page.mouse.click(coords.x, coords.y, { delay: baseDelay(50) });
|
|
811
|
+
if (humanTyping) {
|
|
812
|
+
await humanType(page, null, resolveMaybe(act.value), { allowTypos, naturalTyping, fatigue });
|
|
813
|
+
} else {
|
|
814
|
+
await page.keyboard.type(resolveMaybe(act.value), { delay: baseDelay(50) });
|
|
815
|
+
}
|
|
816
|
+
result = resolveMaybe(act.value);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
await page.waitForSelector(selectorValue, { timeout: actionTimeout });
|
|
820
|
+
if (humanTyping) {
|
|
821
|
+
await humanType(page, selectorValue, resolveMaybe(act.value), { allowTypos, naturalTyping, fatigue });
|
|
822
|
+
} else {
|
|
823
|
+
await page.fill(selectorValue, resolveMaybe(act.value));
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
logs.push(`Typing (global): ${resolveMaybe(act.value)}`);
|
|
827
|
+
if (humanTyping) {
|
|
712
828
|
await humanType(page, null, resolveMaybe(act.value), { allowTypos, naturalTyping, fatigue });
|
|
713
829
|
} else {
|
|
714
830
|
await page.keyboard.type(resolveMaybe(act.value), { delay: baseDelay(50) });
|
|
@@ -716,21 +832,29 @@ async function handleAgent(req, res) {
|
|
|
716
832
|
}
|
|
717
833
|
result = resolveMaybe(act.value);
|
|
718
834
|
break;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
await
|
|
732
|
-
|
|
733
|
-
|
|
835
|
+
case 'hover': {
|
|
836
|
+
const selectorValue = resolveMaybe(act.selector);
|
|
837
|
+
const coords = parseCoords(String(selectorValue || ''));
|
|
838
|
+
logs.push(`Hovering: ${selectorValue}`);
|
|
839
|
+
if (coords) {
|
|
840
|
+
await moveMouseHumanlike(page, coords.x, coords.y);
|
|
841
|
+
result = true;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
await page.waitForSelector(selectorValue, { timeout: actionTimeout });
|
|
845
|
+
{
|
|
846
|
+
const handle = await page.$(selectorValue);
|
|
847
|
+
const box = handle && await handle.boundingBox();
|
|
848
|
+
if (box) {
|
|
849
|
+
const centerX = box.x + box.width / 2 + (Math.random() - 0.5) * 5;
|
|
850
|
+
const centerY = box.y + box.height / 2 + (Math.random() - 0.5) * 5;
|
|
851
|
+
await moveMouseHumanlike(page, centerX, centerY);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
await page.waitForTimeout(baseDelay(150));
|
|
855
|
+
result = true;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
734
858
|
case 'press':
|
|
735
859
|
logs.push(`Pressing key: ${resolveMaybe(act.key)}`);
|
|
736
860
|
await page.keyboard.press(resolveMaybe(act.key), { delay: baseDelay(50) });
|
|
@@ -757,25 +881,65 @@ async function handleAgent(req, res) {
|
|
|
757
881
|
await page.selectOption(resolveMaybe(act.selector), resolveMaybe(act.value));
|
|
758
882
|
result = resolveMaybe(act.value);
|
|
759
883
|
break;
|
|
760
|
-
case 'scroll':
|
|
761
|
-
const amount = act.value ? parseInt(resolveMaybe(act.value), 10) : (400 + Math.random() * 400);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
await page
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
884
|
+
case 'scroll': {
|
|
885
|
+
const amount = act.value ? parseInt(resolveMaybe(act.value), 10) : (400 + Math.random() * 400);
|
|
886
|
+
const speedMs = act.key ? parseInt(resolveMaybe(act.key), 10) : 500;
|
|
887
|
+
const durationMs = Number.isFinite(speedMs) && speedMs > 0 ? speedMs : 500;
|
|
888
|
+
logs.push(`Scrolling page: ${amount}px over ${durationMs}ms...`);
|
|
889
|
+
if (overscroll) {
|
|
890
|
+
await overshootScroll(page, amount);
|
|
891
|
+
await page.waitForTimeout(baseDelay(200));
|
|
892
|
+
} else if (act.selector) {
|
|
893
|
+
await page.evaluate(({ selector, y, duration }) => {
|
|
894
|
+
const el = document.querySelector(selector);
|
|
895
|
+
if (!el) return;
|
|
896
|
+
const start = el.scrollTop;
|
|
897
|
+
const target = start + y;
|
|
898
|
+
const startTime = performance.now();
|
|
899
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
900
|
+
const step = (now) => {
|
|
901
|
+
const elapsed = now - startTime;
|
|
902
|
+
const t = Math.min(1, elapsed / duration);
|
|
903
|
+
const next = start + (target - start) * easeOut(t);
|
|
904
|
+
el.scrollTop = next;
|
|
905
|
+
if (t < 1) requestAnimationFrame(step);
|
|
906
|
+
};
|
|
907
|
+
requestAnimationFrame(step);
|
|
908
|
+
}, { selector: resolveMaybe(act.selector), y: amount, duration: durationMs });
|
|
909
|
+
await page.waitForTimeout(baseDelay(durationMs));
|
|
910
|
+
} else {
|
|
911
|
+
await page.evaluate(({ y, duration }) => {
|
|
912
|
+
const start = window.scrollY || 0;
|
|
913
|
+
const target = start + y;
|
|
914
|
+
const startTime = performance.now();
|
|
915
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
916
|
+
const step = (now) => {
|
|
917
|
+
const elapsed = now - startTime;
|
|
918
|
+
const t = Math.min(1, elapsed / duration);
|
|
919
|
+
const next = start + (target - start) * easeOut(t);
|
|
920
|
+
window.scrollTo(0, next);
|
|
921
|
+
if (t < 1) requestAnimationFrame(step);
|
|
922
|
+
};
|
|
923
|
+
requestAnimationFrame(step);
|
|
924
|
+
}, { y: amount, duration: durationMs });
|
|
925
|
+
await page.waitForTimeout(baseDelay(durationMs));
|
|
926
|
+
}
|
|
927
|
+
result = amount;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case 'screenshot':
|
|
931
|
+
logs.push('Capturing screenshot...');
|
|
932
|
+
try {
|
|
933
|
+
const shotUrl = await captureScreenshot(act.label || act.value || '');
|
|
934
|
+
result = shotUrl;
|
|
935
|
+
logs.push(`Screenshot saved: ${shotUrl}`);
|
|
936
|
+
} catch (e) {
|
|
937
|
+
logs.push(`Screenshot failed: ${e.message}`);
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
case 'javascript':
|
|
941
|
+
logs.push('Running custom JavaScript...');
|
|
942
|
+
if (act.value) {
|
|
779
943
|
result = await page.evaluate((code) => {
|
|
780
944
|
// eslint-disable-next-line no-eval
|
|
781
945
|
return eval(code);
|
|
@@ -936,14 +1100,15 @@ async function handleAgent(req, res) {
|
|
|
936
1100
|
continue;
|
|
937
1101
|
}
|
|
938
1102
|
|
|
939
|
-
if (act.type === 'while') {
|
|
940
|
-
try {
|
|
941
|
-
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1103
|
+
if (act.type === 'while') {
|
|
1104
|
+
try {
|
|
1105
|
+
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
1106
|
+
const hasStructured = act.conditionVarType || act.conditionOp || act.conditionVar || act.conditionValue;
|
|
1107
|
+
const condition = hasStructured ? evalStructuredCondition(act) : await evalCondition(act.value);
|
|
1108
|
+
setBlockOutput(condition);
|
|
1109
|
+
logs.push(`While condition: ${condition ? 'true' : 'false'}`);
|
|
1110
|
+
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
1111
|
+
if (!condition) {
|
|
947
1112
|
index = (startToEnd[index] ?? index) + 1;
|
|
948
1113
|
continue;
|
|
949
1114
|
}
|
|
@@ -960,25 +1125,27 @@ async function handleAgent(req, res) {
|
|
|
960
1125
|
continue;
|
|
961
1126
|
}
|
|
962
1127
|
|
|
963
|
-
if (act.type === 'repeat') {
|
|
964
|
-
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
965
|
-
const rawCount = parseInt(resolveMaybe(act.value) || '0', 10);
|
|
966
|
-
const count = Number.isFinite(rawCount) ? rawCount : 0;
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
repeatState.
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1128
|
+
if (act.type === 'repeat') {
|
|
1129
|
+
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
1130
|
+
const rawCount = parseInt(resolveMaybe(act.value) || '0', 10);
|
|
1131
|
+
const count = Number.isFinite(rawCount) ? rawCount : 0;
|
|
1132
|
+
let state = repeatState.get(index);
|
|
1133
|
+
if (!state) {
|
|
1134
|
+
state = { remaining: count };
|
|
1135
|
+
repeatState.set(index, state);
|
|
1136
|
+
}
|
|
1137
|
+
if (state.remaining <= 0) {
|
|
1138
|
+
repeatState.delete(index);
|
|
1139
|
+
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
1140
|
+
index = (startToEnd[index] ?? index) + 1;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
logs.push(`Repeat block: ${state.remaining} remaining`);
|
|
1144
|
+
setBlockOutput(state.remaining);
|
|
1145
|
+
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
1146
|
+
index += 1;
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
982
1149
|
|
|
983
1150
|
if (act.type === 'foreach') {
|
|
984
1151
|
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
@@ -1012,14 +1179,18 @@ async function handleAgent(req, res) {
|
|
|
1012
1179
|
index = startIndex;
|
|
1013
1180
|
continue;
|
|
1014
1181
|
}
|
|
1015
|
-
if (startAction.type === 'repeat') {
|
|
1016
|
-
const state = repeatState.get(startIndex);
|
|
1017
|
-
if (state
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1182
|
+
if (startAction.type === 'repeat') {
|
|
1183
|
+
const state = repeatState.get(startIndex);
|
|
1184
|
+
if (state) {
|
|
1185
|
+
state.remaining -= 1;
|
|
1186
|
+
if (state.remaining > 0) {
|
|
1187
|
+
setBlockOutput(state.remaining);
|
|
1188
|
+
index = startIndex + 1;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
repeatState.delete(startIndex);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1023
1194
|
if (startAction.type === 'foreach') {
|
|
1024
1195
|
const state = foreachState.get(startIndex);
|
|
1025
1196
|
if (state) {
|
|
@@ -1216,18 +1387,18 @@ async function handleAgent(req, res) {
|
|
|
1216
1387
|
};
|
|
1217
1388
|
|
|
1218
1389
|
// Ensure the public/screenshots directory exists
|
|
1219
|
-
const
|
|
1220
|
-
if (!fs.existsSync(
|
|
1221
|
-
fs.mkdirSync(
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const screenshotName =
|
|
1225
|
-
const screenshotPath = path.join(
|
|
1226
|
-
try {
|
|
1227
|
-
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
console.error('Agent Screenshot failed:', e.message);
|
|
1230
|
-
}
|
|
1390
|
+
const capturesDir = path.join(__dirname, 'public', 'captures');
|
|
1391
|
+
if (!fs.existsSync(capturesDir)) {
|
|
1392
|
+
fs.mkdirSync(capturesDir, { recursive: true });
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const screenshotName = `${captureRunId}_agent_${Date.now()}.png`;
|
|
1396
|
+
const screenshotPath = path.join(capturesDir, screenshotName);
|
|
1397
|
+
try {
|
|
1398
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
console.error('Agent Screenshot failed:', e.message);
|
|
1401
|
+
}
|
|
1231
1402
|
|
|
1232
1403
|
const extractionFormat = String(data.extractionFormat || (data.taskSnapshot && data.taskSnapshot.extractionFormat) || '').toLowerCase() === 'csv'
|
|
1233
1404
|
? 'csv'
|
|
@@ -1241,17 +1412,43 @@ async function handleAgent(req, res) {
|
|
|
1241
1412
|
logs: logs || [],
|
|
1242
1413
|
html: typeof cleanedHtml === 'string' ? safeFormatHTML(cleanedHtml) : '',
|
|
1243
1414
|
data: formattedExtraction,
|
|
1244
|
-
screenshot_url: fs.existsSync(screenshotPath) ? `/
|
|
1245
|
-
};
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
try { await
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1415
|
+
screenshot_url: fs.existsSync(screenshotPath) ? `/captures/${screenshotName}` : null
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
const video = page.video();
|
|
1419
|
+
try { await context.storageState({ path: STORAGE_STATE_FILE }); } catch {}
|
|
1420
|
+
try { await context.close(); } catch {}
|
|
1421
|
+
if (video) {
|
|
1422
|
+
try {
|
|
1423
|
+
const videoPath = await video.path();
|
|
1424
|
+
if (videoPath && fs.existsSync(videoPath)) {
|
|
1425
|
+
const recordingName = `${captureRunId}_agent_${Date.now()}.webm`;
|
|
1426
|
+
const recordingPath = path.join(capturesDir, recordingName);
|
|
1427
|
+
try {
|
|
1428
|
+
fs.renameSync(videoPath, recordingPath);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (err && err.code === 'EXDEV') {
|
|
1431
|
+
fs.copyFileSync(videoPath, recordingPath);
|
|
1432
|
+
fs.unlinkSync(videoPath);
|
|
1433
|
+
} else {
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
console.error('Recording save failed:', e.message);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
try { await browser.close(); } catch {}
|
|
1443
|
+
res.json(outputData);
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
console.error('Agent Error:', error);
|
|
1446
|
+
try {
|
|
1447
|
+
if (context) await context.close();
|
|
1448
|
+
} catch {}
|
|
1449
|
+
if (browser) await browser.close();
|
|
1450
|
+
res.status(500).json({ error: 'Agent failed', details: error.message });
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1256
1453
|
|
|
1257
1454
|
module.exports = { handleAgent, setProgressReporter, setStopChecker };
|