@agentium/browser 2.0.6 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +108 -9
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +108 -9
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -150,7 +150,11 @@ function buildStealthContextOpts(config, viewport) {
|
|
|
150
150
|
locale: config.locale ?? "en-US",
|
|
151
151
|
timezoneId: config.timezone ?? "America/New_York",
|
|
152
152
|
colorScheme: "light",
|
|
153
|
-
|
|
153
|
+
// Default to 1 — matches the most common real-user setup and avoids
|
|
154
|
+
// visual zoom/stretch in headed mode on non-Retina displays (the host
|
|
155
|
+
// OS would downsample a 2× rendered surface). Users on Retina-only
|
|
156
|
+
// deployments can opt back into DPR=2 for sharper screenshots.
|
|
157
|
+
deviceScaleFactor: config.deviceScaleFactor ?? 1,
|
|
154
158
|
hasTouch: false,
|
|
155
159
|
javaScriptEnabled: true,
|
|
156
160
|
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false
|
|
@@ -206,14 +210,10 @@ var BrowserProvider = class {
|
|
|
206
210
|
const launchOpts = {
|
|
207
211
|
headless: opts?.headless ?? true
|
|
208
212
|
};
|
|
209
|
-
const windowSizeArg = `--window-size=${this._viewport.width},${this._viewport.height}`;
|
|
210
|
-
const windowPositionArg = "--window-position=0,0";
|
|
211
213
|
if (stealthEnabled) {
|
|
212
214
|
const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
|
|
213
|
-
launchOpts.args =
|
|
215
|
+
launchOpts.args = args;
|
|
214
216
|
if (proxy) launchOpts.proxy = proxy;
|
|
215
|
-
} else {
|
|
216
|
-
launchOpts.args = [windowSizeArg, windowPositionArg];
|
|
217
217
|
}
|
|
218
218
|
this.browser = await chromium.launch(launchOpts);
|
|
219
219
|
let contextOpts;
|
|
@@ -346,7 +346,24 @@ var BrowserProvider = class {
|
|
|
346
346
|
"[contenteditable='true']",
|
|
347
347
|
"[tabindex]:not([tabindex='-1'])"
|
|
348
348
|
];
|
|
349
|
-
const
|
|
349
|
+
const semantic = Array.from(doc.querySelectorAll(selectors.join(",")));
|
|
350
|
+
const POINTER_SCAN_CAP = 2e3;
|
|
351
|
+
const pointerCandidates = [];
|
|
352
|
+
const allEls = doc.querySelectorAll("*");
|
|
353
|
+
for (let i = 0; i < allEls.length && pointerCandidates.length < POINTER_SCAN_CAP; i++) {
|
|
354
|
+
const el = allEls[i];
|
|
355
|
+
if (!el) continue;
|
|
356
|
+
const tag = el.tagName.toLowerCase();
|
|
357
|
+
if (tag === "html" || tag === "body" || tag === "head" || tag === "script" || tag === "style") continue;
|
|
358
|
+
const rect = el.getBoundingClientRect();
|
|
359
|
+
if (rect.width < 4 || rect.height < 4) continue;
|
|
360
|
+
if (rect.bottom < 0 || rect.top > win.innerHeight) continue;
|
|
361
|
+
if (rect.right < 0 || rect.left > win.innerWidth) continue;
|
|
362
|
+
const style = win.getComputedStyle(el);
|
|
363
|
+
if (style.cursor !== "pointer") continue;
|
|
364
|
+
pointerCandidates.push(el);
|
|
365
|
+
}
|
|
366
|
+
const all = semantic.concat(pointerCandidates);
|
|
350
367
|
const vh = win.innerHeight;
|
|
351
368
|
const vw = win.innerWidth;
|
|
352
369
|
const entries = [];
|
|
@@ -368,6 +385,7 @@ var BrowserProvider = class {
|
|
|
368
385
|
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
369
386
|
continue;
|
|
370
387
|
}
|
|
388
|
+
if (hit && el.contains(hit)) seen.add(hit);
|
|
371
389
|
} catch {
|
|
372
390
|
}
|
|
373
391
|
seen.add(el);
|
|
@@ -396,6 +414,29 @@ var BrowserProvider = class {
|
|
|
396
414
|
}, max);
|
|
397
415
|
return elements;
|
|
398
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Deterministic, DOM-based click using Playwright's text locator.
|
|
419
|
+
*
|
|
420
|
+
* Returns `true` if a matching, visible, clickable element was found and
|
|
421
|
+
* clicked within `timeout` ms; `false` otherwise (so the caller can fall
|
|
422
|
+
* back to coordinate clicking). Substring-matches by default — e.g.
|
|
423
|
+
* `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
|
|
424
|
+
*
|
|
425
|
+
* This is the same pattern browser-use uses in DOM-only mode and is far
|
|
426
|
+
* more reliable than coordinate clicks for text-bearing targets.
|
|
427
|
+
*/
|
|
428
|
+
async clickByText(keyword, opts) {
|
|
429
|
+
this.ensurePage();
|
|
430
|
+
const timeout = opts?.timeout ?? 3e3;
|
|
431
|
+
try {
|
|
432
|
+
const locator = this.page.locator(`text=${keyword}`).first();
|
|
433
|
+
await locator.click({ timeout });
|
|
434
|
+
await this.humanPause();
|
|
435
|
+
return true;
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
399
440
|
// ── Page Info ────────────────────────────────────────────────────────
|
|
400
441
|
async getPageInfo() {
|
|
401
442
|
this.ensurePage();
|
|
@@ -568,6 +609,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
|
|
|
568
609
|
`### click`,
|
|
569
610
|
`Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
|
|
570
611
|
`\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
|
|
612
|
+
`IMPORTANT: when the target has visible text, ALWAYS put that text in single or double quotes inside the description, e.g. \`"description": "Click on 'Cheapest' tab"\` or \`"description": "Press the \\"Sign in\\" button"\`. The runtime uses the quoted phrase to do a deterministic DOM text-locator click and only falls back to (x,y) if no match is found, which dramatically improves accuracy on dynamic pages.`,
|
|
571
613
|
``,
|
|
572
614
|
`### type`,
|
|
573
615
|
`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.`,
|
|
@@ -972,9 +1014,20 @@ ${memoryContext}` : memoryContext;
|
|
|
972
1014
|
async executeAction(browser, action) {
|
|
973
1015
|
try {
|
|
974
1016
|
switch (action.action) {
|
|
975
|
-
case "click":
|
|
976
|
-
|
|
1017
|
+
case "click": {
|
|
1018
|
+
const keyword = this.extractClickKeyword(action.description);
|
|
1019
|
+
let clicked = false;
|
|
1020
|
+
if (keyword) {
|
|
1021
|
+
clicked = await browser.clickByText(keyword);
|
|
1022
|
+
if (clicked) {
|
|
1023
|
+
this.logger.debug("Clicked by text", { keyword });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (!clicked) {
|
|
1027
|
+
await browser.click(action.x, action.y);
|
|
1028
|
+
}
|
|
977
1029
|
break;
|
|
1030
|
+
}
|
|
978
1031
|
case "type": {
|
|
979
1032
|
const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
|
|
980
1033
|
if (action.x != null && action.y != null) {
|
|
@@ -1011,6 +1064,52 @@ ${memoryContext}` : memoryContext;
|
|
|
1011
1064
|
sleep(ms) {
|
|
1012
1065
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1013
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Parse a quoted target keyword from a click action's `description`.
|
|
1069
|
+
*
|
|
1070
|
+
* The system prompt asks the model to include the visible label of its
|
|
1071
|
+
* click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
|
|
1072
|
+
* quoted phrase is present we return it so `executeAction` can try a
|
|
1073
|
+
* deterministic Playwright text locator before falling back to
|
|
1074
|
+
* coordinate clicking.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns `undefined` for generic / ambiguous labels (login buttons,
|
|
1077
|
+
* close, OK, etc.) where a substring text match could trivially fire on
|
|
1078
|
+
* the wrong element.
|
|
1079
|
+
*/
|
|
1080
|
+
extractClickKeyword(description) {
|
|
1081
|
+
if (!description) return void 0;
|
|
1082
|
+
const match = description.match(/['"\u2018\u2019\u201C\u201D]([^'"\u2018\u2019\u201C\u201D]{1,80})['"\u2018\u2019\u201C\u201D]/);
|
|
1083
|
+
if (!match) return void 0;
|
|
1084
|
+
const keyword = match[1].trim();
|
|
1085
|
+
if (!keyword || keyword.length < 2) return void 0;
|
|
1086
|
+
const skip = /* @__PURE__ */ new Set([
|
|
1087
|
+
"log in",
|
|
1088
|
+
"login",
|
|
1089
|
+
"sign in",
|
|
1090
|
+
"sign up",
|
|
1091
|
+
"submit",
|
|
1092
|
+
"close",
|
|
1093
|
+
"ok",
|
|
1094
|
+
"okay",
|
|
1095
|
+
"cancel",
|
|
1096
|
+
"yes",
|
|
1097
|
+
"no",
|
|
1098
|
+
"x",
|
|
1099
|
+
"continue",
|
|
1100
|
+
"next",
|
|
1101
|
+
"back",
|
|
1102
|
+
"accept",
|
|
1103
|
+
"dismiss",
|
|
1104
|
+
"got it",
|
|
1105
|
+
"agree",
|
|
1106
|
+
"i agree",
|
|
1107
|
+
"allow",
|
|
1108
|
+
"deny"
|
|
1109
|
+
]);
|
|
1110
|
+
if (skip.has(keyword.toLowerCase())) return void 0;
|
|
1111
|
+
return keyword;
|
|
1112
|
+
}
|
|
1014
1113
|
};
|
|
1015
1114
|
|
|
1016
1115
|
// src/credential-vault.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -198,6 +198,13 @@ interface StealthConfig {
|
|
|
198
198
|
};
|
|
199
199
|
/** Ignore HTTPS certificate errors. Default: false (secure). Only enable for local testing. */
|
|
200
200
|
ignoreHTTPSErrors?: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* `window.devicePixelRatio` to emulate. Default: `1`. Set to `2` to mimic
|
|
203
|
+
* a Retina display (sharper screenshots at 2× cost). Setting to 2 on a
|
|
204
|
+
* non-Retina host display can cause the headed window to look zoomed-out
|
|
205
|
+
* or stretched because the OS compositor downsamples a 2× surface.
|
|
206
|
+
*/
|
|
207
|
+
deviceScaleFactor?: number;
|
|
201
208
|
/** HTTP/SOCKS proxy. Format: "http://user:pass@host:port" */
|
|
202
209
|
proxy?: {
|
|
203
210
|
server: string;
|
|
@@ -259,6 +266,20 @@ declare class BrowserAgent {
|
|
|
259
266
|
private finalize;
|
|
260
267
|
private executeAction;
|
|
261
268
|
private sleep;
|
|
269
|
+
/**
|
|
270
|
+
* Parse a quoted target keyword from a click action's `description`.
|
|
271
|
+
*
|
|
272
|
+
* The system prompt asks the model to include the visible label of its
|
|
273
|
+
* click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
|
|
274
|
+
* quoted phrase is present we return it so `executeAction` can try a
|
|
275
|
+
* deterministic Playwright text locator before falling back to
|
|
276
|
+
* coordinate clicking.
|
|
277
|
+
*
|
|
278
|
+
* Returns `undefined` for generic / ambiguous labels (login buttons,
|
|
279
|
+
* close, OK, etc.) where a substring text match could trivially fire on
|
|
280
|
+
* the wrong element.
|
|
281
|
+
*/
|
|
282
|
+
private extractClickKeyword;
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
/**
|
|
@@ -305,6 +326,20 @@ declare class BrowserProvider {
|
|
|
305
326
|
extractDOM(opts?: {
|
|
306
327
|
maxElements?: number;
|
|
307
328
|
}): Promise<string>;
|
|
329
|
+
/**
|
|
330
|
+
* Deterministic, DOM-based click using Playwright's text locator.
|
|
331
|
+
*
|
|
332
|
+
* Returns `true` if a matching, visible, clickable element was found and
|
|
333
|
+
* clicked within `timeout` ms; `false` otherwise (so the caller can fall
|
|
334
|
+
* back to coordinate clicking). Substring-matches by default — e.g.
|
|
335
|
+
* `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
|
|
336
|
+
*
|
|
337
|
+
* This is the same pattern browser-use uses in DOM-only mode and is far
|
|
338
|
+
* more reliable than coordinate clicks for text-bearing targets.
|
|
339
|
+
*/
|
|
340
|
+
clickByText(keyword: string, opts?: {
|
|
341
|
+
timeout?: number;
|
|
342
|
+
}): Promise<boolean>;
|
|
308
343
|
getPageInfo(): Promise<PageInfo>;
|
|
309
344
|
waitForStable(minWait?: number): Promise<void>;
|
|
310
345
|
newTab(url?: string): Promise<string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -198,6 +198,13 @@ interface StealthConfig {
|
|
|
198
198
|
};
|
|
199
199
|
/** Ignore HTTPS certificate errors. Default: false (secure). Only enable for local testing. */
|
|
200
200
|
ignoreHTTPSErrors?: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* `window.devicePixelRatio` to emulate. Default: `1`. Set to `2` to mimic
|
|
203
|
+
* a Retina display (sharper screenshots at 2× cost). Setting to 2 on a
|
|
204
|
+
* non-Retina host display can cause the headed window to look zoomed-out
|
|
205
|
+
* or stretched because the OS compositor downsamples a 2× surface.
|
|
206
|
+
*/
|
|
207
|
+
deviceScaleFactor?: number;
|
|
201
208
|
/** HTTP/SOCKS proxy. Format: "http://user:pass@host:port" */
|
|
202
209
|
proxy?: {
|
|
203
210
|
server: string;
|
|
@@ -259,6 +266,20 @@ declare class BrowserAgent {
|
|
|
259
266
|
private finalize;
|
|
260
267
|
private executeAction;
|
|
261
268
|
private sleep;
|
|
269
|
+
/**
|
|
270
|
+
* Parse a quoted target keyword from a click action's `description`.
|
|
271
|
+
*
|
|
272
|
+
* The system prompt asks the model to include the visible label of its
|
|
273
|
+
* click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
|
|
274
|
+
* quoted phrase is present we return it so `executeAction` can try a
|
|
275
|
+
* deterministic Playwright text locator before falling back to
|
|
276
|
+
* coordinate clicking.
|
|
277
|
+
*
|
|
278
|
+
* Returns `undefined` for generic / ambiguous labels (login buttons,
|
|
279
|
+
* close, OK, etc.) where a substring text match could trivially fire on
|
|
280
|
+
* the wrong element.
|
|
281
|
+
*/
|
|
282
|
+
private extractClickKeyword;
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
/**
|
|
@@ -305,6 +326,20 @@ declare class BrowserProvider {
|
|
|
305
326
|
extractDOM(opts?: {
|
|
306
327
|
maxElements?: number;
|
|
307
328
|
}): Promise<string>;
|
|
329
|
+
/**
|
|
330
|
+
* Deterministic, DOM-based click using Playwright's text locator.
|
|
331
|
+
*
|
|
332
|
+
* Returns `true` if a matching, visible, clickable element was found and
|
|
333
|
+
* clicked within `timeout` ms; `false` otherwise (so the caller can fall
|
|
334
|
+
* back to coordinate clicking). Substring-matches by default — e.g.
|
|
335
|
+
* `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
|
|
336
|
+
*
|
|
337
|
+
* This is the same pattern browser-use uses in DOM-only mode and is far
|
|
338
|
+
* more reliable than coordinate clicks for text-bearing targets.
|
|
339
|
+
*/
|
|
340
|
+
clickByText(keyword: string, opts?: {
|
|
341
|
+
timeout?: number;
|
|
342
|
+
}): Promise<boolean>;
|
|
308
343
|
getPageInfo(): Promise<PageInfo>;
|
|
309
344
|
waitForStable(minWait?: number): Promise<void>;
|
|
310
345
|
newTab(url?: string): Promise<string>;
|
package/dist/index.js
CHANGED
|
@@ -112,7 +112,11 @@ function buildStealthContextOpts(config, viewport) {
|
|
|
112
112
|
locale: config.locale ?? "en-US",
|
|
113
113
|
timezoneId: config.timezone ?? "America/New_York",
|
|
114
114
|
colorScheme: "light",
|
|
115
|
-
|
|
115
|
+
// Default to 1 — matches the most common real-user setup and avoids
|
|
116
|
+
// visual zoom/stretch in headed mode on non-Retina displays (the host
|
|
117
|
+
// OS would downsample a 2× rendered surface). Users on Retina-only
|
|
118
|
+
// deployments can opt back into DPR=2 for sharper screenshots.
|
|
119
|
+
deviceScaleFactor: config.deviceScaleFactor ?? 1,
|
|
116
120
|
hasTouch: false,
|
|
117
121
|
javaScriptEnabled: true,
|
|
118
122
|
ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false
|
|
@@ -168,14 +172,10 @@ var BrowserProvider = class {
|
|
|
168
172
|
const launchOpts = {
|
|
169
173
|
headless: opts?.headless ?? true
|
|
170
174
|
};
|
|
171
|
-
const windowSizeArg = `--window-size=${this._viewport.width},${this._viewport.height}`;
|
|
172
|
-
const windowPositionArg = "--window-position=0,0";
|
|
173
175
|
if (stealthEnabled) {
|
|
174
176
|
const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
|
|
175
|
-
launchOpts.args =
|
|
177
|
+
launchOpts.args = args;
|
|
176
178
|
if (proxy) launchOpts.proxy = proxy;
|
|
177
|
-
} else {
|
|
178
|
-
launchOpts.args = [windowSizeArg, windowPositionArg];
|
|
179
179
|
}
|
|
180
180
|
this.browser = await chromium.launch(launchOpts);
|
|
181
181
|
let contextOpts;
|
|
@@ -308,7 +308,24 @@ var BrowserProvider = class {
|
|
|
308
308
|
"[contenteditable='true']",
|
|
309
309
|
"[tabindex]:not([tabindex='-1'])"
|
|
310
310
|
];
|
|
311
|
-
const
|
|
311
|
+
const semantic = Array.from(doc.querySelectorAll(selectors.join(",")));
|
|
312
|
+
const POINTER_SCAN_CAP = 2e3;
|
|
313
|
+
const pointerCandidates = [];
|
|
314
|
+
const allEls = doc.querySelectorAll("*");
|
|
315
|
+
for (let i = 0; i < allEls.length && pointerCandidates.length < POINTER_SCAN_CAP; i++) {
|
|
316
|
+
const el = allEls[i];
|
|
317
|
+
if (!el) continue;
|
|
318
|
+
const tag = el.tagName.toLowerCase();
|
|
319
|
+
if (tag === "html" || tag === "body" || tag === "head" || tag === "script" || tag === "style") continue;
|
|
320
|
+
const rect = el.getBoundingClientRect();
|
|
321
|
+
if (rect.width < 4 || rect.height < 4) continue;
|
|
322
|
+
if (rect.bottom < 0 || rect.top > win.innerHeight) continue;
|
|
323
|
+
if (rect.right < 0 || rect.left > win.innerWidth) continue;
|
|
324
|
+
const style = win.getComputedStyle(el);
|
|
325
|
+
if (style.cursor !== "pointer") continue;
|
|
326
|
+
pointerCandidates.push(el);
|
|
327
|
+
}
|
|
328
|
+
const all = semantic.concat(pointerCandidates);
|
|
312
329
|
const vh = win.innerHeight;
|
|
313
330
|
const vw = win.innerWidth;
|
|
314
331
|
const entries = [];
|
|
@@ -330,6 +347,7 @@ var BrowserProvider = class {
|
|
|
330
347
|
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
331
348
|
continue;
|
|
332
349
|
}
|
|
350
|
+
if (hit && el.contains(hit)) seen.add(hit);
|
|
333
351
|
} catch {
|
|
334
352
|
}
|
|
335
353
|
seen.add(el);
|
|
@@ -358,6 +376,29 @@ var BrowserProvider = class {
|
|
|
358
376
|
}, max);
|
|
359
377
|
return elements;
|
|
360
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Deterministic, DOM-based click using Playwright's text locator.
|
|
381
|
+
*
|
|
382
|
+
* Returns `true` if a matching, visible, clickable element was found and
|
|
383
|
+
* clicked within `timeout` ms; `false` otherwise (so the caller can fall
|
|
384
|
+
* back to coordinate clicking). Substring-matches by default — e.g.
|
|
385
|
+
* `clickByText("Cheapest")` matches "Cheapest · 23-28 days · $2,550".
|
|
386
|
+
*
|
|
387
|
+
* This is the same pattern browser-use uses in DOM-only mode and is far
|
|
388
|
+
* more reliable than coordinate clicks for text-bearing targets.
|
|
389
|
+
*/
|
|
390
|
+
async clickByText(keyword, opts) {
|
|
391
|
+
this.ensurePage();
|
|
392
|
+
const timeout = opts?.timeout ?? 3e3;
|
|
393
|
+
try {
|
|
394
|
+
const locator = this.page.locator(`text=${keyword}`).first();
|
|
395
|
+
await locator.click({ timeout });
|
|
396
|
+
await this.humanPause();
|
|
397
|
+
return true;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
361
402
|
// ── Page Info ────────────────────────────────────────────────────────
|
|
362
403
|
async getPageInfo() {
|
|
363
404
|
this.ensurePage();
|
|
@@ -530,6 +571,7 @@ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
|
|
|
530
571
|
`### click`,
|
|
531
572
|
`Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
|
|
532
573
|
`\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
|
|
574
|
+
`IMPORTANT: when the target has visible text, ALWAYS put that text in single or double quotes inside the description, e.g. \`"description": "Click on 'Cheapest' tab"\` or \`"description": "Press the \\"Sign in\\" button"\`. The runtime uses the quoted phrase to do a deterministic DOM text-locator click and only falls back to (x,y) if no match is found, which dramatically improves accuracy on dynamic pages.`,
|
|
533
575
|
``,
|
|
534
576
|
`### type`,
|
|
535
577
|
`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.`,
|
|
@@ -934,9 +976,20 @@ ${memoryContext}` : memoryContext;
|
|
|
934
976
|
async executeAction(browser, action) {
|
|
935
977
|
try {
|
|
936
978
|
switch (action.action) {
|
|
937
|
-
case "click":
|
|
938
|
-
|
|
979
|
+
case "click": {
|
|
980
|
+
const keyword = this.extractClickKeyword(action.description);
|
|
981
|
+
let clicked = false;
|
|
982
|
+
if (keyword) {
|
|
983
|
+
clicked = await browser.clickByText(keyword);
|
|
984
|
+
if (clicked) {
|
|
985
|
+
this.logger.debug("Clicked by text", { keyword });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (!clicked) {
|
|
989
|
+
await browser.click(action.x, action.y);
|
|
990
|
+
}
|
|
939
991
|
break;
|
|
992
|
+
}
|
|
940
993
|
case "type": {
|
|
941
994
|
const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
|
|
942
995
|
if (action.x != null && action.y != null) {
|
|
@@ -973,6 +1026,52 @@ ${memoryContext}` : memoryContext;
|
|
|
973
1026
|
sleep(ms) {
|
|
974
1027
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
975
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Parse a quoted target keyword from a click action's `description`.
|
|
1031
|
+
*
|
|
1032
|
+
* The system prompt asks the model to include the visible label of its
|
|
1033
|
+
* click target in quotes (e.g. `"Click on 'Cheapest' tab"`). When a usable
|
|
1034
|
+
* quoted phrase is present we return it so `executeAction` can try a
|
|
1035
|
+
* deterministic Playwright text locator before falling back to
|
|
1036
|
+
* coordinate clicking.
|
|
1037
|
+
*
|
|
1038
|
+
* Returns `undefined` for generic / ambiguous labels (login buttons,
|
|
1039
|
+
* close, OK, etc.) where a substring text match could trivially fire on
|
|
1040
|
+
* the wrong element.
|
|
1041
|
+
*/
|
|
1042
|
+
extractClickKeyword(description) {
|
|
1043
|
+
if (!description) return void 0;
|
|
1044
|
+
const match = description.match(/['"\u2018\u2019\u201C\u201D]([^'"\u2018\u2019\u201C\u201D]{1,80})['"\u2018\u2019\u201C\u201D]/);
|
|
1045
|
+
if (!match) return void 0;
|
|
1046
|
+
const keyword = match[1].trim();
|
|
1047
|
+
if (!keyword || keyword.length < 2) return void 0;
|
|
1048
|
+
const skip = /* @__PURE__ */ new Set([
|
|
1049
|
+
"log in",
|
|
1050
|
+
"login",
|
|
1051
|
+
"sign in",
|
|
1052
|
+
"sign up",
|
|
1053
|
+
"submit",
|
|
1054
|
+
"close",
|
|
1055
|
+
"ok",
|
|
1056
|
+
"okay",
|
|
1057
|
+
"cancel",
|
|
1058
|
+
"yes",
|
|
1059
|
+
"no",
|
|
1060
|
+
"x",
|
|
1061
|
+
"continue",
|
|
1062
|
+
"next",
|
|
1063
|
+
"back",
|
|
1064
|
+
"accept",
|
|
1065
|
+
"dismiss",
|
|
1066
|
+
"got it",
|
|
1067
|
+
"agree",
|
|
1068
|
+
"i agree",
|
|
1069
|
+
"allow",
|
|
1070
|
+
"deny"
|
|
1071
|
+
]);
|
|
1072
|
+
if (skip.has(keyword.toLowerCase())) return void 0;
|
|
1073
|
+
return keyword;
|
|
1074
|
+
}
|
|
976
1075
|
};
|
|
977
1076
|
|
|
978
1077
|
// src/credential-vault.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentium/browser",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Browser automation agent for Agentium using Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.6.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@agentium/core": "^2.0.
|
|
49
|
+
"@agentium/core": "^2.0.8",
|
|
50
50
|
"playwright": ">=1.40.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|