@radaros/browser 0.3.20 → 0.3.22
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/dist/index.cjs +1007 -0
- package/dist/index.d.cts +325 -0
- package/package.json +9 -6
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BrowserAgent: () => BrowserAgent,
|
|
34
|
+
BrowserProvider: () => BrowserProvider,
|
|
35
|
+
CredentialVault: () => CredentialVault
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/browser-agent.ts
|
|
40
|
+
var import_core = require("@radaros/core");
|
|
41
|
+
var import_zod = require("zod");
|
|
42
|
+
|
|
43
|
+
// src/stealth.ts
|
|
44
|
+
var REALISTIC_USER_AGENTS = [
|
|
45
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
46
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
47
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
|
48
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
|
49
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
50
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15"
|
|
51
|
+
];
|
|
52
|
+
function pickUserAgent() {
|
|
53
|
+
return REALISTIC_USER_AGENTS[Math.floor(Math.random() * REALISTIC_USER_AGENTS.length)];
|
|
54
|
+
}
|
|
55
|
+
function getStealthScript() {
|
|
56
|
+
return `
|
|
57
|
+
// \u2500\u2500 navigator.webdriver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
58
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
59
|
+
get: () => undefined,
|
|
60
|
+
configurable: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// \u2500\u2500 navigator.plugins \u2014 appear non-empty \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
64
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
65
|
+
get: () => {
|
|
66
|
+
const plugins = [
|
|
67
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
68
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
69
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
70
|
+
];
|
|
71
|
+
plugins.length = 3;
|
|
72
|
+
return plugins;
|
|
73
|
+
},
|
|
74
|
+
configurable: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// \u2500\u2500 navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
78
|
+
Object.defineProperty(navigator, 'languages', {
|
|
79
|
+
get: () => ['en-US', 'en'],
|
|
80
|
+
configurable: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// \u2500\u2500 navigator.permissions.query \u2014 hide "denied" for notifications \u2500
|
|
84
|
+
const originalQuery = window.navigator.permissions.query.bind(window.navigator.permissions);
|
|
85
|
+
window.navigator.permissions.query = (params) => {
|
|
86
|
+
if (params.name === 'notifications') {
|
|
87
|
+
return Promise.resolve({ state: 'prompt', onchange: null });
|
|
88
|
+
}
|
|
89
|
+
return originalQuery(params);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// \u2500\u2500 chrome runtime \u2014 make it look like a real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
93
|
+
if (!window.chrome) {
|
|
94
|
+
window.chrome = {};
|
|
95
|
+
}
|
|
96
|
+
if (!window.chrome.runtime) {
|
|
97
|
+
window.chrome.runtime = {
|
|
98
|
+
connect: () => {},
|
|
99
|
+
sendMessage: () => {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// \u2500\u2500 WebGL renderer \u2014 mask headless indicators \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
104
|
+
const getParameterOrig = WebGLRenderingContext.prototype.getParameter;
|
|
105
|
+
WebGLRenderingContext.prototype.getParameter = function(param) {
|
|
106
|
+
if (param === 37445) return 'Intel Inc.';
|
|
107
|
+
if (param === 37446) return 'Intel Iris OpenGL Engine';
|
|
108
|
+
return getParameterOrig.call(this, param);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// \u2500\u2500 WebGL2 renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
112
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
113
|
+
const getParam2Orig = WebGL2RenderingContext.prototype.getParameter;
|
|
114
|
+
WebGL2RenderingContext.prototype.getParameter = function(param) {
|
|
115
|
+
if (param === 37445) return 'Intel Inc.';
|
|
116
|
+
if (param === 37446) return 'Intel Iris OpenGL Engine';
|
|
117
|
+
return getParam2Orig.call(this, param);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// \u2500\u2500 Prevent iframe detection of automation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
122
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
123
|
+
get: function() {
|
|
124
|
+
return window;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// \u2500\u2500 Remove "cdc_" Playwright/ChromeDriver markers from DOM \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
129
|
+
const observer = new MutationObserver((mutations) => {
|
|
130
|
+
for (const mutation of mutations) {
|
|
131
|
+
for (const node of mutation.addedNodes) {
|
|
132
|
+
if (node.nodeType === 1) {
|
|
133
|
+
const el = node;
|
|
134
|
+
for (const attr of [...el.attributes]) {
|
|
135
|
+
if (attr.name.startsWith('cdc_') || attr.name.startsWith('__playwright')) {
|
|
136
|
+
el.removeAttribute(attr.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
observer.observe(document.documentElement, { attributes: true, childList: true, subtree: true });
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
function buildStealthContextOpts(config, viewport) {
|
|
147
|
+
const opts = {
|
|
148
|
+
viewport,
|
|
149
|
+
userAgent: config.userAgent ?? pickUserAgent(),
|
|
150
|
+
locale: config.locale ?? "en-US",
|
|
151
|
+
timezoneId: config.timezone ?? "America/New_York",
|
|
152
|
+
colorScheme: "light",
|
|
153
|
+
deviceScaleFactor: 2,
|
|
154
|
+
hasTouch: false,
|
|
155
|
+
javaScriptEnabled: true,
|
|
156
|
+
ignoreHTTPSErrors: true
|
|
157
|
+
};
|
|
158
|
+
if (config.geolocation) {
|
|
159
|
+
opts.geolocation = config.geolocation;
|
|
160
|
+
opts.permissions = ["geolocation"];
|
|
161
|
+
}
|
|
162
|
+
return opts;
|
|
163
|
+
}
|
|
164
|
+
function buildStealthLaunchArgs(config) {
|
|
165
|
+
const args = [
|
|
166
|
+
"--disable-blink-features=AutomationControlled",
|
|
167
|
+
"--disable-infobars",
|
|
168
|
+
"--disable-dev-shm-usage",
|
|
169
|
+
"--no-first-run",
|
|
170
|
+
"--no-default-browser-check"
|
|
171
|
+
];
|
|
172
|
+
const proxy = config.proxy ? { server: config.proxy.server, username: config.proxy.username, password: config.proxy.password } : void 0;
|
|
173
|
+
return { args, proxy };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/browser-provider.ts
|
|
177
|
+
var BrowserProvider = class {
|
|
178
|
+
browser = null;
|
|
179
|
+
context = null;
|
|
180
|
+
page = null;
|
|
181
|
+
pages = /* @__PURE__ */ new Map();
|
|
182
|
+
activeTabId = "tab-0";
|
|
183
|
+
tabCounter = 0;
|
|
184
|
+
_viewport;
|
|
185
|
+
_videoDir;
|
|
186
|
+
_humanize;
|
|
187
|
+
constructor() {
|
|
188
|
+
this._viewport = { width: 1280, height: 720 };
|
|
189
|
+
}
|
|
190
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
191
|
+
async launch(opts) {
|
|
192
|
+
const pw = await import("playwright");
|
|
193
|
+
const chromium = pw.chromium;
|
|
194
|
+
this._viewport = opts?.viewport ?? { width: 1280, height: 720 };
|
|
195
|
+
const stealthEnabled = !!opts?.stealth;
|
|
196
|
+
const stealthCfg = typeof opts?.stealth === "object" ? opts.stealth : {};
|
|
197
|
+
if (opts?.humanize) {
|
|
198
|
+
const h = typeof opts.humanize === "object" ? opts.humanize : {};
|
|
199
|
+
this._humanize = {
|
|
200
|
+
typingDelay: h.typingDelay ?? [40, 120],
|
|
201
|
+
clickJitter: h.clickJitter ?? 3,
|
|
202
|
+
actionDelay: h.actionDelay ?? [200, 800],
|
|
203
|
+
mouseMovement: h.mouseMovement ?? true
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const launchOpts = {
|
|
207
|
+
headless: opts?.headless ?? true
|
|
208
|
+
};
|
|
209
|
+
if (stealthEnabled) {
|
|
210
|
+
const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
|
|
211
|
+
launchOpts.args = args;
|
|
212
|
+
if (proxy) launchOpts.proxy = proxy;
|
|
213
|
+
}
|
|
214
|
+
this.browser = await chromium.launch(launchOpts);
|
|
215
|
+
let contextOpts;
|
|
216
|
+
if (stealthEnabled) {
|
|
217
|
+
contextOpts = buildStealthContextOpts(stealthCfg, this._viewport);
|
|
218
|
+
} else {
|
|
219
|
+
contextOpts = {
|
|
220
|
+
viewport: this._viewport,
|
|
221
|
+
userAgent: pickUserAgent()
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (opts?.storageState) {
|
|
225
|
+
contextOpts.storageState = opts.storageState;
|
|
226
|
+
}
|
|
227
|
+
if (opts?.recordVideo) {
|
|
228
|
+
const dir = typeof opts.recordVideo === "object" ? opts.recordVideo.dir : "./browser-videos";
|
|
229
|
+
contextOpts.recordVideo = { dir, size: this._viewport };
|
|
230
|
+
this._videoDir = dir;
|
|
231
|
+
}
|
|
232
|
+
this.context = await this.browser.newContext(contextOpts);
|
|
233
|
+
if (stealthEnabled && stealthCfg.patchFingerprint !== false) {
|
|
234
|
+
await this.context.addInitScript(getStealthScript());
|
|
235
|
+
}
|
|
236
|
+
this.page = await this.context.newPage();
|
|
237
|
+
this.tabCounter = 0;
|
|
238
|
+
this.activeTabId = "tab-0";
|
|
239
|
+
this.pages.set("tab-0", this.page);
|
|
240
|
+
}
|
|
241
|
+
// ── Cookie / Auth Persistence ────────────────────────────────────────
|
|
242
|
+
async saveStorageState(path) {
|
|
243
|
+
this.ensureContext();
|
|
244
|
+
await this.context.storageState({ path });
|
|
245
|
+
}
|
|
246
|
+
// ── Navigation ───────────────────────────────────────────────────────
|
|
247
|
+
async navigate(url) {
|
|
248
|
+
this.ensurePage();
|
|
249
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
250
|
+
await this.waitForStable(500);
|
|
251
|
+
}
|
|
252
|
+
async back() {
|
|
253
|
+
this.ensurePage();
|
|
254
|
+
await this.page.goBack({ waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
255
|
+
}
|
|
256
|
+
// ── Screenshot ───────────────────────────────────────────────────────
|
|
257
|
+
async screenshot() {
|
|
258
|
+
this.ensurePage();
|
|
259
|
+
return await this.page.screenshot({ type: "png", fullPage: false });
|
|
260
|
+
}
|
|
261
|
+
// ── Interaction (with optional humanize) ─────────────────────────────
|
|
262
|
+
async click(x, y) {
|
|
263
|
+
this.ensurePage();
|
|
264
|
+
const [fx, fy] = this.jitter(x, y);
|
|
265
|
+
if (this._humanize?.mouseMovement) {
|
|
266
|
+
await this.humanMouseMove(fx, fy);
|
|
267
|
+
}
|
|
268
|
+
await this.page.mouse.click(fx, fy);
|
|
269
|
+
await this.humanPause();
|
|
270
|
+
}
|
|
271
|
+
async type(text) {
|
|
272
|
+
this.ensurePage();
|
|
273
|
+
const delay = this._humanize ? this.randInt(this._humanize.typingDelay[0], this._humanize.typingDelay[1]) : 30;
|
|
274
|
+
await this.page.keyboard.type(text, { delay });
|
|
275
|
+
await this.humanPause();
|
|
276
|
+
}
|
|
277
|
+
async clickAndType(x, y, text) {
|
|
278
|
+
await this.click(x, y);
|
|
279
|
+
await this.sleep(this._humanize ? this.randInt(150, 350) : 200);
|
|
280
|
+
const [fx, fy] = this.jitter(x, y);
|
|
281
|
+
await this.page.mouse.click(fx, fy, { clickCount: 3 });
|
|
282
|
+
await this.sleep(this._humanize ? this.randInt(80, 200) : 100);
|
|
283
|
+
await this.type(text);
|
|
284
|
+
}
|
|
285
|
+
async pressKey(key) {
|
|
286
|
+
this.ensurePage();
|
|
287
|
+
await this.page.keyboard.press(key);
|
|
288
|
+
}
|
|
289
|
+
async scroll(direction, amount) {
|
|
290
|
+
this.ensurePage();
|
|
291
|
+
const base = amount ?? 400;
|
|
292
|
+
const jittered = this._humanize ? base + this.randInt(-40, 40) : base;
|
|
293
|
+
const scrollY = direction === "down" ? jittered : -jittered;
|
|
294
|
+
if (this._humanize) {
|
|
295
|
+
const steps = this.randInt(2, 4);
|
|
296
|
+
const perStep = scrollY / steps;
|
|
297
|
+
for (let i = 0; i < steps; i++) {
|
|
298
|
+
await this.page.mouse.wheel(0, perStep);
|
|
299
|
+
await this.sleep(this.randInt(30, 80));
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
await this.page.mouse.wheel(0, scrollY);
|
|
303
|
+
}
|
|
304
|
+
await this.humanPause();
|
|
305
|
+
}
|
|
306
|
+
// ── DOM Extraction ───────────────────────────────────────────────────
|
|
307
|
+
async extractDOM(opts) {
|
|
308
|
+
this.ensurePage();
|
|
309
|
+
const max = opts?.maxElements ?? 80;
|
|
310
|
+
const elements = await this.page.evaluate((limit) => {
|
|
311
|
+
const selectors = [
|
|
312
|
+
"a[href]",
|
|
313
|
+
"button",
|
|
314
|
+
"input",
|
|
315
|
+
"textarea",
|
|
316
|
+
"select",
|
|
317
|
+
"[role='button']",
|
|
318
|
+
"[role='link']",
|
|
319
|
+
"[role='tab']",
|
|
320
|
+
"[role='menuitem']",
|
|
321
|
+
"[onclick]",
|
|
322
|
+
"[contenteditable='true']"
|
|
323
|
+
];
|
|
324
|
+
const all = globalThis.document.querySelectorAll(selectors.join(","));
|
|
325
|
+
const lines = [];
|
|
326
|
+
let count = 0;
|
|
327
|
+
const vh = globalThis.window.innerHeight;
|
|
328
|
+
const vw = globalThis.window.innerWidth;
|
|
329
|
+
for (const el of all) {
|
|
330
|
+
if (count >= limit) break;
|
|
331
|
+
const rect = el.getBoundingClientRect();
|
|
332
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
333
|
+
if (rect.bottom < 0 || rect.top > vh) continue;
|
|
334
|
+
if (rect.right < 0 || rect.left > vw) continue;
|
|
335
|
+
const tag = el.tagName.toLowerCase();
|
|
336
|
+
const role = el.getAttribute("role") || tag;
|
|
337
|
+
const type = el.getAttribute("type") || "";
|
|
338
|
+
const text = (el.textContent || "").trim().slice(0, 80);
|
|
339
|
+
const placeholder = el.getAttribute("placeholder") || "";
|
|
340
|
+
const ariaLabel = el.getAttribute("aria-label") || "";
|
|
341
|
+
const href = el.getAttribute("href") || "";
|
|
342
|
+
const value = el.value || "";
|
|
343
|
+
const cx = Math.round(rect.left + rect.width / 2);
|
|
344
|
+
const cy = Math.round(rect.top + rect.height / 2);
|
|
345
|
+
let label = ariaLabel || text || placeholder || value;
|
|
346
|
+
if (!label && href) label = href.slice(0, 60);
|
|
347
|
+
if (!label) label = `(${tag}${type ? ` type=${type}` : ""})`;
|
|
348
|
+
lines.push(`[${cx},${cy}] ${role}${type ? `(${type})` : ""}: "${label}"`);
|
|
349
|
+
count++;
|
|
350
|
+
}
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}, max);
|
|
353
|
+
return elements;
|
|
354
|
+
}
|
|
355
|
+
// ── Page Info ────────────────────────────────────────────────────────
|
|
356
|
+
async getPageInfo() {
|
|
357
|
+
this.ensurePage();
|
|
358
|
+
const url = this.page.url();
|
|
359
|
+
let title = "";
|
|
360
|
+
try {
|
|
361
|
+
title = await this.page.title();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.warn("[radaros/browser] Error getting page title:", err instanceof Error ? err.message : err);
|
|
364
|
+
}
|
|
365
|
+
return { url, title, viewportSize: this._viewport };
|
|
366
|
+
}
|
|
367
|
+
async waitForStable(minWait = 300) {
|
|
368
|
+
this.ensurePage();
|
|
369
|
+
await this.sleep(minWait);
|
|
370
|
+
try {
|
|
371
|
+
await this.page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.warn("[radaros/browser] Error waiting for stable:", err instanceof Error ? err.message : err);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ── Multi-Tab / Parallel Browsing ────────────────────────────────────
|
|
377
|
+
async newTab(url) {
|
|
378
|
+
this.ensureContext();
|
|
379
|
+
const newPage = await this.context.newPage();
|
|
380
|
+
this.tabCounter++;
|
|
381
|
+
const tabId = `tab-${this.tabCounter}`;
|
|
382
|
+
this.pages.set(tabId, newPage);
|
|
383
|
+
if (url) {
|
|
384
|
+
await newPage.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
385
|
+
}
|
|
386
|
+
return tabId;
|
|
387
|
+
}
|
|
388
|
+
async switchTab(tabId) {
|
|
389
|
+
const targetPage = this.pages.get(tabId);
|
|
390
|
+
if (!targetPage) throw new Error(`Tab "${tabId}" not found`);
|
|
391
|
+
this.page = targetPage;
|
|
392
|
+
this.activeTabId = tabId;
|
|
393
|
+
await this.page.bringToFront();
|
|
394
|
+
}
|
|
395
|
+
async closeTab(tabId) {
|
|
396
|
+
if (this.pages.size <= 1) throw new Error("Cannot close the last tab");
|
|
397
|
+
const targetPage = this.pages.get(tabId);
|
|
398
|
+
if (!targetPage) throw new Error(`Tab "${tabId}" not found`);
|
|
399
|
+
await targetPage.close();
|
|
400
|
+
this.pages.delete(tabId);
|
|
401
|
+
if (this.activeTabId === tabId) {
|
|
402
|
+
const firstRemaining = this.pages.entries().next().value;
|
|
403
|
+
if (firstRemaining) {
|
|
404
|
+
this.activeTabId = firstRemaining[0];
|
|
405
|
+
this.page = firstRemaining[1];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
listTabs() {
|
|
410
|
+
const tabs = [];
|
|
411
|
+
for (const [id, pg] of this.pages) {
|
|
412
|
+
tabs.push({ id, url: pg.url(), active: id === this.activeTabId });
|
|
413
|
+
}
|
|
414
|
+
return tabs;
|
|
415
|
+
}
|
|
416
|
+
get currentTabId() {
|
|
417
|
+
return this.activeTabId;
|
|
418
|
+
}
|
|
419
|
+
// ── Video Recording ──────────────────────────────────────────────────
|
|
420
|
+
async getVideoPath(tabId) {
|
|
421
|
+
const targetPage = tabId ? this.pages.get(tabId) : this.page;
|
|
422
|
+
if (!targetPage) return null;
|
|
423
|
+
try {
|
|
424
|
+
const video = targetPage.video();
|
|
425
|
+
if (!video) return null;
|
|
426
|
+
return await video.path();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.warn("[radaros/browser] Error getting video path:", err instanceof Error ? err.message : err);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
get videoDir() {
|
|
433
|
+
return this._videoDir;
|
|
434
|
+
}
|
|
435
|
+
// ── Cleanup ──────────────────────────────────────────────────────────
|
|
436
|
+
async close() {
|
|
437
|
+
try {
|
|
438
|
+
if (this.context) await this.context.close();
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.warn("[radaros/browser] Error closing context:", err instanceof Error ? err.message : err);
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
if (this.browser) await this.browser.close();
|
|
444
|
+
} catch (err) {
|
|
445
|
+
console.warn("[radaros/browser] Error closing browser:", err instanceof Error ? err.message : err);
|
|
446
|
+
}
|
|
447
|
+
this.page = null;
|
|
448
|
+
this.context = null;
|
|
449
|
+
this.browser = null;
|
|
450
|
+
this.pages.clear();
|
|
451
|
+
}
|
|
452
|
+
// ── Private: Humanize helpers ────────────────────────────────────────
|
|
453
|
+
/** Add small random offset to coordinates to avoid pixel-perfect bot patterns. */
|
|
454
|
+
jitter(x, y) {
|
|
455
|
+
if (!this._humanize) return [x, y];
|
|
456
|
+
const j = this._humanize.clickJitter;
|
|
457
|
+
return [x + this.randInt(-j, j), y + this.randInt(-j, j)];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Simulate human mouse movement using Bézier-like interpolation.
|
|
461
|
+
* Moves from the current mouse position to the target in small steps.
|
|
462
|
+
*/
|
|
463
|
+
async humanMouseMove(targetX, targetY) {
|
|
464
|
+
const steps = this.randInt(5, 12);
|
|
465
|
+
const startX = this._viewport.width / 2;
|
|
466
|
+
const startY = this._viewport.height / 2;
|
|
467
|
+
for (let i = 1; i <= steps; i++) {
|
|
468
|
+
const t = i / steps;
|
|
469
|
+
const ease = t * t * (3 - 2 * t);
|
|
470
|
+
const cx = startX + (targetX - startX) * ease + this.randInt(-2, 2);
|
|
471
|
+
const cy = startY + (targetY - startY) * ease + this.randInt(-2, 2);
|
|
472
|
+
await this.page.mouse.move(cx, cy);
|
|
473
|
+
await this.sleep(this.randInt(5, 20));
|
|
474
|
+
}
|
|
475
|
+
await this.page.mouse.move(targetX, targetY);
|
|
476
|
+
}
|
|
477
|
+
/** Small random pause after an interaction. */
|
|
478
|
+
async humanPause() {
|
|
479
|
+
if (!this._humanize) return;
|
|
480
|
+
const [min, max] = this._humanize.actionDelay;
|
|
481
|
+
await this.sleep(this.randInt(min, max));
|
|
482
|
+
}
|
|
483
|
+
randInt(min, max) {
|
|
484
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
485
|
+
}
|
|
486
|
+
ensurePage() {
|
|
487
|
+
if (!this.page) throw new Error("Browser not launched. Call launch() first.");
|
|
488
|
+
}
|
|
489
|
+
ensureContext() {
|
|
490
|
+
if (!this.context) throw new Error("Browser not launched. Call launch() first.");
|
|
491
|
+
}
|
|
492
|
+
sleep(ms) {
|
|
493
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/prompts.ts
|
|
498
|
+
function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
|
|
499
|
+
const lines = [
|
|
500
|
+
`You are a browser automation agent. You receive a screenshot of a web browser and decide what action to take next to complete the user's task.`,
|
|
501
|
+
``,
|
|
502
|
+
`## Viewport`,
|
|
503
|
+
`The browser viewport is ${viewport.width}\xD7${viewport.height} pixels. All coordinates you provide must be within this range.`,
|
|
504
|
+
``,
|
|
505
|
+
`## Available Actions`,
|
|
506
|
+
`Respond with a JSON object containing one of these actions:`,
|
|
507
|
+
``,
|
|
508
|
+
`### click`,
|
|
509
|
+
`Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
|
|
510
|
+
`\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
|
|
511
|
+
``,
|
|
512
|
+
`### type`,
|
|
513
|
+
`Type text. If x/y are provided, click that position first (to focus the input), then type. If omitted, types into the currently focused element. To press Enter after typing (e.g., to submit a search), append "\\n" to the text.`,
|
|
514
|
+
`\`{ "action": "type", "text": "<text to type>", "x": <number|optional>, "y": <number|optional> }\``,
|
|
515
|
+
`Example: \`{ "action": "type", "text": "search query\\n", "x": 640, "y": 300 }\` \u2014 clicks the search box, types, and presses Enter.`,
|
|
516
|
+
``,
|
|
517
|
+
`### scroll`,
|
|
518
|
+
`Scroll the page up or down. Use when content is below or above the visible area.`,
|
|
519
|
+
`\`{ "action": "scroll", "direction": "up"|"down", "amount": <pixels, optional, default 400> }\``,
|
|
520
|
+
``,
|
|
521
|
+
`### navigate`,
|
|
522
|
+
`Navigate to a specific URL. Use when you know the exact URL to visit.`,
|
|
523
|
+
`\`{ "action": "navigate", "url": "<full URL>" }\``,
|
|
524
|
+
``,
|
|
525
|
+
`### back`,
|
|
526
|
+
`Go back to the previous page.`,
|
|
527
|
+
`\`{ "action": "back" }\``,
|
|
528
|
+
``,
|
|
529
|
+
`### wait`,
|
|
530
|
+
`Wait for the page to load or for a timed event. Use sparingly.`,
|
|
531
|
+
`\`{ "action": "wait", "ms": <milliseconds> }\``,
|
|
532
|
+
``,
|
|
533
|
+
`### done`,
|
|
534
|
+
`The task is complete. Provide a summary of what was accomplished.`,
|
|
535
|
+
`\`{ "action": "done", "result": "<summary of what was accomplished>" }\``,
|
|
536
|
+
``,
|
|
537
|
+
`### fail`,
|
|
538
|
+
`The task cannot be completed. Explain why.`,
|
|
539
|
+
`\`{ "action": "fail", "reason": "<why the task failed>" }\``,
|
|
540
|
+
``,
|
|
541
|
+
`## Rules`,
|
|
542
|
+
`1. ALWAYS look at the screenshot carefully before deciding your action.`,
|
|
543
|
+
`2. Provide coordinates that target the CENTER of the element you want to interact with.`,
|
|
544
|
+
`3. For text inputs: click the input field first (using "type" with x/y), then the text will be typed.`,
|
|
545
|
+
`4. After typing in a search box, you often need to press Enter \u2014 use type with text "\\n" or click the search/submit button.`,
|
|
546
|
+
`5. If your previous action didn't produce the expected result, try a different approach.`,
|
|
547
|
+
`6. If a page is loading or blank, use "wait" with a short delay and try again.`,
|
|
548
|
+
`7. If you see a cookie banner or popup, dismiss it first before proceeding with the task.`,
|
|
549
|
+
`8. NEVER hallucinate content. Only report what you can actually see on the screen.`,
|
|
550
|
+
`9. When the task is fully complete, use "done" immediately with a comprehensive result.`,
|
|
551
|
+
`10. If after several attempts you cannot complete the task, use "fail" with a clear reason.`,
|
|
552
|
+
``,
|
|
553
|
+
`## Response Format`,
|
|
554
|
+
`Respond with ONLY a valid JSON object. No markdown, no explanation, just the JSON action.`
|
|
555
|
+
];
|
|
556
|
+
if (credentialKeys && credentialKeys.length > 0) {
|
|
557
|
+
lines.push(
|
|
558
|
+
``,
|
|
559
|
+
`## Secure Credentials`,
|
|
560
|
+
`The following credential placeholders are available for use in "type" actions:`,
|
|
561
|
+
...credentialKeys.map((k) => `- \`{{${k}}}\``),
|
|
562
|
+
``,
|
|
563
|
+
`When you need to fill in a login form or any field requiring these credentials,`,
|
|
564
|
+
`use the EXACT placeholder (e.g. \`{{email}}\`) as the "text" value in a type action.`,
|
|
565
|
+
`The system will securely replace them with real values at execution time.`,
|
|
566
|
+
`NEVER guess, invent, or ask the user for the actual credential values.`,
|
|
567
|
+
`NEVER include real credential values in "done" or "fail" results.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (extraInstructions) {
|
|
571
|
+
lines.push(``, `## Additional Instructions`, extraInstructions);
|
|
572
|
+
}
|
|
573
|
+
return lines.join("\n");
|
|
574
|
+
}
|
|
575
|
+
function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot) {
|
|
576
|
+
const lines = [];
|
|
577
|
+
lines.push(`**Task:** ${task}`);
|
|
578
|
+
lines.push(`**Current URL:** ${pageUrl}`);
|
|
579
|
+
if (pageTitle) lines.push(`**Page Title:** ${pageTitle}`);
|
|
580
|
+
lines.push(`**Step:** ${stepIndex + 1}`);
|
|
581
|
+
if (domSnapshot) {
|
|
582
|
+
lines.push(``);
|
|
583
|
+
lines.push(`**Interactive elements on page (format: [centerX,centerY] role: "label"):**`);
|
|
584
|
+
lines.push(domSnapshot);
|
|
585
|
+
}
|
|
586
|
+
if (actionHistory.length > 0) {
|
|
587
|
+
lines.push(``);
|
|
588
|
+
lines.push(`**Previous actions:**`);
|
|
589
|
+
for (const entry of actionHistory.slice(-10)) {
|
|
590
|
+
lines.push(`- ${entry}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
lines.push(``);
|
|
594
|
+
lines.push(`Look at the screenshot and decide the next action to complete the task.`);
|
|
595
|
+
return lines.join("\n");
|
|
596
|
+
}
|
|
597
|
+
function summarizeAction(action) {
|
|
598
|
+
switch (action.action) {
|
|
599
|
+
case "click":
|
|
600
|
+
return `Clicked at (${action.x}, ${action.y}): ${action.description}`;
|
|
601
|
+
case "type":
|
|
602
|
+
return action.x != null ? `Clicked (${action.x}, ${action.y}) and typed "${action.text}"` : `Typed "${action.text}"`;
|
|
603
|
+
case "scroll":
|
|
604
|
+
return `Scrolled ${action.direction}${action.amount ? ` ${action.amount}px` : ""}`;
|
|
605
|
+
case "navigate":
|
|
606
|
+
return `Navigated to ${action.url}`;
|
|
607
|
+
case "back":
|
|
608
|
+
return `Went back to previous page`;
|
|
609
|
+
case "wait":
|
|
610
|
+
return `Waited ${action.ms}ms`;
|
|
611
|
+
case "screenshot":
|
|
612
|
+
return `Took an extra screenshot`;
|
|
613
|
+
case "done":
|
|
614
|
+
return `Done: ${action.result}`;
|
|
615
|
+
case "fail":
|
|
616
|
+
return `Failed: ${action.reason}`;
|
|
617
|
+
default:
|
|
618
|
+
return JSON.stringify(action);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/browser-agent.ts
|
|
623
|
+
var BrowserAgent = class {
|
|
624
|
+
name;
|
|
625
|
+
eventBus;
|
|
626
|
+
model;
|
|
627
|
+
instructions;
|
|
628
|
+
maxSteps;
|
|
629
|
+
headless;
|
|
630
|
+
viewport;
|
|
631
|
+
defaultStartUrl;
|
|
632
|
+
waitAfterAction;
|
|
633
|
+
maxRepeats;
|
|
634
|
+
useDOM;
|
|
635
|
+
storageState;
|
|
636
|
+
recordVideo;
|
|
637
|
+
credentials;
|
|
638
|
+
stealth;
|
|
639
|
+
humanize;
|
|
640
|
+
memoryManager = null;
|
|
641
|
+
logger;
|
|
642
|
+
/** Access the MemoryManager (if memory is configured). */
|
|
643
|
+
get memory() {
|
|
644
|
+
return this.memoryManager;
|
|
645
|
+
}
|
|
646
|
+
constructor(config) {
|
|
647
|
+
this.name = config.name;
|
|
648
|
+
this.model = config.model;
|
|
649
|
+
this.instructions = config.instructions;
|
|
650
|
+
this.maxSteps = config.maxSteps ?? 30;
|
|
651
|
+
this.headless = config.headless ?? true;
|
|
652
|
+
this.viewport = config.viewport ?? { width: 1280, height: 720 };
|
|
653
|
+
this.defaultStartUrl = config.startUrl;
|
|
654
|
+
this.waitAfterAction = config.waitAfterAction ?? 1500;
|
|
655
|
+
this.maxRepeats = config.maxRepeats ?? 3;
|
|
656
|
+
this.useDOM = config.useDOM ?? false;
|
|
657
|
+
this.storageState = config.storageState;
|
|
658
|
+
this.recordVideo = config.recordVideo;
|
|
659
|
+
this.credentials = config.credentials;
|
|
660
|
+
this.stealth = config.stealth;
|
|
661
|
+
this.humanize = config.humanize;
|
|
662
|
+
this.eventBus = config.eventBus ?? new import_core.EventBus();
|
|
663
|
+
this.logger = new import_core.Logger({
|
|
664
|
+
prefix: `BrowserAgent:${config.name}`,
|
|
665
|
+
level: config.logLevel ?? "silent"
|
|
666
|
+
});
|
|
667
|
+
if (config.memory) {
|
|
668
|
+
this.memoryManager = new import_core.MemoryManager(config.memory);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async run(task, opts) {
|
|
672
|
+
const startTime = Date.now();
|
|
673
|
+
const startUrl = opts?.startUrl ?? this.defaultStartUrl;
|
|
674
|
+
const sessionId = opts?.sessionId ?? `browser_${Date.now()}`;
|
|
675
|
+
const userId = opts?.userId;
|
|
676
|
+
const browser = new BrowserProvider();
|
|
677
|
+
const steps = [];
|
|
678
|
+
const actionHistory = [];
|
|
679
|
+
let extraInstructions = this.instructions ?? "";
|
|
680
|
+
if (this.memoryManager) {
|
|
681
|
+
await this.memoryManager.ensureReady();
|
|
682
|
+
const memoryContext = await this.memoryManager.buildContext(sessionId, userId, task, this.name);
|
|
683
|
+
if (memoryContext) {
|
|
684
|
+
extraInstructions = extraInstructions ? `${extraInstructions}
|
|
685
|
+
|
|
686
|
+
${memoryContext}` : memoryContext;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const credentialKeys = this.credentials?.keys();
|
|
690
|
+
const systemPrompt = buildSystemPrompt(this.viewport, extraInstructions || void 0, credentialKeys);
|
|
691
|
+
let lastActionKey = "";
|
|
692
|
+
let repeatCount = 0;
|
|
693
|
+
try {
|
|
694
|
+
this.logger.info("Launching browser", {
|
|
695
|
+
headless: this.headless,
|
|
696
|
+
viewport: this.viewport,
|
|
697
|
+
useDOM: this.useDOM,
|
|
698
|
+
recordVideo: !!this.recordVideo,
|
|
699
|
+
stealth: !!this.stealth,
|
|
700
|
+
humanize: !!this.humanize
|
|
701
|
+
});
|
|
702
|
+
await browser.launch({
|
|
703
|
+
headless: this.headless,
|
|
704
|
+
viewport: this.viewport,
|
|
705
|
+
storageState: this.storageState,
|
|
706
|
+
recordVideo: this.recordVideo,
|
|
707
|
+
stealth: this.stealth,
|
|
708
|
+
humanize: this.humanize
|
|
709
|
+
});
|
|
710
|
+
if (startUrl) {
|
|
711
|
+
this.logger.info("Navigating to start URL", { url: startUrl });
|
|
712
|
+
await browser.navigate(startUrl);
|
|
713
|
+
}
|
|
714
|
+
for (let step = 0; step < this.maxSteps; step++) {
|
|
715
|
+
const screenshot = await browser.screenshot();
|
|
716
|
+
const pageInfo = await browser.getPageInfo();
|
|
717
|
+
let domSnapshot;
|
|
718
|
+
if (this.useDOM) {
|
|
719
|
+
domSnapshot = await browser.extractDOM();
|
|
720
|
+
}
|
|
721
|
+
this.eventBus.emit("browser.screenshot", { data: screenshot });
|
|
722
|
+
const userText = buildUserMessage(task, pageInfo.url, pageInfo.title, step, actionHistory, domSnapshot);
|
|
723
|
+
const messages = [
|
|
724
|
+
{ role: "system", content: systemPrompt },
|
|
725
|
+
{
|
|
726
|
+
role: "user",
|
|
727
|
+
content: [
|
|
728
|
+
{
|
|
729
|
+
type: "image",
|
|
730
|
+
data: screenshot.toString("base64"),
|
|
731
|
+
mimeType: "image/png"
|
|
732
|
+
},
|
|
733
|
+
{ type: "text", text: userText }
|
|
734
|
+
]
|
|
735
|
+
}
|
|
736
|
+
];
|
|
737
|
+
this.logger.debug("Sending screenshot to vision model", { step, url: pageInfo.url });
|
|
738
|
+
const response = await this.model.generate(messages, {
|
|
739
|
+
temperature: 0.1,
|
|
740
|
+
maxTokens: 1024,
|
|
741
|
+
apiKey: opts?.apiKey,
|
|
742
|
+
responseFormat: "json"
|
|
743
|
+
});
|
|
744
|
+
const raw = typeof response.message.content === "string" ? response.message.content : "";
|
|
745
|
+
let action;
|
|
746
|
+
try {
|
|
747
|
+
action = JSON.parse(raw);
|
|
748
|
+
} catch {
|
|
749
|
+
this.logger.warn("Failed to parse model response as JSON, retrying", { raw });
|
|
750
|
+
actionHistory.push("(invalid JSON response \u2014 retrying)");
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const step_ = {
|
|
754
|
+
index: step,
|
|
755
|
+
action,
|
|
756
|
+
screenshot,
|
|
757
|
+
pageUrl: pageInfo.url,
|
|
758
|
+
pageTitle: pageInfo.title,
|
|
759
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
760
|
+
dom: domSnapshot
|
|
761
|
+
};
|
|
762
|
+
steps.push(step_);
|
|
763
|
+
let summary = summarizeAction(action);
|
|
764
|
+
if (this.credentials) {
|
|
765
|
+
summary = this.credentials.mask(summary);
|
|
766
|
+
}
|
|
767
|
+
const actionKey = JSON.stringify(action);
|
|
768
|
+
if (actionKey === lastActionKey) {
|
|
769
|
+
repeatCount++;
|
|
770
|
+
} else {
|
|
771
|
+
lastActionKey = actionKey;
|
|
772
|
+
repeatCount = 1;
|
|
773
|
+
}
|
|
774
|
+
if (repeatCount > this.maxRepeats && action.action !== "done" && action.action !== "fail") {
|
|
775
|
+
this.logger.warn("Stuck in a loop \u2014 same action repeated", {
|
|
776
|
+
action: action.action,
|
|
777
|
+
repeats: repeatCount,
|
|
778
|
+
maxRepeats: this.maxRepeats
|
|
779
|
+
});
|
|
780
|
+
actionHistory.push(
|
|
781
|
+
`\u26A0 LOOP DETECTED: "${summary}" repeated ${repeatCount} times. The agent was stuck and auto-stopped. Try a different approach or a different startUrl.`
|
|
782
|
+
);
|
|
783
|
+
return await this.finalize(browser, steps, startTime, opts, {
|
|
784
|
+
result: `Stuck in a loop: "${summary}" was repeated ${repeatCount} times. The page may have a popup, consent banner, or unexpected state blocking progress.`,
|
|
785
|
+
success: false
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
actionHistory.push(summary);
|
|
789
|
+
this.logger.info(`Step ${step + 1}: ${summary}`);
|
|
790
|
+
this.eventBus.emit("browser.action", { action });
|
|
791
|
+
this.eventBus.emit("browser.step", {
|
|
792
|
+
index: step,
|
|
793
|
+
action,
|
|
794
|
+
pageUrl: pageInfo.url,
|
|
795
|
+
screenshot
|
|
796
|
+
});
|
|
797
|
+
if (action.action === "done") {
|
|
798
|
+
const result = this.credentials ? this.credentials.mask(action.result) : action.result;
|
|
799
|
+
return await this.finalize(browser, steps, startTime, opts, {
|
|
800
|
+
result,
|
|
801
|
+
success: true
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (action.action === "fail") {
|
|
805
|
+
const result = this.credentials ? this.credentials.mask(action.reason) : action.reason;
|
|
806
|
+
return await this.finalize(browser, steps, startTime, opts, {
|
|
807
|
+
result,
|
|
808
|
+
success: false
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
await this.executeAction(browser, action);
|
|
812
|
+
await this.sleep(this.waitAfterAction);
|
|
813
|
+
}
|
|
814
|
+
this.logger.warn("Max steps reached without completing task", { maxSteps: this.maxSteps });
|
|
815
|
+
return await this.finalize(browser, steps, startTime, opts, {
|
|
816
|
+
result: `Task not completed within ${this.maxSteps} steps. Last actions: ${actionHistory.slice(-3).join("; ")}`,
|
|
817
|
+
success: false
|
|
818
|
+
});
|
|
819
|
+
} catch (error) {
|
|
820
|
+
this.logger.error("Browser agent error", { error: error.message });
|
|
821
|
+
this.eventBus.emit("browser.error", { error });
|
|
822
|
+
await browser.close();
|
|
823
|
+
return {
|
|
824
|
+
result: `Error: ${error.message}`,
|
|
825
|
+
success: false,
|
|
826
|
+
steps,
|
|
827
|
+
finalUrl: "",
|
|
828
|
+
finalScreenshot: Buffer.alloc(0),
|
|
829
|
+
durationMs: Date.now() - startTime
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Returns a ToolDef that lets a regular Agent delegate browser tasks
|
|
835
|
+
* to this BrowserAgent.
|
|
836
|
+
*/
|
|
837
|
+
asTool(config) {
|
|
838
|
+
return {
|
|
839
|
+
name: config?.name ?? "browse_web",
|
|
840
|
+
description: config?.description ?? "Open a browser and autonomously complete a task on a website. Provide a clear task description and optionally a starting URL.",
|
|
841
|
+
parameters: import_zod.z.object({
|
|
842
|
+
task: import_zod.z.string().describe("What to do in the browser (e.g., 'Search for X and return the top 3 results')"),
|
|
843
|
+
startUrl: import_zod.z.string().optional().describe("URL to start at (e.g., 'https://www.google.com')")
|
|
844
|
+
}),
|
|
845
|
+
execute: async (args) => {
|
|
846
|
+
const result = await this.run(args.task, { startUrl: args.startUrl });
|
|
847
|
+
return result.result;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
// ── Private helpers ──────────────────────────────────────────────────
|
|
852
|
+
async finalize(browser, steps, startTime, opts, outcome) {
|
|
853
|
+
const finalScreenshot = await browser.screenshot();
|
|
854
|
+
const finalInfo = await browser.getPageInfo();
|
|
855
|
+
if (opts?.saveStorageState) {
|
|
856
|
+
try {
|
|
857
|
+
await browser.saveStorageState(opts.saveStorageState);
|
|
858
|
+
this.logger.info("Storage state saved", { path: opts.saveStorageState });
|
|
859
|
+
} catch (e) {
|
|
860
|
+
this.logger.warn("Failed to save storage state", { error: e.message });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
let videoPath;
|
|
864
|
+
if (this.recordVideo) {
|
|
865
|
+
videoPath = await browser.getVideoPath() ?? void 0;
|
|
866
|
+
}
|
|
867
|
+
await browser.close();
|
|
868
|
+
const output = {
|
|
869
|
+
result: outcome.result,
|
|
870
|
+
success: outcome.success,
|
|
871
|
+
steps,
|
|
872
|
+
finalUrl: finalInfo.url,
|
|
873
|
+
finalScreenshot,
|
|
874
|
+
durationMs: Date.now() - startTime,
|
|
875
|
+
videoPath
|
|
876
|
+
};
|
|
877
|
+
if (this.memoryManager) {
|
|
878
|
+
const sessionId = opts?.sessionId ?? `browser_${startTime}`;
|
|
879
|
+
const userId = opts?.userId;
|
|
880
|
+
const actionSummary = steps.map((s) => summarizeAction(s.action)).join("; ");
|
|
881
|
+
const messages = [
|
|
882
|
+
{ role: "user", content: `Task: ${outcome.result}` },
|
|
883
|
+
{ role: "assistant", content: `Actions: ${actionSummary}. Result: ${outcome.result}` }
|
|
884
|
+
];
|
|
885
|
+
this.memoryManager.appendMessages(sessionId, messages, this.model).catch((e) => this.logger.warn("Memory persist failed", { error: String(e) }));
|
|
886
|
+
this.memoryManager.afterRun(sessionId, userId, messages, this.model, this.name);
|
|
887
|
+
}
|
|
888
|
+
this.eventBus.emit("browser.done", {
|
|
889
|
+
result: output.result,
|
|
890
|
+
success: outcome.success,
|
|
891
|
+
steps
|
|
892
|
+
});
|
|
893
|
+
return output;
|
|
894
|
+
}
|
|
895
|
+
async executeAction(browser, action) {
|
|
896
|
+
try {
|
|
897
|
+
switch (action.action) {
|
|
898
|
+
case "click":
|
|
899
|
+
await browser.click(action.x, action.y);
|
|
900
|
+
break;
|
|
901
|
+
case "type": {
|
|
902
|
+
const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
|
|
903
|
+
if (action.x != null && action.y != null) {
|
|
904
|
+
await browser.clickAndType(action.x, action.y, resolvedText);
|
|
905
|
+
} else {
|
|
906
|
+
await browser.type(resolvedText);
|
|
907
|
+
}
|
|
908
|
+
if (resolvedText.includes("\n")) {
|
|
909
|
+
await browser.pressKey("Enter");
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case "scroll":
|
|
914
|
+
await browser.scroll(action.direction, action.amount);
|
|
915
|
+
break;
|
|
916
|
+
case "navigate":
|
|
917
|
+
await browser.navigate(action.url);
|
|
918
|
+
break;
|
|
919
|
+
case "back":
|
|
920
|
+
await browser.back();
|
|
921
|
+
break;
|
|
922
|
+
case "wait":
|
|
923
|
+
await this.sleep(Math.min(action.ms, 1e4));
|
|
924
|
+
break;
|
|
925
|
+
case "screenshot":
|
|
926
|
+
break;
|
|
927
|
+
default:
|
|
928
|
+
this.logger.warn("Unknown action", { action });
|
|
929
|
+
}
|
|
930
|
+
} catch (error) {
|
|
931
|
+
this.logger.warn("Action execution failed", { action: action.action, error: error.message });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
sleep(ms) {
|
|
935
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// src/credential-vault.ts
|
|
940
|
+
var CredentialVault = class {
|
|
941
|
+
secrets = /* @__PURE__ */ new Map();
|
|
942
|
+
constructor(initial) {
|
|
943
|
+
if (initial) {
|
|
944
|
+
for (const [key, value] of Object.entries(initial)) {
|
|
945
|
+
this.set(key, value);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/** Store a credential. Key names become the placeholder: `{{key}}`. */
|
|
950
|
+
set(key, value) {
|
|
951
|
+
this.secrets.set(key.toLowerCase(), value);
|
|
952
|
+
return this;
|
|
953
|
+
}
|
|
954
|
+
/** Retrieve a credential value. Returns undefined if not found. */
|
|
955
|
+
get(key) {
|
|
956
|
+
return this.secrets.get(key.toLowerCase());
|
|
957
|
+
}
|
|
958
|
+
has(key) {
|
|
959
|
+
return this.secrets.has(key.toLowerCase());
|
|
960
|
+
}
|
|
961
|
+
/** List available placeholder names (never exposes values). */
|
|
962
|
+
keys() {
|
|
963
|
+
return [...this.secrets.keys()];
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Load credentials from environment variables.
|
|
967
|
+
* Maps env var names to placeholder keys.
|
|
968
|
+
*
|
|
969
|
+
* @example
|
|
970
|
+
* vault.fromEnv({ email: "LOGIN_EMAIL", password: "LOGIN_PASS" });
|
|
971
|
+
*/
|
|
972
|
+
fromEnv(mapping) {
|
|
973
|
+
for (const [key, envVar] of Object.entries(mapping)) {
|
|
974
|
+
const value = process.env[envVar];
|
|
975
|
+
if (value) this.set(key, value);
|
|
976
|
+
}
|
|
977
|
+
return this;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Replace `{{key}}` placeholders in text with actual credential values.
|
|
981
|
+
* Used internally by BrowserAgent right before executing a type action.
|
|
982
|
+
*/
|
|
983
|
+
resolve(text) {
|
|
984
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
985
|
+
return this.get(key) ?? `{{${key}}}`;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Replace any occurrence of real credential values in text with
|
|
990
|
+
* their `{{key}}` placeholder. Used to sanitize logs and action history.
|
|
991
|
+
*/
|
|
992
|
+
mask(text) {
|
|
993
|
+
let masked = text;
|
|
994
|
+
for (const [key, value] of this.secrets) {
|
|
995
|
+
if (value && masked.includes(value)) {
|
|
996
|
+
masked = masked.split(value).join(`{{${key}}}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return masked;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1003
|
+
0 && (module.exports = {
|
|
1004
|
+
BrowserAgent,
|
|
1005
|
+
BrowserProvider,
|
|
1006
|
+
CredentialVault
|
|
1007
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as _radaros_core from '@radaros/core';
|
|
2
|
+
import { ModelProvider, UnifiedMemoryConfig, LogLevel, EventBus, MemoryManager, ToolDef } from '@radaros/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Secure credential store for BrowserAgent.
|
|
6
|
+
*
|
|
7
|
+
* Secrets are stored in memory and NEVER sent to the LLM.
|
|
8
|
+
* The model works with placeholders (e.g. `{{email}}`, `{{password}}`),
|
|
9
|
+
* and the agent resolves them to real values only at execution time.
|
|
10
|
+
*/
|
|
11
|
+
declare class CredentialVault {
|
|
12
|
+
private secrets;
|
|
13
|
+
constructor(initial?: Record<string, string>);
|
|
14
|
+
/** Store a credential. Key names become the placeholder: `{{key}}`. */
|
|
15
|
+
set(key: string, value: string): this;
|
|
16
|
+
/** Retrieve a credential value. Returns undefined if not found. */
|
|
17
|
+
get(key: string): string | undefined;
|
|
18
|
+
has(key: string): boolean;
|
|
19
|
+
/** List available placeholder names (never exposes values). */
|
|
20
|
+
keys(): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Load credentials from environment variables.
|
|
23
|
+
* Maps env var names to placeholder keys.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* vault.fromEnv({ email: "LOGIN_EMAIL", password: "LOGIN_PASS" });
|
|
27
|
+
*/
|
|
28
|
+
fromEnv(mapping: Record<string, string>): this;
|
|
29
|
+
/**
|
|
30
|
+
* Replace `{{key}}` placeholders in text with actual credential values.
|
|
31
|
+
* Used internally by BrowserAgent right before executing a type action.
|
|
32
|
+
*/
|
|
33
|
+
resolve(text: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Replace any occurrence of real credential values in text with
|
|
36
|
+
* their `{{key}}` placeholder. Used to sanitize logs and action history.
|
|
37
|
+
*/
|
|
38
|
+
mask(text: string): string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type BrowserAction = {
|
|
42
|
+
action: "click";
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
description: string;
|
|
46
|
+
} | {
|
|
47
|
+
action: "type";
|
|
48
|
+
text: string;
|
|
49
|
+
x?: number;
|
|
50
|
+
y?: number;
|
|
51
|
+
} | {
|
|
52
|
+
action: "scroll";
|
|
53
|
+
direction: "up" | "down";
|
|
54
|
+
amount?: number;
|
|
55
|
+
} | {
|
|
56
|
+
action: "navigate";
|
|
57
|
+
url: string;
|
|
58
|
+
} | {
|
|
59
|
+
action: "back";
|
|
60
|
+
} | {
|
|
61
|
+
action: "wait";
|
|
62
|
+
ms: number;
|
|
63
|
+
} | {
|
|
64
|
+
action: "screenshot";
|
|
65
|
+
} | {
|
|
66
|
+
action: "done";
|
|
67
|
+
result: string;
|
|
68
|
+
} | {
|
|
69
|
+
action: "fail";
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
interface BrowserAgentConfig {
|
|
73
|
+
name: string;
|
|
74
|
+
/** Vision-capable model (GPT-4o, Gemini, etc.) */
|
|
75
|
+
model: ModelProvider;
|
|
76
|
+
/** Extra instructions appended to the system prompt */
|
|
77
|
+
instructions?: string;
|
|
78
|
+
/** Max vision loop iterations. Default: 30 */
|
|
79
|
+
maxSteps?: number;
|
|
80
|
+
/** Run browser without visible window. Default: true */
|
|
81
|
+
headless?: boolean;
|
|
82
|
+
/** Browser viewport size. Default: 1280x720 */
|
|
83
|
+
viewport?: {
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
};
|
|
87
|
+
/** Initial URL to navigate to before starting the task */
|
|
88
|
+
startUrl?: string;
|
|
89
|
+
/** Milliseconds to wait after each action for the page to settle. Default: 1500 */
|
|
90
|
+
waitAfterAction?: number;
|
|
91
|
+
/** Max consecutive identical actions before the agent auto-fails. Default: 3 */
|
|
92
|
+
maxRepeats?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Include a simplified DOM/accessibility tree alongside the screenshot.
|
|
95
|
+
* Helps the model target elements more accurately. Default: false
|
|
96
|
+
*/
|
|
97
|
+
useDOM?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Path to a Playwright storageState JSON file.
|
|
100
|
+
* Restores cookies, localStorage, and sessionStorage from a previous session.
|
|
101
|
+
*/
|
|
102
|
+
storageState?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Enable video recording of the browser session.
|
|
105
|
+
* Pass `true` for default dir (`./browser-videos`) or `{ dir: "/path" }`.
|
|
106
|
+
*/
|
|
107
|
+
recordVideo?: boolean | {
|
|
108
|
+
dir: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Secure credential vault. The LLM never sees real values — only
|
|
112
|
+
* placeholders like `{{email}}`, `{{password}}`. Real values are
|
|
113
|
+
* injected at execution time and scrubbed from all logs.
|
|
114
|
+
*/
|
|
115
|
+
credentials?: CredentialVault;
|
|
116
|
+
/**
|
|
117
|
+
* Enable stealth mode to avoid bot detection.
|
|
118
|
+
* Pass `true` for sensible defaults or a `StealthConfig` object for fine control.
|
|
119
|
+
* Patches navigator.webdriver, plugins, permissions, WebGL, and more.
|
|
120
|
+
*/
|
|
121
|
+
stealth?: boolean | StealthConfig;
|
|
122
|
+
/**
|
|
123
|
+
* Simulate human-like behavior — jittered clicks, variable typing speed,
|
|
124
|
+
* mouse movement curves, random micro-pauses.
|
|
125
|
+
* Pass `true` for defaults or a `HumanizeConfig` for fine control.
|
|
126
|
+
*/
|
|
127
|
+
humanize?: boolean | HumanizeConfig;
|
|
128
|
+
/**
|
|
129
|
+
* Unified memory config — persist browser sessions, decisions, and
|
|
130
|
+
* summaries of past runs. Same config as Agent and VoiceAgent.
|
|
131
|
+
*/
|
|
132
|
+
memory?: UnifiedMemoryConfig;
|
|
133
|
+
/** Skills — pre-packaged or learned tool bundles. */
|
|
134
|
+
skills?: Array<_radaros_core.Skill | string>;
|
|
135
|
+
logLevel?: LogLevel;
|
|
136
|
+
eventBus?: EventBus;
|
|
137
|
+
}
|
|
138
|
+
interface BrowserRunOpts {
|
|
139
|
+
/** Override startUrl from config */
|
|
140
|
+
startUrl?: string;
|
|
141
|
+
/** Per-run model API key override */
|
|
142
|
+
apiKey?: string;
|
|
143
|
+
/** Session identifier for memory persistence and event tracking */
|
|
144
|
+
sessionId?: string;
|
|
145
|
+
/** User identifier for memory personalization */
|
|
146
|
+
userId?: string;
|
|
147
|
+
/** Path to save storageState (cookies/auth) after the run completes */
|
|
148
|
+
saveStorageState?: string;
|
|
149
|
+
}
|
|
150
|
+
interface BrowserRunOutput {
|
|
151
|
+
/** Final text result produced by the agent */
|
|
152
|
+
result: string;
|
|
153
|
+
/** Whether the task completed successfully (vs maxSteps exhausted or fail) */
|
|
154
|
+
success: boolean;
|
|
155
|
+
/** Full action history with screenshots */
|
|
156
|
+
steps: BrowserStep[];
|
|
157
|
+
/** URL at completion */
|
|
158
|
+
finalUrl: string;
|
|
159
|
+
/** Last screenshot captured */
|
|
160
|
+
finalScreenshot: Buffer;
|
|
161
|
+
/** Total time taken in milliseconds */
|
|
162
|
+
durationMs: number;
|
|
163
|
+
/** Video file path (if recordVideo was enabled) */
|
|
164
|
+
videoPath?: string;
|
|
165
|
+
}
|
|
166
|
+
interface BrowserStep {
|
|
167
|
+
index: number;
|
|
168
|
+
action: BrowserAction;
|
|
169
|
+
/** Screenshot taken before this action was executed */
|
|
170
|
+
screenshot: Buffer;
|
|
171
|
+
pageUrl: string;
|
|
172
|
+
pageTitle: string;
|
|
173
|
+
timestamp: Date;
|
|
174
|
+
/** Simplified DOM snapshot (if useDOM is enabled) */
|
|
175
|
+
dom?: string;
|
|
176
|
+
}
|
|
177
|
+
interface StealthConfig {
|
|
178
|
+
/**
|
|
179
|
+
* Remove `navigator.webdriver` flag and patch common detection vectors
|
|
180
|
+
* (plugins, languages, permissions, WebGL, etc.). Default when stealth=true.
|
|
181
|
+
*/
|
|
182
|
+
patchFingerprint?: boolean;
|
|
183
|
+
/** Custom User-Agent string. A realistic one is used by default. */
|
|
184
|
+
userAgent?: string;
|
|
185
|
+
/** Browser locale. Default: "en-US" */
|
|
186
|
+
locale?: string;
|
|
187
|
+
/** Timezone ID (IANA). Default: "America/New_York" */
|
|
188
|
+
timezone?: string;
|
|
189
|
+
/** Fake geolocation */
|
|
190
|
+
geolocation?: {
|
|
191
|
+
latitude: number;
|
|
192
|
+
longitude: number;
|
|
193
|
+
accuracy?: number;
|
|
194
|
+
};
|
|
195
|
+
/** HTTP/SOCKS proxy. Format: "http://user:pass@host:port" */
|
|
196
|
+
proxy?: {
|
|
197
|
+
server: string;
|
|
198
|
+
username?: string;
|
|
199
|
+
password?: string;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
interface HumanizeConfig {
|
|
203
|
+
/** Per-character typing delay range in ms. Default: [40, 120] */
|
|
204
|
+
typingDelay?: [number, number];
|
|
205
|
+
/** Random pixel offset added to click coordinates. Default: 3 */
|
|
206
|
+
clickJitter?: number;
|
|
207
|
+
/** Extra random pause between actions in ms range. Default: [200, 800] */
|
|
208
|
+
actionDelay?: [number, number];
|
|
209
|
+
/** Simulate human-like mouse movement to target before clicking. Default: true */
|
|
210
|
+
mouseMovement?: boolean;
|
|
211
|
+
}
|
|
212
|
+
interface PageInfo {
|
|
213
|
+
url: string;
|
|
214
|
+
title: string;
|
|
215
|
+
viewportSize: {
|
|
216
|
+
width: number;
|
|
217
|
+
height: number;
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
declare class BrowserAgent {
|
|
222
|
+
readonly name: string;
|
|
223
|
+
readonly eventBus: EventBus;
|
|
224
|
+
private model;
|
|
225
|
+
private instructions?;
|
|
226
|
+
private maxSteps;
|
|
227
|
+
private headless;
|
|
228
|
+
private viewport;
|
|
229
|
+
private defaultStartUrl?;
|
|
230
|
+
private waitAfterAction;
|
|
231
|
+
private maxRepeats;
|
|
232
|
+
private useDOM;
|
|
233
|
+
private storageState?;
|
|
234
|
+
private recordVideo?;
|
|
235
|
+
private credentials?;
|
|
236
|
+
private stealth?;
|
|
237
|
+
private humanize?;
|
|
238
|
+
private memoryManager;
|
|
239
|
+
private logger;
|
|
240
|
+
/** Access the MemoryManager (if memory is configured). */
|
|
241
|
+
get memory(): MemoryManager | null;
|
|
242
|
+
constructor(config: BrowserAgentConfig);
|
|
243
|
+
run(task: string, opts?: BrowserRunOpts): Promise<BrowserRunOutput>;
|
|
244
|
+
/**
|
|
245
|
+
* Returns a ToolDef that lets a regular Agent delegate browser tasks
|
|
246
|
+
* to this BrowserAgent.
|
|
247
|
+
*/
|
|
248
|
+
asTool(config?: {
|
|
249
|
+
name?: string;
|
|
250
|
+
description?: string;
|
|
251
|
+
}): ToolDef;
|
|
252
|
+
private finalize;
|
|
253
|
+
private executeAction;
|
|
254
|
+
private sleep;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Playwright wrapper with stealth anti-detection and human-like behavior.
|
|
259
|
+
*/
|
|
260
|
+
declare class BrowserProvider {
|
|
261
|
+
private browser;
|
|
262
|
+
private context;
|
|
263
|
+
private page;
|
|
264
|
+
private pages;
|
|
265
|
+
private activeTabId;
|
|
266
|
+
private tabCounter;
|
|
267
|
+
private _viewport;
|
|
268
|
+
private _videoDir?;
|
|
269
|
+
private _humanize?;
|
|
270
|
+
constructor();
|
|
271
|
+
launch(opts?: {
|
|
272
|
+
headless?: boolean;
|
|
273
|
+
viewport?: {
|
|
274
|
+
width: number;
|
|
275
|
+
height: number;
|
|
276
|
+
};
|
|
277
|
+
storageState?: string;
|
|
278
|
+
recordVideo?: boolean | {
|
|
279
|
+
dir: string;
|
|
280
|
+
};
|
|
281
|
+
stealth?: boolean | StealthConfig;
|
|
282
|
+
humanize?: boolean | HumanizeConfig;
|
|
283
|
+
}): Promise<void>;
|
|
284
|
+
saveStorageState(path: string): Promise<void>;
|
|
285
|
+
navigate(url: string): Promise<void>;
|
|
286
|
+
back(): Promise<void>;
|
|
287
|
+
screenshot(): Promise<Buffer>;
|
|
288
|
+
click(x: number, y: number): Promise<void>;
|
|
289
|
+
type(text: string): Promise<void>;
|
|
290
|
+
clickAndType(x: number, y: number, text: string): Promise<void>;
|
|
291
|
+
pressKey(key: string): Promise<void>;
|
|
292
|
+
scroll(direction: "up" | "down", amount?: number): Promise<void>;
|
|
293
|
+
extractDOM(opts?: {
|
|
294
|
+
maxElements?: number;
|
|
295
|
+
}): Promise<string>;
|
|
296
|
+
getPageInfo(): Promise<PageInfo>;
|
|
297
|
+
waitForStable(minWait?: number): Promise<void>;
|
|
298
|
+
newTab(url?: string): Promise<string>;
|
|
299
|
+
switchTab(tabId: string): Promise<void>;
|
|
300
|
+
closeTab(tabId: string): Promise<void>;
|
|
301
|
+
listTabs(): {
|
|
302
|
+
id: string;
|
|
303
|
+
url: string;
|
|
304
|
+
active: boolean;
|
|
305
|
+
}[];
|
|
306
|
+
get currentTabId(): string;
|
|
307
|
+
getVideoPath(tabId?: string): Promise<string | null>;
|
|
308
|
+
get videoDir(): string | undefined;
|
|
309
|
+
close(): Promise<void>;
|
|
310
|
+
/** Add small random offset to coordinates to avoid pixel-perfect bot patterns. */
|
|
311
|
+
private jitter;
|
|
312
|
+
/**
|
|
313
|
+
* Simulate human mouse movement using Bézier-like interpolation.
|
|
314
|
+
* Moves from the current mouse position to the target in small steps.
|
|
315
|
+
*/
|
|
316
|
+
private humanMouseMove;
|
|
317
|
+
/** Small random pause after an interaction. */
|
|
318
|
+
private humanPause;
|
|
319
|
+
private randInt;
|
|
320
|
+
private ensurePage;
|
|
321
|
+
private ensureContext;
|
|
322
|
+
private sleep;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export { type BrowserAction, BrowserAgent, type BrowserAgentConfig, BrowserProvider, type BrowserRunOpts, type BrowserRunOutput, type BrowserStep, CredentialVault, type HumanizeConfig, type PageInfo, type StealthConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radaros/browser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "Browser automation agent for RadarOS using Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,20 +17,23 @@
|
|
|
17
17
|
"scraping"
|
|
18
18
|
],
|
|
19
19
|
"type": "module",
|
|
20
|
-
"main": "./dist/index.
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
21
22
|
"types": "./dist/index.d.ts",
|
|
22
23
|
"exports": {
|
|
23
24
|
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
24
26
|
"import": "./dist/index.js",
|
|
25
|
-
"
|
|
27
|
+
"require": "./dist/index.cjs",
|
|
28
|
+
"default": "./dist/index.js"
|
|
26
29
|
}
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
32
|
"dist"
|
|
30
33
|
],
|
|
31
34
|
"scripts": {
|
|
32
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
33
|
-
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
35
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
36
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
34
37
|
"prepublishOnly": "npm run build"
|
|
35
38
|
},
|
|
36
39
|
"dependencies": {
|
|
@@ -43,7 +46,7 @@
|
|
|
43
46
|
"typescript": "^5.6.0"
|
|
44
47
|
},
|
|
45
48
|
"peerDependencies": {
|
|
46
|
-
"@radaros/core": "^0.3.
|
|
49
|
+
"@radaros/core": "^0.3.22",
|
|
47
50
|
"playwright": ">=1.40.0"
|
|
48
51
|
},
|
|
49
52
|
"peerDependenciesMeta": {
|