@doppelgangerdev/doppelganger 0.4.3 → 0.5.3
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/LICENSE +162 -162
- package/README.md +39 -37
- package/agent.js +342 -130
- 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
|
@@ -3,6 +3,7 @@ const { JSDOM } = require('jsdom');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
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
|
|
|
@@ -146,7 +141,7 @@ async function humanType(page, selector, text, options = {}) {
|
|
|
146
141
|
const typo = keys[Math.floor(Math.random() * keys.length)];
|
|
147
142
|
await page.keyboard.press(typo, { delay: 40 + Math.random() * 120 });
|
|
148
143
|
if (Math.random() < 0.5) {
|
|
149
|
-
await page.waitForTimeout(
|
|
144
|
+
await page.waitForTimeout(60 + Math.random() * 120);
|
|
150
145
|
}
|
|
151
146
|
await page.keyboard.press('Backspace', { delay: 40 + Math.random() * 120 });
|
|
152
147
|
if (Math.random() < 0.3) {
|
|
@@ -155,13 +150,13 @@ async function humanType(page, selector, text, options = {}) {
|
|
|
155
150
|
}
|
|
156
151
|
}
|
|
157
152
|
|
|
158
|
-
const extra = punctuationPause.test(char) ? randomBetween(
|
|
159
|
-
const fatiguePause = fatigue && Math.random() < 0.06 ? randomBetween(
|
|
153
|
+
const extra = punctuationPause.test(char) ? randomBetween(60, 150) : randomBetween(0, 40);
|
|
154
|
+
const fatiguePause = fatigue && Math.random() < 0.06 ? randomBetween(90, 200) : 0;
|
|
160
155
|
await typeChar(char, baseDelay + extra + fatiguePause);
|
|
161
156
|
burstCounter += 1;
|
|
162
157
|
|
|
163
158
|
if (naturalTyping && char === ' ') {
|
|
164
|
-
await page.waitForTimeout(randomBetween(
|
|
159
|
+
await page.waitForTimeout(randomBetween(20, 80));
|
|
165
160
|
}
|
|
166
161
|
}
|
|
167
162
|
}
|
|
@@ -170,6 +165,7 @@ async function handleAgent(req, res) {
|
|
|
170
165
|
const data = (req.method === 'POST') ? req.body : req.query;
|
|
171
166
|
let { url, actions, wait: globalWait, rotateUserAgents, rotateProxies, humanTyping, stealth = {} } = data;
|
|
172
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
|
|
@@ -198,9 +194,9 @@ async function handleAgent(req, res) {
|
|
|
198
194
|
});
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
const localPort = req.socket && req.socket.localPort;
|
|
202
|
-
const localHost = localPort ? `127.0.0.1:${localPort}` : req.get('host');
|
|
203
|
-
const baseUrl = `${req.protocol || 'http'}://${localHost}`;
|
|
197
|
+
const localPort = req.socket && req.socket.localPort;
|
|
198
|
+
const localHost = localPort ? `127.0.0.1:${localPort}` : req.get('host');
|
|
199
|
+
const baseUrl = `${req.protocol || 'http'}://${localHost}`;
|
|
204
200
|
const runtimeVars = { ...(data.taskVariables || data.variables || {}) };
|
|
205
201
|
let lastBlockOutput = null;
|
|
206
202
|
runtimeVars['block.output'] = lastBlockOutput;
|
|
@@ -232,6 +228,16 @@ async function handleAgent(req, res) {
|
|
|
232
228
|
return resolveTemplate(value);
|
|
233
229
|
};
|
|
234
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
|
+
};
|
|
240
|
+
|
|
235
241
|
const parseValue = (value) => {
|
|
236
242
|
if (typeof value !== 'string') return value;
|
|
237
243
|
const trimmed = value.trim();
|
|
@@ -391,12 +397,11 @@ 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
402
|
let browser;
|
|
403
|
+
let context;
|
|
404
|
+
let page;
|
|
400
405
|
try {
|
|
401
406
|
const launchOptions = {
|
|
402
407
|
headless: true,
|
|
@@ -404,6 +409,7 @@ async function handleAgent(req, res) {
|
|
|
404
409
|
args: [
|
|
405
410
|
'--no-sandbox',
|
|
406
411
|
'--disable-setuid-sandbox',
|
|
412
|
+
'--disable-dev-shm-usage',
|
|
407
413
|
'--disable-blink-features=AutomationControlled',
|
|
408
414
|
'--hide-scrollbars',
|
|
409
415
|
'--mute-audio'
|
|
@@ -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
|
|
|
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
|
+
|
|
420
436
|
const contextOptions = {
|
|
421
437
|
userAgent: selectedUA,
|
|
422
|
-
viewport
|
|
438
|
+
viewport,
|
|
423
439
|
deviceScaleFactor: 1,
|
|
424
440
|
locale: 'en-US',
|
|
425
441
|
timezoneId: 'America/New_York',
|
|
426
442
|
colorScheme: 'dark',
|
|
427
|
-
permissions: ['geolocation']
|
|
443
|
+
permissions: ['geolocation'],
|
|
444
|
+
recordVideo: { dir: recordingsDir, size: viewport }
|
|
428
445
|
};
|
|
429
446
|
|
|
430
447
|
if (fs.existsSync(STORAGE_STATE_FILE)) {
|
|
431
448
|
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
432
449
|
}
|
|
433
450
|
|
|
434
|
-
|
|
451
|
+
context = await browser.newContext(contextOptions);
|
|
435
452
|
|
|
436
453
|
await context.addInitScript(() => {
|
|
437
454
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
438
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,89 +726,150 @@ async function handleAgent(req, res) {
|
|
|
648
726
|
return merged;
|
|
649
727
|
};
|
|
650
728
|
|
|
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
|
+
|
|
651
747
|
const executeAction = async (act) => {
|
|
652
748
|
const { type, timeout } = act;
|
|
653
749
|
const actionTimeout = timeout || 10000;
|
|
654
750
|
let result = null;
|
|
655
751
|
|
|
656
752
|
switch (type) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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;
|
|
662
766
|
break;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
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
|
+
}
|
|
677
780
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
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));
|
|
691
793
|
}
|
|
794
|
+
}
|
|
692
795
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
796
|
+
await page.waitForTimeout(baseDelay(50));
|
|
797
|
+
await page.click(selectorValue, {
|
|
798
|
+
delay: baseDelay(50)
|
|
799
|
+
});
|
|
800
|
+
result = true;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
699
803
|
case 'type':
|
|
700
|
-
case 'fill':
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
804
|
+
case 'fill': {
|
|
805
|
+
const selectorValue = act.selector ? resolveMaybe(act.selector) : null;
|
|
806
|
+
const valueText = resolveMaybe(act.value) || '';
|
|
807
|
+
const typeMode = act.typeMode === 'append' ? 'append' : 'replace';
|
|
808
|
+
const humanOptions = { allowTypos, naturalTyping, fatigue };
|
|
809
|
+
|
|
810
|
+
const typeIntoSelector = async () => {
|
|
811
|
+
if (!selectorValue) return;
|
|
812
|
+
if (typeMode === 'replace') {
|
|
813
|
+
if (humanTyping) {
|
|
814
|
+
await page.fill(selectorValue, '');
|
|
815
|
+
await humanType(page, selectorValue, valueText, humanOptions);
|
|
816
|
+
} else {
|
|
817
|
+
await page.fill(selectorValue, valueText);
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
704
821
|
if (humanTyping) {
|
|
705
|
-
await humanType(page,
|
|
822
|
+
await humanType(page, selectorValue, valueText, humanOptions);
|
|
706
823
|
} else {
|
|
707
|
-
await page.
|
|
824
|
+
await page.type(selectorValue, valueText, { delay: baseDelay(50) });
|
|
708
825
|
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
if (selectorValue) {
|
|
829
|
+
const coords = parseCoords(String(selectorValue));
|
|
830
|
+
logs.push(`Typing into ${selectorValue}: ${valueText}`);
|
|
831
|
+
if (coords) {
|
|
832
|
+
await page.mouse.click(coords.x, coords.y, { delay: baseDelay(50) });
|
|
833
|
+
await typeIntoSelector();
|
|
834
|
+
result = valueText;
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
await page.waitForSelector(selectorValue, { timeout: actionTimeout });
|
|
838
|
+
await typeIntoSelector();
|
|
709
839
|
} else {
|
|
710
|
-
logs.push(`Typing (global): ${
|
|
840
|
+
logs.push(`Typing (global): ${valueText}`);
|
|
711
841
|
if (humanTyping) {
|
|
712
|
-
await humanType(page, null,
|
|
842
|
+
await humanType(page, null, valueText, humanOptions);
|
|
713
843
|
} else {
|
|
714
|
-
await page.keyboard.type(
|
|
844
|
+
await page.keyboard.type(valueText, { delay: baseDelay(50) });
|
|
715
845
|
}
|
|
716
846
|
}
|
|
717
|
-
result =
|
|
847
|
+
result = valueText;
|
|
718
848
|
break;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const centerX = box.x + box.width / 2 + (Math.random() - 0.5) * 5;
|
|
727
|
-
const centerY = box.y + box.height / 2 + (Math.random() - 0.5) * 5;
|
|
728
|
-
await moveMouseHumanlike(page, centerX, centerY);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
await page.waitForTimeout(baseDelay(150));
|
|
849
|
+
}
|
|
850
|
+
case 'hover': {
|
|
851
|
+
const selectorValue = resolveMaybe(act.selector);
|
|
852
|
+
const coords = parseCoords(String(selectorValue || ''));
|
|
853
|
+
logs.push(`Hovering: ${selectorValue}`);
|
|
854
|
+
if (coords) {
|
|
855
|
+
await moveMouseHumanlike(page, coords.x, coords.y);
|
|
732
856
|
result = true;
|
|
733
857
|
break;
|
|
858
|
+
}
|
|
859
|
+
await page.waitForSelector(selectorValue, { timeout: actionTimeout });
|
|
860
|
+
{
|
|
861
|
+
const handle = await page.$(selectorValue);
|
|
862
|
+
const box = handle && await handle.boundingBox();
|
|
863
|
+
if (box) {
|
|
864
|
+
const centerX = box.x + box.width / 2 + (Math.random() - 0.5) * 5;
|
|
865
|
+
const centerY = box.y + box.height / 2 + (Math.random() - 0.5) * 5;
|
|
866
|
+
await moveMouseHumanlike(page, centerX, centerY);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
await page.waitForTimeout(baseDelay(150));
|
|
870
|
+
result = true;
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
734
873
|
case 'press':
|
|
735
874
|
logs.push(`Pressing key: ${resolveMaybe(act.key)}`);
|
|
736
875
|
await page.keyboard.press(resolveMaybe(act.key), { delay: baseDelay(50) });
|
|
@@ -757,22 +896,62 @@ async function handleAgent(req, res) {
|
|
|
757
896
|
await page.selectOption(resolveMaybe(act.selector), resolveMaybe(act.value));
|
|
758
897
|
result = resolveMaybe(act.value);
|
|
759
898
|
break;
|
|
760
|
-
case 'scroll':
|
|
899
|
+
case 'scroll': {
|
|
761
900
|
const amount = act.value ? parseInt(resolveMaybe(act.value), 10) : (400 + Math.random() * 400);
|
|
762
|
-
|
|
901
|
+
const speedMs = act.key ? parseInt(resolveMaybe(act.key), 10) : 500;
|
|
902
|
+
const durationMs = Number.isFinite(speedMs) && speedMs > 0 ? speedMs : 500;
|
|
903
|
+
logs.push(`Scrolling page: ${amount}px over ${durationMs}ms...`);
|
|
763
904
|
if (overscroll) {
|
|
764
905
|
await overshootScroll(page, amount);
|
|
906
|
+
await page.waitForTimeout(baseDelay(200));
|
|
765
907
|
} else if (act.selector) {
|
|
766
|
-
await page.evaluate(({ selector, y }) => {
|
|
908
|
+
await page.evaluate(({ selector, y, duration }) => {
|
|
767
909
|
const el = document.querySelector(selector);
|
|
768
|
-
if (el)
|
|
769
|
-
|
|
910
|
+
if (!el) return;
|
|
911
|
+
const start = el.scrollTop;
|
|
912
|
+
const target = start + y;
|
|
913
|
+
const startTime = performance.now();
|
|
914
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
915
|
+
const step = (now) => {
|
|
916
|
+
const elapsed = now - startTime;
|
|
917
|
+
const t = Math.min(1, elapsed / duration);
|
|
918
|
+
const next = start + (target - start) * easeOut(t);
|
|
919
|
+
el.scrollTop = next;
|
|
920
|
+
if (t < 1) requestAnimationFrame(step);
|
|
921
|
+
};
|
|
922
|
+
requestAnimationFrame(step);
|
|
923
|
+
}, { selector: resolveMaybe(act.selector), y: amount, duration: durationMs });
|
|
924
|
+
await page.waitForTimeout(baseDelay(durationMs));
|
|
770
925
|
} else {
|
|
771
|
-
await page.evaluate((
|
|
926
|
+
await page.evaluate(({ y, duration }) => {
|
|
927
|
+
const start = window.scrollY || 0;
|
|
928
|
+
const target = start + y;
|
|
929
|
+
const startTime = performance.now();
|
|
930
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
931
|
+
const step = (now) => {
|
|
932
|
+
const elapsed = now - startTime;
|
|
933
|
+
const t = Math.min(1, elapsed / duration);
|
|
934
|
+
const next = start + (target - start) * easeOut(t);
|
|
935
|
+
window.scrollTo(0, next);
|
|
936
|
+
if (t < 1) requestAnimationFrame(step);
|
|
937
|
+
};
|
|
938
|
+
requestAnimationFrame(step);
|
|
939
|
+
}, { y: amount, duration: durationMs });
|
|
940
|
+
await page.waitForTimeout(baseDelay(durationMs));
|
|
772
941
|
}
|
|
773
|
-
await page.waitForTimeout(baseDelay(500));
|
|
774
942
|
result = amount;
|
|
775
943
|
break;
|
|
944
|
+
}
|
|
945
|
+
case 'screenshot':
|
|
946
|
+
logs.push('Capturing screenshot...');
|
|
947
|
+
try {
|
|
948
|
+
const shotUrl = await captureScreenshot(act.label || act.value || '');
|
|
949
|
+
result = shotUrl;
|
|
950
|
+
logs.push(`Screenshot saved: ${shotUrl}`);
|
|
951
|
+
} catch (e) {
|
|
952
|
+
logs.push(`Screenshot failed: ${e.message}`);
|
|
953
|
+
}
|
|
954
|
+
break;
|
|
776
955
|
case 'javascript':
|
|
777
956
|
logs.push('Running custom JavaScript...');
|
|
778
957
|
if (act.value) {
|
|
@@ -829,24 +1008,24 @@ async function handleAgent(req, res) {
|
|
|
829
1008
|
case 'start': {
|
|
830
1009
|
const taskId = resolveMaybe(act.value);
|
|
831
1010
|
if (!taskId) throw new Error('Missing task id.');
|
|
832
|
-
const apiKey = loadApiKey() || data.apiKey || data.key;
|
|
833
|
-
if (!apiKey) {
|
|
834
|
-
logs.push('No API key available; attempting internal start.');
|
|
835
|
-
}
|
|
836
|
-
logs.push(`Starting task: ${taskId}`);
|
|
837
|
-
const headers = {
|
|
838
|
-
'Content-Type': 'application/json',
|
|
839
|
-
'x-internal-run': '1'
|
|
840
|
-
};
|
|
841
|
-
if (apiKey) {
|
|
842
|
-
headers['x-api-key'] = apiKey;
|
|
843
|
-
}
|
|
844
|
-
const response = await fetch(`${baseUrl}/tasks/${taskId}/api`, {
|
|
845
|
-
method: 'POST',
|
|
846
|
-
headers,
|
|
847
|
-
body: JSON.stringify({
|
|
848
|
-
variables: runtimeVars,
|
|
849
|
-
taskVariables: runtimeVars,
|
|
1011
|
+
const apiKey = loadApiKey() || data.apiKey || data.key;
|
|
1012
|
+
if (!apiKey) {
|
|
1013
|
+
logs.push('No API key available; attempting internal start.');
|
|
1014
|
+
}
|
|
1015
|
+
logs.push(`Starting task: ${taskId}`);
|
|
1016
|
+
const headers = {
|
|
1017
|
+
'Content-Type': 'application/json',
|
|
1018
|
+
'x-internal-run': '1'
|
|
1019
|
+
};
|
|
1020
|
+
if (apiKey) {
|
|
1021
|
+
headers['x-api-key'] = apiKey;
|
|
1022
|
+
}
|
|
1023
|
+
const response = await fetch(`${baseUrl}/tasks/${taskId}/api`, {
|
|
1024
|
+
method: 'POST',
|
|
1025
|
+
headers,
|
|
1026
|
+
body: JSON.stringify({
|
|
1027
|
+
variables: runtimeVars,
|
|
1028
|
+
taskVariables: runtimeVars,
|
|
850
1029
|
runSource: 'agent_block',
|
|
851
1030
|
taskId
|
|
852
1031
|
})
|
|
@@ -856,10 +1035,10 @@ async function handleAgent(req, res) {
|
|
|
856
1035
|
const detail = payload?.error || payload?.message || response.statusText;
|
|
857
1036
|
throw new Error(`Start task failed: ${detail}`);
|
|
858
1037
|
}
|
|
859
|
-
result = payload?.data ?? payload?.html ?? payload;
|
|
860
|
-
setBlockOutput(result);
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
1038
|
+
result = payload?.data ?? payload?.html ?? payload;
|
|
1039
|
+
setBlockOutput(result);
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
863
1042
|
}
|
|
864
1043
|
return result;
|
|
865
1044
|
};
|
|
@@ -939,7 +1118,8 @@ async function handleAgent(req, res) {
|
|
|
939
1118
|
if (act.type === 'while') {
|
|
940
1119
|
try {
|
|
941
1120
|
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
942
|
-
const
|
|
1121
|
+
const hasStructured = act.conditionVarType || act.conditionOp || act.conditionVar || act.conditionValue;
|
|
1122
|
+
const condition = hasStructured ? evalStructuredCondition(act) : await evalCondition(act.value);
|
|
943
1123
|
setBlockOutput(condition);
|
|
944
1124
|
logs.push(`While condition: ${condition ? 'true' : 'false'}`);
|
|
945
1125
|
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
@@ -964,17 +1144,19 @@ async function handleAgent(req, res) {
|
|
|
964
1144
|
reportProgress(runId, { actionId: act.id, status: 'running' });
|
|
965
1145
|
const rawCount = parseInt(resolveMaybe(act.value) || '0', 10);
|
|
966
1146
|
const count = Number.isFinite(rawCount) ? rawCount : 0;
|
|
967
|
-
|
|
968
|
-
|
|
1147
|
+
let state = repeatState.get(index);
|
|
1148
|
+
if (!state) {
|
|
1149
|
+
state = { remaining: count };
|
|
1150
|
+
repeatState.set(index, state);
|
|
1151
|
+
}
|
|
969
1152
|
if (state.remaining <= 0) {
|
|
970
1153
|
repeatState.delete(index);
|
|
971
1154
|
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
972
1155
|
index = (startToEnd[index] ?? index) + 1;
|
|
973
1156
|
continue;
|
|
974
1157
|
}
|
|
975
|
-
state.remaining
|
|
976
|
-
|
|
977
|
-
setBlockOutput(state.remaining + 1);
|
|
1158
|
+
logs.push(`Repeat block: ${state.remaining} remaining`);
|
|
1159
|
+
setBlockOutput(state.remaining);
|
|
978
1160
|
reportProgress(runId, { actionId: act.id, status: 'success' });
|
|
979
1161
|
index += 1;
|
|
980
1162
|
continue;
|
|
@@ -1014,11 +1196,15 @@ async function handleAgent(req, res) {
|
|
|
1014
1196
|
}
|
|
1015
1197
|
if (startAction.type === 'repeat') {
|
|
1016
1198
|
const state = repeatState.get(startIndex);
|
|
1017
|
-
if (state
|
|
1018
|
-
|
|
1019
|
-
|
|
1199
|
+
if (state) {
|
|
1200
|
+
state.remaining -= 1;
|
|
1201
|
+
if (state.remaining > 0) {
|
|
1202
|
+
setBlockOutput(state.remaining);
|
|
1203
|
+
index = startIndex + 1;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
repeatState.delete(startIndex);
|
|
1020
1207
|
}
|
|
1021
|
-
repeatState.delete(startIndex);
|
|
1022
1208
|
}
|
|
1023
1209
|
if (startAction.type === 'foreach') {
|
|
1024
1210
|
const state = foreachState.get(startIndex);
|
|
@@ -1216,13 +1402,13 @@ async function handleAgent(req, res) {
|
|
|
1216
1402
|
};
|
|
1217
1403
|
|
|
1218
1404
|
// Ensure the public/screenshots directory exists
|
|
1219
|
-
const
|
|
1220
|
-
if (!fs.existsSync(
|
|
1221
|
-
fs.mkdirSync(
|
|
1405
|
+
const capturesDir = path.join(__dirname, 'public', 'captures');
|
|
1406
|
+
if (!fs.existsSync(capturesDir)) {
|
|
1407
|
+
fs.mkdirSync(capturesDir, { recursive: true });
|
|
1222
1408
|
}
|
|
1223
1409
|
|
|
1224
|
-
const screenshotName =
|
|
1225
|
-
const screenshotPath = path.join(
|
|
1410
|
+
const screenshotName = `${captureRunId}_agent_${Date.now()}.png`;
|
|
1411
|
+
const screenshotPath = path.join(capturesDir, screenshotName);
|
|
1226
1412
|
try {
|
|
1227
1413
|
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
1228
1414
|
} catch (e) {
|
|
@@ -1241,14 +1427,40 @@ async function handleAgent(req, res) {
|
|
|
1241
1427
|
logs: logs || [],
|
|
1242
1428
|
html: typeof cleanedHtml === 'string' ? safeFormatHTML(cleanedHtml) : '',
|
|
1243
1429
|
data: formattedExtraction,
|
|
1244
|
-
screenshot_url: fs.existsSync(screenshotPath) ? `/
|
|
1430
|
+
screenshot_url: fs.existsSync(screenshotPath) ? `/captures/${screenshotName}` : null
|
|
1245
1431
|
};
|
|
1246
1432
|
|
|
1433
|
+
const video = page.video();
|
|
1247
1434
|
try { await context.storageState({ path: STORAGE_STATE_FILE }); } catch {}
|
|
1435
|
+
try { await context.close(); } catch {}
|
|
1436
|
+
if (video) {
|
|
1437
|
+
try {
|
|
1438
|
+
const videoPath = await video.path();
|
|
1439
|
+
if (videoPath && fs.existsSync(videoPath)) {
|
|
1440
|
+
const recordingName = `${captureRunId}_agent_${Date.now()}.webm`;
|
|
1441
|
+
const recordingPath = path.join(capturesDir, recordingName);
|
|
1442
|
+
try {
|
|
1443
|
+
fs.renameSync(videoPath, recordingPath);
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
if (err && err.code === 'EXDEV') {
|
|
1446
|
+
fs.copyFileSync(videoPath, recordingPath);
|
|
1447
|
+
fs.unlinkSync(videoPath);
|
|
1448
|
+
} else {
|
|
1449
|
+
throw err;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
console.error('Recording save failed:', e.message);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1248
1457
|
try { await browser.close(); } catch {}
|
|
1249
1458
|
res.json(outputData);
|
|
1250
1459
|
} catch (error) {
|
|
1251
1460
|
console.error('Agent Error:', error);
|
|
1461
|
+
try {
|
|
1462
|
+
if (context) await context.close();
|
|
1463
|
+
} catch {}
|
|
1252
1464
|
if (browser) await browser.close();
|
|
1253
1465
|
res.status(500).json({ error: 'Agent failed', details: error.message });
|
|
1254
1466
|
}
|