agentic-browser 2.0.1 → 2.1.0
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.
|
@@ -8,7 +8,6 @@ import WebSocket, { WebSocketServer } from "ws";
|
|
|
8
8
|
import { URL as URL$1, fileURLToPath } from "node:url";
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
|
|
12
11
|
//#region src/session/chrome-launcher.ts
|
|
13
12
|
const CANDIDATES = [
|
|
14
13
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -22,7 +21,6 @@ function discoverChrome(explicitPath) {
|
|
|
22
21
|
if (!found) throw new Error("No supported Chrome installation found.");
|
|
23
22
|
return found;
|
|
24
23
|
}
|
|
25
|
-
|
|
26
24
|
//#endregion
|
|
27
25
|
//#region src/session/extension-loader.ts
|
|
28
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -33,22 +31,93 @@ function loadControlExtension() {
|
|
|
33
31
|
loadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
|
-
|
|
37
34
|
//#endregion
|
|
38
35
|
//#region src/session/browser-controller.ts
|
|
39
36
|
var CdpConnection = class CdpConnection {
|
|
40
37
|
nextId = 0;
|
|
38
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
39
|
+
pendingEvents = /* @__PURE__ */ new Map();
|
|
40
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
41
|
+
closed = false;
|
|
41
42
|
constructor(ws) {
|
|
42
43
|
this.ws = ws;
|
|
44
|
+
this.ensureListening();
|
|
43
45
|
}
|
|
44
46
|
static async connect(targetWsUrl) {
|
|
45
47
|
const ws = new WebSocket(targetWsUrl);
|
|
46
48
|
await new Promise((resolve, reject) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const onOpen = () => {
|
|
50
|
+
ws.off("error", onError);
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
const onError = (err) => {
|
|
54
|
+
ws.off("open", onOpen);
|
|
55
|
+
reject(err);
|
|
56
|
+
};
|
|
57
|
+
ws.once("open", onOpen);
|
|
58
|
+
ws.once("error", onError);
|
|
49
59
|
});
|
|
50
60
|
return new CdpConnection(ws);
|
|
51
61
|
}
|
|
62
|
+
ensureListening() {
|
|
63
|
+
this.ws.on("message", this.handleMessage);
|
|
64
|
+
this.ws.once("close", this.handleClose);
|
|
65
|
+
this.ws.on("error", this.handleError);
|
|
66
|
+
}
|
|
67
|
+
handleMessage = (raw) => {
|
|
68
|
+
let message;
|
|
69
|
+
try {
|
|
70
|
+
message = JSON.parse(raw.toString("utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (typeof message.id === "number") {
|
|
75
|
+
const pending = this.pendingRequests.get(message.id);
|
|
76
|
+
if (!pending) return;
|
|
77
|
+
this.pendingRequests.delete(message.id);
|
|
78
|
+
clearTimeout(pending.timeout);
|
|
79
|
+
if (message.error) {
|
|
80
|
+
pending.reject(new Error(message.error.message));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
pending.resolve(message.result ?? {});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!message.method) return;
|
|
87
|
+
const params = message.params ?? {};
|
|
88
|
+
const handlers = this.eventHandlers.get(message.method);
|
|
89
|
+
if (handlers) for (const handler of handlers) handler(params);
|
|
90
|
+
const waiters = this.pendingEvents.get(message.method);
|
|
91
|
+
if (!waiters?.length) return;
|
|
92
|
+
this.pendingEvents.delete(message.method);
|
|
93
|
+
for (const waiter of waiters) {
|
|
94
|
+
clearTimeout(waiter.timeout);
|
|
95
|
+
waiter.resolve(params);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
handleError = (err) => {
|
|
99
|
+
this.failAllPending(err);
|
|
100
|
+
};
|
|
101
|
+
handleClose = () => {
|
|
102
|
+
this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
|
|
103
|
+
};
|
|
104
|
+
failAllPending(error) {
|
|
105
|
+
if (this.closed) return;
|
|
106
|
+
this.closed = true;
|
|
107
|
+
this.ws.off("message", this.handleMessage);
|
|
108
|
+
this.ws.off("error", this.handleError);
|
|
109
|
+
for (const pending of this.pendingRequests.values()) {
|
|
110
|
+
clearTimeout(pending.timeout);
|
|
111
|
+
pending.reject(error);
|
|
112
|
+
}
|
|
113
|
+
this.pendingRequests.clear();
|
|
114
|
+
for (const waiters of this.pendingEvents.values()) for (const waiter of waiters) {
|
|
115
|
+
clearTimeout(waiter.timeout);
|
|
116
|
+
waiter.reject(error);
|
|
117
|
+
}
|
|
118
|
+
this.pendingEvents.clear();
|
|
119
|
+
this.eventHandlers.clear();
|
|
120
|
+
}
|
|
52
121
|
async send(method, params, timeoutMs = 15e3) {
|
|
53
122
|
if (this.ws.readyState !== WebSocket.OPEN) throw new Error(`WebSocket not open (state=${this.ws.readyState}), cannot send '${method}'`);
|
|
54
123
|
const id = ++this.nextId;
|
|
@@ -59,51 +128,55 @@ var CdpConnection = class CdpConnection {
|
|
|
59
128
|
};
|
|
60
129
|
return await new Promise((resolve, reject) => {
|
|
61
130
|
const timeout = setTimeout(() => {
|
|
62
|
-
this.
|
|
131
|
+
this.pendingRequests.delete(id);
|
|
63
132
|
reject(/* @__PURE__ */ new Error(`CDP call '${method}' timed out after ${timeoutMs}ms`));
|
|
64
133
|
}, timeoutMs);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (message.error) {
|
|
71
|
-
reject(new Error(message.error.message));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
resolve(message.result ?? {});
|
|
75
|
-
};
|
|
76
|
-
this.ws.on("message", onMessage);
|
|
134
|
+
this.pendingRequests.set(id, {
|
|
135
|
+
resolve: (value) => resolve(value),
|
|
136
|
+
reject,
|
|
137
|
+
timeout
|
|
138
|
+
});
|
|
77
139
|
this.ws.send(JSON.stringify(payload));
|
|
78
140
|
});
|
|
79
141
|
}
|
|
80
142
|
waitForEvent(method, timeoutMs = 5e3) {
|
|
81
143
|
return new Promise((resolve, reject) => {
|
|
82
144
|
const timeout = setTimeout(() => {
|
|
83
|
-
this.
|
|
145
|
+
const waiters = this.pendingEvents.get(method);
|
|
146
|
+
if (!waiters) {
|
|
147
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const next = waiters.filter((waiter) => waiter.timeout !== timeout);
|
|
151
|
+
if (next.length > 0) this.pendingEvents.set(method, next);
|
|
152
|
+
else this.pendingEvents.delete(method);
|
|
84
153
|
reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
|
|
85
154
|
}, timeoutMs);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.ws.on("message", onMessage);
|
|
155
|
+
const waiters = this.pendingEvents.get(method) ?? [];
|
|
156
|
+
waiters.push({
|
|
157
|
+
resolve: (value) => resolve(value),
|
|
158
|
+
reject,
|
|
159
|
+
timeout
|
|
160
|
+
});
|
|
161
|
+
this.pendingEvents.set(method, waiters);
|
|
94
162
|
});
|
|
95
163
|
}
|
|
96
164
|
onEvent(method, handler) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
165
|
+
const handlers = this.eventHandlers.get(method) ?? /* @__PURE__ */ new Set();
|
|
166
|
+
const wrapped = (params) => {
|
|
167
|
+
handler(params);
|
|
100
168
|
};
|
|
101
|
-
|
|
169
|
+
handlers.add(wrapped);
|
|
170
|
+
this.eventHandlers.set(method, handlers);
|
|
102
171
|
return () => {
|
|
103
|
-
this.
|
|
172
|
+
const current = this.eventHandlers.get(method);
|
|
173
|
+
if (!current) return;
|
|
174
|
+
current.delete(wrapped);
|
|
175
|
+
if (current.size === 0) this.eventHandlers.delete(method);
|
|
104
176
|
};
|
|
105
177
|
}
|
|
106
178
|
close() {
|
|
179
|
+
this.failAllPending(/* @__PURE__ */ new Error("CDP connection closed"));
|
|
107
180
|
this.ws.close();
|
|
108
181
|
}
|
|
109
182
|
};
|
|
@@ -201,6 +274,214 @@ function resolveDefaultProfileDir() {
|
|
|
201
274
|
if (platform === "win32") return path.join(process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "Google", "Chrome", "User Data");
|
|
202
275
|
return path.join(home, ".config", "google-chrome");
|
|
203
276
|
}
|
|
277
|
+
const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
278
|
+
const LOCATOR_SEPARATOR = ' >>> ';
|
|
279
|
+
|
|
280
|
+
function splitLocator(locator) {
|
|
281
|
+
return String(locator ?? '')
|
|
282
|
+
.split(/\s*>>>\s*/g)
|
|
283
|
+
.map((part) => part.trim())
|
|
284
|
+
.filter(Boolean);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getFrameElementForDocument(doc) {
|
|
288
|
+
try {
|
|
289
|
+
return doc && doc.defaultView && doc.defaultView.frameElement ? doc.defaultView.frameElement : null;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createComposedChain(node) {
|
|
296
|
+
const chain = [];
|
|
297
|
+
let current = node;
|
|
298
|
+
while (current) {
|
|
299
|
+
chain.push(current);
|
|
300
|
+
if (current.parentNode) {
|
|
301
|
+
current = current.parentNode;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (current.host) {
|
|
305
|
+
current = current.host;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (current.nodeType === Node.DOCUMENT_NODE) {
|
|
309
|
+
current = getFrameElementForDocument(current);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
current = null;
|
|
313
|
+
}
|
|
314
|
+
return chain;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isSameComposedTarget(left, right) {
|
|
318
|
+
if (!left || !right) return false;
|
|
319
|
+
const seen = new Set(createComposedChain(left));
|
|
320
|
+
for (const current of createComposedChain(right)) {
|
|
321
|
+
if (seen.has(current)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isWithinComposedSubtree(root, candidate) {
|
|
329
|
+
if (!root || !candidate) return false;
|
|
330
|
+
for (const current of createComposedChain(candidate)) {
|
|
331
|
+
if (current === root) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveLocator(locator) {
|
|
339
|
+
const parts = splitLocator(locator);
|
|
340
|
+
if (parts.length === 0) return null;
|
|
341
|
+
|
|
342
|
+
let root = document;
|
|
343
|
+
let element = null;
|
|
344
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
345
|
+
element = root.querySelector(parts[index]);
|
|
346
|
+
if (!element) return null;
|
|
347
|
+
|
|
348
|
+
if (index === parts.length - 1) {
|
|
349
|
+
return element;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (element.tagName && element.tagName.toLowerCase() === 'iframe') {
|
|
353
|
+
let nextDocument = null;
|
|
354
|
+
try {
|
|
355
|
+
nextDocument = element.contentDocument;
|
|
356
|
+
} catch {
|
|
357
|
+
nextDocument = null;
|
|
358
|
+
}
|
|
359
|
+
if (!nextDocument) {
|
|
360
|
+
throw new Error('Unable to access iframe content for locator segment: ' + parts[index]);
|
|
361
|
+
}
|
|
362
|
+
root = nextDocument;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (element.shadowRoot) {
|
|
367
|
+
root = element.shadowRoot;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
throw new Error('Locator segment does not enter a shadow root or iframe: ' + parts[index]);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return element;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function resolveWithFallbacks(selector, fallbacks) {
|
|
378
|
+
let lastError = null;
|
|
379
|
+
const locators = [];
|
|
380
|
+
if (selector) locators.push(selector);
|
|
381
|
+
if (Array.isArray(fallbacks)) {
|
|
382
|
+
for (const fallback of fallbacks) {
|
|
383
|
+
locators.push(fallback);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const locator of locators) {
|
|
388
|
+
try {
|
|
389
|
+
const element = resolveLocator(locator);
|
|
390
|
+
if (element) {
|
|
391
|
+
return { element, locator };
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
lastError = error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (lastError) {
|
|
399
|
+
throw lastError;
|
|
400
|
+
}
|
|
401
|
+
throw new Error('Selector not found');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function ensureInView(element) {
|
|
405
|
+
if (element && typeof element.scrollIntoView === 'function') {
|
|
406
|
+
element.scrollIntoView({ block: 'center', inline: 'center' });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function toMainFramePoint(element) {
|
|
411
|
+
const rect = element.getBoundingClientRect();
|
|
412
|
+
let x = rect.left + rect.width / 2;
|
|
413
|
+
let y = rect.top + rect.height / 2;
|
|
414
|
+
let doc = element.ownerDocument;
|
|
415
|
+
|
|
416
|
+
while (doc) {
|
|
417
|
+
const frameElement = getFrameElementForDocument(doc);
|
|
418
|
+
if (!frameElement) {
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
const frameRect = frameElement.getBoundingClientRect();
|
|
422
|
+
x += frameRect.left;
|
|
423
|
+
y += frameRect.top;
|
|
424
|
+
doc = frameElement.ownerDocument;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { x, y, width: rect.width, height: rect.height };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getTopFrameElement(element) {
|
|
431
|
+
let topFrame = null;
|
|
432
|
+
let doc = element.ownerDocument;
|
|
433
|
+
while (doc) {
|
|
434
|
+
const frameElement = getFrameElementForDocument(doc);
|
|
435
|
+
if (!frameElement) {
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
topFrame = frameElement;
|
|
439
|
+
doc = frameElement.ownerDocument;
|
|
440
|
+
}
|
|
441
|
+
return topFrame;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function describeElement(element) {
|
|
445
|
+
if (!element || !element.tagName) return 'unknown';
|
|
446
|
+
const tag = element.tagName.toLowerCase();
|
|
447
|
+
const id = element.id ? '#' + element.id : '';
|
|
448
|
+
const className =
|
|
449
|
+
element.className && typeof element.className === 'string'
|
|
450
|
+
? '.' + element.className.trim().split(/\s+/).filter(Boolean).join('.')
|
|
451
|
+
: '';
|
|
452
|
+
return tag + id + className;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function deepActiveElement() {
|
|
456
|
+
let current = document.activeElement;
|
|
457
|
+
while (current) {
|
|
458
|
+
if (current.shadowRoot && current.shadowRoot.activeElement) {
|
|
459
|
+
current = current.shadowRoot.activeElement;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
463
|
+
try {
|
|
464
|
+
const frameActive = current.contentDocument && current.contentDocument.activeElement;
|
|
465
|
+
if (frameActive) {
|
|
466
|
+
current = frameActive;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// ignore inaccessible frames
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
return current;
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
478
|
+
function buildLocatorRuntimeExpression(inputName, inputValue, body) {
|
|
479
|
+
return `(async () => {
|
|
480
|
+
${LOCATOR_RUNTIME_HELPERS}
|
|
481
|
+
const ${inputName} = ${JSON.stringify(inputValue)};
|
|
482
|
+
${body}
|
|
483
|
+
})()`;
|
|
484
|
+
}
|
|
204
485
|
var ChromeCdpBrowserController = class {
|
|
205
486
|
connections = /* @__PURE__ */ new Map();
|
|
206
487
|
cleanupInterval;
|
|
@@ -392,6 +673,115 @@ var ChromeCdpBrowserController = class {
|
|
|
392
673
|
return `${result}: ${dialog.message}`;
|
|
393
674
|
});
|
|
394
675
|
}
|
|
676
|
+
async preparePointerTarget(conn, payload) {
|
|
677
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
678
|
+
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
679
|
+
const el = resolved.element;
|
|
680
|
+
ensureInView(el);
|
|
681
|
+
|
|
682
|
+
const point = toMainFramePoint(el);
|
|
683
|
+
if (point.width === 0 && point.height === 0) {
|
|
684
|
+
throw new Error('Element has zero size - it may be hidden or not rendered');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const topEl = document.elementFromPoint(point.x, point.y);
|
|
688
|
+
const topFrame = getTopFrameElement(el);
|
|
689
|
+
if (topEl && !isSameComposedTarget(topEl, el) && (!topFrame || topEl !== topFrame)) {
|
|
690
|
+
throw new Error('Element is covered by another element: ' + describeElement(topEl));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return { x: point.x, y: point.y };
|
|
694
|
+
`);
|
|
695
|
+
const point = (await conn.send("Runtime.evaluate", {
|
|
696
|
+
expression,
|
|
697
|
+
returnByValue: true,
|
|
698
|
+
awaitPromise: true
|
|
699
|
+
})).result.value;
|
|
700
|
+
if (!point || typeof point.x !== "number" || typeof point.y !== "number") throw new Error("Unable to resolve pointer target");
|
|
701
|
+
return {
|
|
702
|
+
x: point.x,
|
|
703
|
+
y: point.y
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
async prepareTypeTarget(conn, payload) {
|
|
707
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
708
|
+
const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
|
|
709
|
+
const el = resolved.element;
|
|
710
|
+
ensureInView(el);
|
|
711
|
+
|
|
712
|
+
const point = toMainFramePoint(el);
|
|
713
|
+
if (point.width === 0 && point.height === 0) {
|
|
714
|
+
throw new Error('Element has zero size - it may be hidden or not rendered');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const tag = el.tagName.toLowerCase();
|
|
718
|
+
const inputType = tag === 'input' ? (el.type || 'text').toLowerCase() : '';
|
|
719
|
+
const editable =
|
|
720
|
+
el.isContentEditable ||
|
|
721
|
+
tag === 'textarea' ||
|
|
722
|
+
(tag === 'input' &&
|
|
723
|
+
!['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'hidden'].includes(inputType));
|
|
724
|
+
|
|
725
|
+
if (!editable) {
|
|
726
|
+
throw new Error('Element is not text-editable');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (typeof el.focus === 'function') {
|
|
730
|
+
el.focus();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if ((payload.text ?? '') === '') {
|
|
734
|
+
if (el.isContentEditable) {
|
|
735
|
+
el.textContent = '';
|
|
736
|
+
} else if ('value' in el) {
|
|
737
|
+
el.value = '';
|
|
738
|
+
}
|
|
739
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
740
|
+
return 'cleared';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (el.isContentEditable) {
|
|
744
|
+
const selection = el.ownerDocument.getSelection ? el.ownerDocument.getSelection() : null;
|
|
745
|
+
if (selection) {
|
|
746
|
+
const range = el.ownerDocument.createRange();
|
|
747
|
+
range.selectNodeContents(el);
|
|
748
|
+
selection.removeAllRanges();
|
|
749
|
+
selection.addRange(range);
|
|
750
|
+
}
|
|
751
|
+
} else if (typeof el.select === 'function') {
|
|
752
|
+
el.select();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return 'ready';
|
|
756
|
+
`);
|
|
757
|
+
return (await conn.send("Runtime.evaluate", {
|
|
758
|
+
expression,
|
|
759
|
+
returnByValue: true,
|
|
760
|
+
awaitPromise: true
|
|
761
|
+
})).result.value ?? "ready";
|
|
762
|
+
}
|
|
763
|
+
async dispatchMouseClick(conn, x, y) {
|
|
764
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
765
|
+
type: "mouseMoved",
|
|
766
|
+
x,
|
|
767
|
+
y,
|
|
768
|
+
button: "none"
|
|
769
|
+
});
|
|
770
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
771
|
+
type: "mousePressed",
|
|
772
|
+
x,
|
|
773
|
+
y,
|
|
774
|
+
button: "left",
|
|
775
|
+
clickCount: 1
|
|
776
|
+
});
|
|
777
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
778
|
+
type: "mouseReleased",
|
|
779
|
+
x,
|
|
780
|
+
y,
|
|
781
|
+
button: "left",
|
|
782
|
+
clickCount: 1
|
|
783
|
+
});
|
|
784
|
+
}
|
|
395
785
|
async interact(targetWsUrl, payload) {
|
|
396
786
|
if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
397
787
|
await this.ensureEnabled(targetWsUrl);
|
|
@@ -430,48 +820,35 @@ var ChromeCdpBrowserController = class {
|
|
|
430
820
|
return "page refreshed";
|
|
431
821
|
});
|
|
432
822
|
if (payload.action === "dialog") return await this.handleDialogAction(targetWsUrl, payload);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const cls = topEl.className && typeof topEl.className === 'string' ? '.' + topEl.className.split(' ').join('.') : '';
|
|
461
|
-
throw new Error('Element is covered by another element: ' + tag + id + cls);
|
|
462
|
-
}
|
|
463
|
-
el.click();
|
|
464
|
-
return 'clicked';
|
|
465
|
-
}
|
|
466
|
-
if (payload.action === 'type') {
|
|
467
|
-
const el = resolveElement(payload.selector, payload.fallbackSelectors);
|
|
468
|
-
el.focus();
|
|
469
|
-
el.value = payload.text ?? '';
|
|
470
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
471
|
-
return 'typed';
|
|
472
|
-
}
|
|
823
|
+
if (payload.action === "click" || payload.action === "hover" || payload.action === "type") return await this.withRetry(targetWsUrl, async (conn) => {
|
|
824
|
+
await this.ensureEnabled(targetWsUrl);
|
|
825
|
+
if (payload.action === "click") {
|
|
826
|
+
const point = await this.preparePointerTarget(conn, payload);
|
|
827
|
+
await this.dispatchMouseClick(conn, point.x, point.y);
|
|
828
|
+
try {
|
|
829
|
+
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
830
|
+
try {
|
|
831
|
+
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
832
|
+
} catch {}
|
|
833
|
+
} catch {}
|
|
834
|
+
return "clicked";
|
|
835
|
+
}
|
|
836
|
+
if (payload.action === "hover") {
|
|
837
|
+
const point = await this.preparePointerTarget(conn, payload);
|
|
838
|
+
await conn.send("Input.dispatchMouseEvent", {
|
|
839
|
+
type: "mouseMoved",
|
|
840
|
+
x: point.x,
|
|
841
|
+
y: point.y,
|
|
842
|
+
button: "none"
|
|
843
|
+
});
|
|
844
|
+
return "hovered";
|
|
845
|
+
}
|
|
846
|
+
if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
|
|
847
|
+
return "typed";
|
|
848
|
+
});
|
|
849
|
+
const expression = buildLocatorRuntimeExpression("payload", payload, `
|
|
473
850
|
if (payload.action === 'press') {
|
|
474
|
-
const target =
|
|
851
|
+
const target = deepActiveElement();
|
|
475
852
|
if (!target) throw new Error('No active element to press key on');
|
|
476
853
|
const key = payload.key ?? 'Enter';
|
|
477
854
|
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
@@ -482,7 +859,7 @@ var ChromeCdpBrowserController = class {
|
|
|
482
859
|
const timeout = payload.timeoutMs ?? 2000;
|
|
483
860
|
const started = Date.now();
|
|
484
861
|
while (Date.now() - started < timeout) {
|
|
485
|
-
if (
|
|
862
|
+
if (resolveLocator(payload.selector)) {
|
|
486
863
|
return 'found';
|
|
487
864
|
}
|
|
488
865
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -499,25 +876,15 @@ var ChromeCdpBrowserController = class {
|
|
|
499
876
|
}
|
|
500
877
|
if (payload.action === 'scroll') {
|
|
501
878
|
if (payload.selector) {
|
|
502
|
-
const el =
|
|
879
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
503
880
|
el.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
504
881
|
return 'scrolled element';
|
|
505
882
|
}
|
|
506
883
|
window.scrollBy({ left: payload.scrollX ?? 0, top: payload.scrollY ?? 0, behavior: 'smooth' });
|
|
507
884
|
return 'scrolled page';
|
|
508
885
|
}
|
|
509
|
-
if (payload.action === 'hover') {
|
|
510
|
-
const el = resolveElement(payload.selector, payload.fallbackSelectors);
|
|
511
|
-
const rect = el.getBoundingClientRect();
|
|
512
|
-
const cx = rect.left + rect.width / 2;
|
|
513
|
-
const cy = rect.top + rect.height / 2;
|
|
514
|
-
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, clientX: cx, clientY: cy }));
|
|
515
|
-
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: cx, clientY: cy }));
|
|
516
|
-
el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: cx, clientY: cy }));
|
|
517
|
-
return 'hovered';
|
|
518
|
-
}
|
|
519
886
|
if (payload.action === 'select') {
|
|
520
|
-
const el =
|
|
887
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
521
888
|
if (el.tagName.toLowerCase() !== 'select') throw new Error('Element is not a <select>');
|
|
522
889
|
el.value = payload.value ?? '';
|
|
523
890
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
@@ -525,50 +892,42 @@ var ChromeCdpBrowserController = class {
|
|
|
525
892
|
return 'selected ' + el.value;
|
|
526
893
|
}
|
|
527
894
|
if (payload.action === 'toggle') {
|
|
528
|
-
const el =
|
|
895
|
+
const el = resolveWithFallbacks(payload.selector, payload.fallbackSelectors).element;
|
|
529
896
|
el.click();
|
|
530
897
|
const checked = el.checked !== undefined ? el.checked : el.getAttribute('aria-checked') === 'true';
|
|
531
898
|
return 'toggled to ' + (checked ? 'checked' : 'unchecked');
|
|
532
899
|
}
|
|
533
900
|
throw new Error('Unsupported interact action');
|
|
534
|
-
|
|
901
|
+
`);
|
|
535
902
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
536
903
|
await this.ensureEnabled(targetWsUrl);
|
|
537
|
-
|
|
904
|
+
return (await conn.send("Runtime.evaluate", {
|
|
538
905
|
expression,
|
|
539
906
|
returnByValue: true,
|
|
540
907
|
awaitPromise: true
|
|
541
908
|
})).result.value ?? "";
|
|
542
|
-
if (payload.action === "click" && value === "clicked") try {
|
|
543
|
-
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
544
|
-
try {
|
|
545
|
-
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
546
|
-
} catch {}
|
|
547
|
-
} catch {}
|
|
548
|
-
return value;
|
|
549
909
|
});
|
|
550
910
|
}
|
|
551
911
|
async getContent(targetWsUrl, options) {
|
|
552
912
|
if (options.mode === "a11y") return {
|
|
553
913
|
mode: "a11y",
|
|
554
|
-
content: await this.getAccessibilityTree(targetWsUrl)
|
|
914
|
+
content: await this.getAccessibilityTree(targetWsUrl, options.selector)
|
|
555
915
|
};
|
|
556
|
-
const expression =
|
|
557
|
-
const options = ${JSON.stringify(options)};
|
|
916
|
+
const expression = buildLocatorRuntimeExpression("options", options, `
|
|
558
917
|
if (options.mode === 'title') return document.title ?? '';
|
|
559
918
|
if (options.mode === 'html') {
|
|
560
919
|
if (options.selector) {
|
|
561
|
-
const el =
|
|
920
|
+
const el = resolveLocator(options.selector);
|
|
562
921
|
return el ? el.outerHTML : '';
|
|
563
922
|
}
|
|
564
923
|
return document.documentElement?.outerHTML ?? '';
|
|
565
924
|
}
|
|
566
925
|
if (options.selector) {
|
|
567
|
-
const el =
|
|
926
|
+
const el = resolveLocator(options.selector);
|
|
568
927
|
return el ? el.innerText ?? '' : '';
|
|
569
928
|
}
|
|
570
929
|
return document.body?.innerText ?? '';
|
|
571
|
-
|
|
930
|
+
`);
|
|
572
931
|
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
573
932
|
await this.ensureEnabled(targetWsUrl);
|
|
574
933
|
const content = (await conn.send("Runtime.evaluate", {
|
|
@@ -581,64 +940,88 @@ var ChromeCdpBrowserController = class {
|
|
|
581
940
|
};
|
|
582
941
|
});
|
|
583
942
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
943
|
+
formatAccessibilityTree(nodes) {
|
|
944
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
945
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
946
|
+
for (const node of nodes) {
|
|
947
|
+
nodeMap.set(node.nodeId, node);
|
|
948
|
+
if (node.parentId) {
|
|
949
|
+
const siblings = childrenMap.get(node.parentId);
|
|
950
|
+
if (siblings) siblings.push(node.nodeId);
|
|
951
|
+
else childrenMap.set(node.parentId, [node.nodeId]);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const lines = [];
|
|
955
|
+
const formatNode = (nodeId, depth) => {
|
|
956
|
+
const node = nodeMap.get(nodeId);
|
|
957
|
+
if (!node) return;
|
|
958
|
+
const role = node.role?.value ?? "unknown";
|
|
959
|
+
const name = node.name?.value ?? "";
|
|
960
|
+
const value = node.value?.value ?? "";
|
|
961
|
+
if (node.ignored) {
|
|
962
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
963
|
+
for (const childId of children) formatNode(childId, depth);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const skip = !name && !value && (role === "generic" || role === "none" || role === "GenericContainer");
|
|
967
|
+
if (!skip) {
|
|
968
|
+
let line = `${" ".repeat(depth)}${role}`;
|
|
969
|
+
if (name) line += ` "${name}"`;
|
|
970
|
+
if (value) line += ` value="${value}"`;
|
|
971
|
+
if (node.properties) {
|
|
972
|
+
for (const prop of node.properties) if (prop.value.value === true) line += ` [${prop.name}]`;
|
|
973
|
+
else if (prop.name === "checked" && prop.value.value === "mixed") line += ` [indeterminate]`;
|
|
596
974
|
}
|
|
975
|
+
lines.push(line);
|
|
597
976
|
}
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
977
|
+
const children = childrenMap.get(nodeId) ?? node.childIds ?? [];
|
|
978
|
+
for (const childId of children) formatNode(childId, skip ? depth : depth + 1);
|
|
979
|
+
};
|
|
980
|
+
const roots = nodes.filter((node) => !node.parentId || !nodeMap.has(node.parentId));
|
|
981
|
+
for (const root of roots) formatNode(root.nodeId, 0);
|
|
982
|
+
return lines.join("\n");
|
|
983
|
+
}
|
|
984
|
+
async getAccessibilityTree(targetWsUrl, selector) {
|
|
985
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
986
|
+
await this.ensureEnabled(targetWsUrl);
|
|
987
|
+
await conn.send("Accessibility.enable");
|
|
988
|
+
let nodes;
|
|
989
|
+
if (selector) {
|
|
990
|
+
const objectGroup = "agentic-browser-a11y";
|
|
991
|
+
const selected = await conn.send("Runtime.evaluate", {
|
|
992
|
+
expression: buildLocatorRuntimeExpression("selector", selector, `
|
|
993
|
+
return resolveLocator(selector);
|
|
994
|
+
`),
|
|
995
|
+
objectGroup
|
|
996
|
+
});
|
|
997
|
+
const objectId = selected.result.objectId;
|
|
998
|
+
if (!objectId || selected.result.subtype === "null") {
|
|
999
|
+
try {
|
|
1000
|
+
await conn.send("Runtime.releaseObjectGroup", { objectGroup });
|
|
1001
|
+
} catch {}
|
|
1002
|
+
return "";
|
|
609
1003
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
lines.push(line);
|
|
1004
|
+
try {
|
|
1005
|
+
nodes = (await conn.send("Accessibility.getPartialAXTree", {
|
|
1006
|
+
objectId,
|
|
1007
|
+
fetchRelatives: false
|
|
1008
|
+
})).nodes;
|
|
1009
|
+
} finally {
|
|
1010
|
+
try {
|
|
1011
|
+
await conn.send("Runtime.releaseObjectGroup", { objectGroup });
|
|
1012
|
+
} catch {}
|
|
620
1013
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
};
|
|
624
|
-
const roots = nodes.filter((n) => !n.parentId);
|
|
625
|
-
for (const root of roots) formatNode(root.nodeId, 0);
|
|
626
|
-
return lines.join("\n");
|
|
1014
|
+
} else nodes = (await conn.send("Accessibility.getFullAXTree")).nodes;
|
|
1015
|
+
return this.formatAccessibilityTree(nodes);
|
|
627
1016
|
});
|
|
628
1017
|
}
|
|
629
1018
|
async getInteractiveElements(targetWsUrl, options) {
|
|
630
|
-
const expression =
|
|
631
|
-
const options = ${JSON.stringify(options)};
|
|
1019
|
+
const expression = buildLocatorRuntimeExpression("options", options, `
|
|
632
1020
|
const visibleOnly = options.visibleOnly !== false;
|
|
633
1021
|
const limit = options.limit ?? 50;
|
|
634
1022
|
const scopeSelector = options.selector;
|
|
635
1023
|
const roleFilter = options.roles ? new Set(options.roles) : null;
|
|
636
|
-
|
|
637
|
-
const root = scopeSelector
|
|
638
|
-
? document.querySelector(scopeSelector) ?? document.body
|
|
639
|
-
: document.body;
|
|
640
|
-
|
|
641
|
-
const candidates = root.querySelectorAll([
|
|
1024
|
+
const candidateSelector = [
|
|
642
1025
|
'a[href]',
|
|
643
1026
|
'button',
|
|
644
1027
|
'input:not([type="hidden"])',
|
|
@@ -654,43 +1037,15 @@ var ChromeCdpBrowserController = class {
|
|
|
654
1037
|
'[onclick]',
|
|
655
1038
|
'[tabindex]',
|
|
656
1039
|
'[contenteditable="true"]',
|
|
657
|
-
'[contenteditable=""]'
|
|
658
|
-
].join(',')
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (tag === 'button' || ariaRole === 'button') return 'button';
|
|
667
|
-
if (tag === 'input') {
|
|
668
|
-
const t = (el.type || 'text').toLowerCase();
|
|
669
|
-
if (t === 'checkbox') return 'checkbox';
|
|
670
|
-
if (t === 'radio') return 'radio';
|
|
671
|
-
return 'input';
|
|
672
|
-
}
|
|
673
|
-
if (tag === 'select') return 'select';
|
|
674
|
-
if (tag === 'textarea') return 'textarea';
|
|
675
|
-
if (el.isContentEditable) return 'contenteditable';
|
|
676
|
-
if (ariaRole === 'link') return 'link';
|
|
677
|
-
if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
|
|
678
|
-
if (ariaRole === 'radio') return 'radio';
|
|
679
|
-
return 'custom';
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function getActions(role, el) {
|
|
683
|
-
switch (role) {
|
|
684
|
-
case 'link': case 'button': case 'custom': return ['click'];
|
|
685
|
-
case 'input': {
|
|
686
|
-
const t = (el.type || 'text').toLowerCase();
|
|
687
|
-
if (t === 'submit' || t === 'reset' || t === 'button' || t === 'file') return ['click'];
|
|
688
|
-
return ['click', 'type', 'press'];
|
|
689
|
-
}
|
|
690
|
-
case 'textarea': case 'contenteditable': return ['click', 'type', 'press'];
|
|
691
|
-
case 'select': return ['click', 'select'];
|
|
692
|
-
case 'checkbox': case 'radio': return ['click', 'toggle'];
|
|
693
|
-
default: return ['click'];
|
|
1040
|
+
'[contenteditable=""]'
|
|
1041
|
+
].join(',');
|
|
1042
|
+
|
|
1043
|
+
let scopeElement = null;
|
|
1044
|
+
if (scopeSelector) {
|
|
1045
|
+
try {
|
|
1046
|
+
scopeElement = resolveLocator(scopeSelector) ?? document.body;
|
|
1047
|
+
} catch {
|
|
1048
|
+
scopeElement = document.body;
|
|
694
1049
|
}
|
|
695
1050
|
}
|
|
696
1051
|
|
|
@@ -698,79 +1053,90 @@ var ChromeCdpBrowserController = class {
|
|
|
698
1053
|
return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
|
|
699
1054
|
}
|
|
700
1055
|
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
1056
|
+
function composeLocator(path, selector) {
|
|
1057
|
+
if (!path.length) return selector;
|
|
1058
|
+
return path.join(LOCATOR_SEPARATOR) + LOCATOR_SEPARATOR + selector;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function tryUniqueSelector(root, selector) {
|
|
1062
|
+
try {
|
|
1063
|
+
return root.querySelectorAll(selector).length === 1 ? selector : null;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
704
1067
|
}
|
|
705
1068
|
|
|
706
|
-
function buildSelector(el) {
|
|
1069
|
+
function buildSelector(root, el) {
|
|
707
1070
|
if (el.id) return '#' + CSS.escape(el.id);
|
|
708
1071
|
|
|
709
1072
|
const name = el.getAttribute('name');
|
|
710
1073
|
if (name) {
|
|
711
1074
|
const tag = el.tagName.toLowerCase();
|
|
712
|
-
const
|
|
713
|
-
if (
|
|
1075
|
+
const selector = tryUniqueSelector(root, tag + '[name="' + escapeAttr(name) + '"]');
|
|
1076
|
+
if (selector) return selector;
|
|
714
1077
|
}
|
|
715
1078
|
|
|
716
1079
|
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
717
1080
|
if (testId) {
|
|
718
1081
|
const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
1082
|
+
const selector = tryUniqueSelector(root, '[' + attr + '="' + escapeAttr(testId) + '"]');
|
|
1083
|
+
if (selector) return selector;
|
|
721
1084
|
}
|
|
722
1085
|
|
|
723
1086
|
const ariaLabel = el.getAttribute('aria-label');
|
|
724
1087
|
if (ariaLabel) {
|
|
725
1088
|
const tag = el.tagName.toLowerCase();
|
|
726
|
-
const
|
|
727
|
-
if (
|
|
1089
|
+
const selector = tryUniqueSelector(root, tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]');
|
|
1090
|
+
if (selector) return selector;
|
|
728
1091
|
}
|
|
729
1092
|
|
|
730
1093
|
const dataCy = el.getAttribute('data-cy');
|
|
731
1094
|
if (dataCy) {
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
1095
|
+
const selector = tryUniqueSelector(root, '[data-cy="' + escapeAttr(dataCy) + '"]');
|
|
1096
|
+
if (selector) return selector;
|
|
734
1097
|
}
|
|
735
1098
|
|
|
736
1099
|
const dataTest = el.getAttribute('data-test');
|
|
737
1100
|
if (dataTest) {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
1101
|
+
const selector = tryUniqueSelector(root, '[data-test="' + escapeAttr(dataTest) + '"]');
|
|
1102
|
+
if (selector) return selector;
|
|
740
1103
|
}
|
|
741
1104
|
|
|
742
1105
|
const role = el.getAttribute('role');
|
|
743
1106
|
if (role && ariaLabel) {
|
|
744
|
-
const
|
|
745
|
-
|
|
1107
|
+
const selector = tryUniqueSelector(
|
|
1108
|
+
root,
|
|
1109
|
+
'[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]'
|
|
1110
|
+
);
|
|
1111
|
+
if (selector) return selector;
|
|
746
1112
|
}
|
|
747
1113
|
|
|
748
|
-
// Path fallback — anchor at nearest ancestor with an id for shorter selectors
|
|
749
1114
|
const parts = [];
|
|
750
1115
|
let current = el;
|
|
751
|
-
while (current && current
|
|
1116
|
+
while (current && current.nodeType === 1) {
|
|
752
1117
|
const tag = current.tagName.toLowerCase();
|
|
753
1118
|
if (current !== el && current.id) {
|
|
754
1119
|
parts.unshift('#' + CSS.escape(current.id));
|
|
755
1120
|
break;
|
|
756
1121
|
}
|
|
757
1122
|
const parent = current.parentElement;
|
|
758
|
-
if (!parent) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1123
|
+
if (!parent) {
|
|
1124
|
+
parts.unshift(tag);
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
const siblings = Array.from(parent.children).filter((candidate) => candidate.tagName === current.tagName);
|
|
762
1128
|
if (siblings.length === 1) {
|
|
763
1129
|
parts.unshift(tag);
|
|
764
1130
|
} else {
|
|
765
|
-
const
|
|
766
|
-
parts.unshift(tag + ':nth-of-type(' +
|
|
1131
|
+
const index = siblings.indexOf(current) + 1;
|
|
1132
|
+
parts.unshift(tag + ':nth-of-type(' + index + ')');
|
|
767
1133
|
}
|
|
768
1134
|
current = parent;
|
|
769
1135
|
}
|
|
770
1136
|
return parts.join(' > ');
|
|
771
1137
|
}
|
|
772
1138
|
|
|
773
|
-
function buildFallbackSelectors(el, primarySelector) {
|
|
1139
|
+
function buildFallbackSelectors(root, path, el, primarySelector) {
|
|
774
1140
|
const fallbacks = [];
|
|
775
1141
|
const candidates = [];
|
|
776
1142
|
|
|
@@ -779,8 +1145,7 @@ var ChromeCdpBrowserController = class {
|
|
|
779
1145
|
const name = el.getAttribute('name');
|
|
780
1146
|
if (name) {
|
|
781
1147
|
const tag = el.tagName.toLowerCase();
|
|
782
|
-
|
|
783
|
-
candidates.push(sel);
|
|
1148
|
+
candidates.push(tag + '[name="' + escapeAttr(name) + '"]');
|
|
784
1149
|
}
|
|
785
1150
|
|
|
786
1151
|
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
@@ -806,16 +1171,68 @@ var ChromeCdpBrowserController = class {
|
|
|
806
1171
|
candidates.push('[role="' + escapeAttr(role) + '"][aria-label="' + escapeAttr(ariaLabel) + '"]');
|
|
807
1172
|
}
|
|
808
1173
|
|
|
809
|
-
for (const
|
|
810
|
-
if (
|
|
811
|
-
|
|
1174
|
+
for (const candidate of candidates) {
|
|
1175
|
+
if (!tryUniqueSelector(root, candidate)) continue;
|
|
1176
|
+
const locator = composeLocator(path, candidate);
|
|
1177
|
+
if (locator === primarySelector) continue;
|
|
1178
|
+
fallbacks.push(locator);
|
|
812
1179
|
if (fallbacks.length >= 3) break;
|
|
813
1180
|
}
|
|
814
1181
|
return fallbacks;
|
|
815
1182
|
}
|
|
816
1183
|
|
|
1184
|
+
function classifyRole(el) {
|
|
1185
|
+
const tag = el.tagName.toLowerCase();
|
|
1186
|
+
const ariaRole = el.getAttribute('role');
|
|
1187
|
+
if (tag === 'a') return 'link';
|
|
1188
|
+
if (tag === 'button' || ariaRole === 'button') return 'button';
|
|
1189
|
+
if (tag === 'input') {
|
|
1190
|
+
const type = (el.type || 'text').toLowerCase();
|
|
1191
|
+
if (type === 'checkbox') return 'checkbox';
|
|
1192
|
+
if (type === 'radio') return 'radio';
|
|
1193
|
+
return 'input';
|
|
1194
|
+
}
|
|
1195
|
+
if (tag === 'select') return 'select';
|
|
1196
|
+
if (tag === 'textarea') return 'textarea';
|
|
1197
|
+
if (el.isContentEditable) return 'contenteditable';
|
|
1198
|
+
if (ariaRole === 'link') return 'link';
|
|
1199
|
+
if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
|
|
1200
|
+
if (ariaRole === 'radio') return 'radio';
|
|
1201
|
+
return 'custom';
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function getActions(role, el) {
|
|
1205
|
+
switch (role) {
|
|
1206
|
+
case 'link':
|
|
1207
|
+
case 'button':
|
|
1208
|
+
case 'custom':
|
|
1209
|
+
return ['click'];
|
|
1210
|
+
case 'input': {
|
|
1211
|
+
const type = (el.type || 'text').toLowerCase();
|
|
1212
|
+
if (type === 'submit' || type === 'reset' || type === 'button' || type === 'file') {
|
|
1213
|
+
return ['click'];
|
|
1214
|
+
}
|
|
1215
|
+
return ['click', 'type', 'press'];
|
|
1216
|
+
}
|
|
1217
|
+
case 'textarea':
|
|
1218
|
+
case 'contenteditable':
|
|
1219
|
+
return ['click', 'type', 'press'];
|
|
1220
|
+
case 'select':
|
|
1221
|
+
return ['click', 'select'];
|
|
1222
|
+
case 'checkbox':
|
|
1223
|
+
case 'radio':
|
|
1224
|
+
return ['click', 'toggle'];
|
|
1225
|
+
default:
|
|
1226
|
+
return ['click'];
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function viewForElement(el) {
|
|
1231
|
+
return el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
817
1234
|
function isVisible(el) {
|
|
818
|
-
const style =
|
|
1235
|
+
const style = viewForElement(el).getComputedStyle(el);
|
|
819
1236
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
820
1237
|
return false;
|
|
821
1238
|
}
|
|
@@ -829,16 +1246,16 @@ var ChromeCdpBrowserController = class {
|
|
|
829
1246
|
|
|
830
1247
|
const tag = el.tagName.toLowerCase();
|
|
831
1248
|
if (tag === 'input') {
|
|
832
|
-
const
|
|
833
|
-
if (
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
1249
|
+
const value = el.value;
|
|
1250
|
+
if (value) return value.slice(0, 80);
|
|
1251
|
+
const placeholder = el.getAttribute('placeholder');
|
|
1252
|
+
if (placeholder) return placeholder.slice(0, 80);
|
|
836
1253
|
return el.type || 'text';
|
|
837
1254
|
}
|
|
838
1255
|
|
|
839
1256
|
const directText = Array.from(el.childNodes)
|
|
840
|
-
.filter(
|
|
841
|
-
.map(
|
|
1257
|
+
.filter((node) => node.nodeType === 3)
|
|
1258
|
+
.map((node) => node.textContent.trim())
|
|
842
1259
|
.filter(Boolean)
|
|
843
1260
|
.join(' ');
|
|
844
1261
|
if (directText) return directText.slice(0, 80);
|
|
@@ -861,48 +1278,95 @@ var ChromeCdpBrowserController = class {
|
|
|
861
1278
|
return ariaDisabled !== 'true';
|
|
862
1279
|
}
|
|
863
1280
|
|
|
1281
|
+
function rootEntries(root) {
|
|
1282
|
+
if (!root) return [];
|
|
1283
|
+
if (root.nodeType === Node.DOCUMENT_NODE) {
|
|
1284
|
+
if (root.body) return [root.body];
|
|
1285
|
+
if (root.documentElement) return [root.documentElement];
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
1289
|
+
return Array.from(root.children || []);
|
|
1290
|
+
}
|
|
1291
|
+
return [root];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
864
1294
|
const results = [];
|
|
865
1295
|
let totalFound = 0;
|
|
866
1296
|
|
|
867
|
-
|
|
868
|
-
if (
|
|
869
|
-
|
|
1297
|
+
function maybeRecordElement(root, path, el) {
|
|
1298
|
+
if (scopeElement && !isWithinComposedSubtree(scopeElement, el)) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (!el.matches(candidateSelector)) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
870
1305
|
|
|
871
1306
|
const role = classifyRole(el);
|
|
872
|
-
if (roleFilter && !roleFilter.has(role))
|
|
1307
|
+
if (roleFilter && !roleFilter.has(role)) return;
|
|
873
1308
|
|
|
874
|
-
const
|
|
875
|
-
if (visibleOnly && !
|
|
1309
|
+
const visible = isVisible(el);
|
|
1310
|
+
if (visibleOnly && !visible) return;
|
|
876
1311
|
|
|
877
|
-
totalFound
|
|
878
|
-
if (results.length >= limit)
|
|
1312
|
+
totalFound += 1;
|
|
1313
|
+
if (results.length >= limit) return;
|
|
879
1314
|
|
|
880
|
-
const
|
|
1315
|
+
const selector = composeLocator(path, buildSelector(root, el));
|
|
881
1316
|
const entry = {
|
|
882
|
-
selector
|
|
1317
|
+
selector,
|
|
883
1318
|
role,
|
|
884
1319
|
tagName: el.tagName.toLowerCase(),
|
|
885
1320
|
text: getText(el),
|
|
886
1321
|
actions: getActions(role, el),
|
|
887
|
-
visible
|
|
1322
|
+
visible,
|
|
888
1323
|
enabled: isEnabled(el),
|
|
889
1324
|
};
|
|
890
1325
|
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
1326
|
+
const fallbacks = buildFallbackSelectors(root, path, el, selector);
|
|
1327
|
+
if (fallbacks.length) entry.fallbackSelectors = fallbacks;
|
|
893
1328
|
|
|
894
1329
|
if (role === 'link' && el.href) entry.href = el.href;
|
|
895
1330
|
if (role === 'input') entry.inputType = (el.type || 'text').toLowerCase();
|
|
896
|
-
const
|
|
897
|
-
if (
|
|
898
|
-
const
|
|
899
|
-
if (
|
|
1331
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
1332
|
+
if (ariaLabel) entry.ariaLabel = ariaLabel;
|
|
1333
|
+
const placeholder = el.getAttribute('placeholder');
|
|
1334
|
+
if (placeholder) entry.placeholder = placeholder;
|
|
900
1335
|
|
|
901
1336
|
results.push(entry);
|
|
902
1337
|
}
|
|
903
1338
|
|
|
1339
|
+
function walkRoot(root, path) {
|
|
1340
|
+
const queue = rootEntries(root).slice();
|
|
1341
|
+
while (queue.length) {
|
|
1342
|
+
const current = queue.shift();
|
|
1343
|
+
if (!current || current.nodeType !== 1) continue;
|
|
1344
|
+
|
|
1345
|
+
maybeRecordElement(root, path, current);
|
|
1346
|
+
|
|
1347
|
+
if (current.shadowRoot) {
|
|
1348
|
+
walkRoot(current.shadowRoot, path.concat(buildSelector(root, current)));
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (current.tagName && current.tagName.toLowerCase() === 'iframe') {
|
|
1352
|
+
try {
|
|
1353
|
+
if (current.contentDocument) {
|
|
1354
|
+
walkRoot(current.contentDocument, path.concat(buildSelector(root, current)));
|
|
1355
|
+
}
|
|
1356
|
+
} catch {
|
|
1357
|
+
// ignore inaccessible frames
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
for (const child of Array.from(current.children)) {
|
|
1362
|
+
queue.push(child);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
walkRoot(document, []);
|
|
904
1368
|
return { elements: results, totalFound, truncated: totalFound > results.length };
|
|
905
|
-
|
|
1369
|
+
`);
|
|
906
1370
|
const emptyResult = {
|
|
907
1371
|
elements: [],
|
|
908
1372
|
totalFound: 0,
|
|
@@ -1178,7 +1642,6 @@ var MockBrowserController = class {
|
|
|
1178
1642
|
terminate(_pid) {}
|
|
1179
1643
|
closeConnection(_targetWsUrl) {}
|
|
1180
1644
|
};
|
|
1181
|
-
|
|
1182
1645
|
//#endregion
|
|
1183
1646
|
//#region src/session/session-store.ts
|
|
1184
1647
|
var SessionStore = class {
|
|
@@ -1267,7 +1730,6 @@ var SessionStore = class {
|
|
|
1267
1730
|
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
|
|
1268
1731
|
}
|
|
1269
1732
|
};
|
|
1270
|
-
|
|
1271
1733
|
//#endregion
|
|
1272
1734
|
//#region src/session/session-manager.ts
|
|
1273
1735
|
var SessionManager = class {
|
|
@@ -1640,7 +2102,6 @@ var SessionManager = class {
|
|
|
1640
2102
|
}
|
|
1641
2103
|
}
|
|
1642
2104
|
};
|
|
1643
|
-
|
|
1644
2105
|
//#endregion
|
|
1645
2106
|
//#region src/transport/control-api.ts
|
|
1646
2107
|
var ControlApi = class {
|
|
@@ -1694,7 +2155,6 @@ var ControlApi = class {
|
|
|
1694
2155
|
return this.sessions.cleanupSessions(input);
|
|
1695
2156
|
}
|
|
1696
2157
|
};
|
|
1697
|
-
|
|
1698
2158
|
//#endregion
|
|
1699
2159
|
//#region src/lib/config.ts
|
|
1700
2160
|
const DEFAULT_PORT = 43111;
|
|
@@ -1720,7 +2180,6 @@ function loadConfig(env = process.env) {
|
|
|
1720
2180
|
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
1721
2181
|
};
|
|
1722
2182
|
}
|
|
1723
|
-
|
|
1724
2183
|
//#endregion
|
|
1725
2184
|
//#region src/observability/logger.ts
|
|
1726
2185
|
var Logger = class {
|
|
@@ -1752,7 +2211,6 @@ var Logger = class {
|
|
|
1752
2211
|
this.emit("error", message, context);
|
|
1753
2212
|
}
|
|
1754
2213
|
};
|
|
1755
|
-
|
|
1756
2214
|
//#endregion
|
|
1757
2215
|
//#region src/observability/event-store.ts
|
|
1758
2216
|
const MAX_EVENTS_PER_SESSION = 500;
|
|
@@ -1778,7 +2236,6 @@ var EventStore = class {
|
|
|
1778
2236
|
this.events.delete(sessionId);
|
|
1779
2237
|
}
|
|
1780
2238
|
};
|
|
1781
|
-
|
|
1782
2239
|
//#endregion
|
|
1783
2240
|
//#region src/auth/session-token.ts
|
|
1784
2241
|
var SessionTokenService = class {
|
|
@@ -1806,7 +2263,6 @@ var SessionTokenService = class {
|
|
|
1806
2263
|
this.tokens.set(sessionId, token);
|
|
1807
2264
|
}
|
|
1808
2265
|
};
|
|
1809
|
-
|
|
1810
2266
|
//#endregion
|
|
1811
2267
|
//#region src/transport/ws-server.ts
|
|
1812
2268
|
var AuthenticatedWsServer = class {
|
|
@@ -1841,7 +2297,6 @@ var AuthenticatedWsServer = class {
|
|
|
1841
2297
|
this.server = void 0;
|
|
1842
2298
|
}
|
|
1843
2299
|
};
|
|
1844
|
-
|
|
1845
2300
|
//#endregion
|
|
1846
2301
|
//#region src/memory/memory-index.ts
|
|
1847
2302
|
function normalize(value) {
|
|
@@ -1902,11 +2357,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
|
|
|
1902
2357
|
var MemoryIndex = class {
|
|
1903
2358
|
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
1904
2359
|
domainIndex = /* @__PURE__ */ new Map();
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
/** Rebuild the domain index when the underlying array or its size changes. */
|
|
2360
|
+
indexedRefs = [];
|
|
2361
|
+
/** Rebuild the domain index when the insight object set changes. */
|
|
1908
2362
|
ensureIndex(insights) {
|
|
1909
|
-
if (this.
|
|
2363
|
+
if (this.indexedRefs.length === insights.length && insights.every((insight, index) => this.indexedRefs[index] === insight)) return;
|
|
1910
2364
|
this.domainIndex.clear();
|
|
1911
2365
|
for (const insight of insights) {
|
|
1912
2366
|
const domain = normalize(insight.siteDomain);
|
|
@@ -1917,8 +2371,7 @@ var MemoryIndex = class {
|
|
|
1917
2371
|
}
|
|
1918
2372
|
bucket.push(insight);
|
|
1919
2373
|
}
|
|
1920
|
-
this.
|
|
1921
|
-
this.indexedLength = insights.length;
|
|
2374
|
+
this.indexedRefs = insights.slice();
|
|
1922
2375
|
}
|
|
1923
2376
|
search(insights, input) {
|
|
1924
2377
|
const normalizedIntent = normalize(input.taskIntent);
|
|
@@ -1940,7 +2393,6 @@ var MemoryIndex = class {
|
|
|
1940
2393
|
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1941
2394
|
}
|
|
1942
2395
|
};
|
|
1943
|
-
|
|
1944
2396
|
//#endregion
|
|
1945
2397
|
//#region src/memory/staleness-detector.ts
|
|
1946
2398
|
const MAX_SUSPECT_STRIKES = 2;
|
|
@@ -1983,7 +2435,6 @@ function applySuccess(insight) {
|
|
|
1983
2435
|
updatedAt: now
|
|
1984
2436
|
};
|
|
1985
2437
|
}
|
|
1986
|
-
|
|
1987
2438
|
//#endregion
|
|
1988
2439
|
//#region src/memory/memory-schemas.ts
|
|
1989
2440
|
const InsightFreshnessSchema = z.enum([
|
|
@@ -2036,7 +2487,6 @@ const TaskInsightSchema = z.object({
|
|
|
2036
2487
|
selectorAliases: z.array(SelectorAliasSchema).default([])
|
|
2037
2488
|
});
|
|
2038
2489
|
const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
2039
|
-
|
|
2040
2490
|
//#endregion
|
|
2041
2491
|
//#region src/memory/task-insight-store.ts
|
|
2042
2492
|
const EMPTY_STATE = { insights: [] };
|
|
@@ -2160,7 +2610,6 @@ var TaskInsightStore = class {
|
|
|
2160
2610
|
});
|
|
2161
2611
|
}
|
|
2162
2612
|
};
|
|
2163
|
-
|
|
2164
2613
|
//#endregion
|
|
2165
2614
|
//#region src/memory/memory-service.ts
|
|
2166
2615
|
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
@@ -2380,7 +2829,6 @@ var MemoryService = class {
|
|
|
2380
2829
|
return [...recipe, step].slice(-8);
|
|
2381
2830
|
}
|
|
2382
2831
|
};
|
|
2383
|
-
|
|
2384
2832
|
//#endregion
|
|
2385
2833
|
//#region src/cli/app.ts
|
|
2386
2834
|
function createAppContext(env = process.env) {
|
|
@@ -2401,7 +2849,6 @@ function createAppContext(env = process.env) {
|
|
|
2401
2849
|
memoryService: new MemoryService(config.logDir)
|
|
2402
2850
|
};
|
|
2403
2851
|
}
|
|
2404
|
-
|
|
2405
2852
|
//#endregion
|
|
2406
2853
|
//#region src/cli/runtime.ts
|
|
2407
2854
|
var AgenticBrowserCore = class {
|
|
@@ -2475,6 +2922,5 @@ function createMockAgenticBrowserCore(env) {
|
|
|
2475
2922
|
function createCliRuntime() {
|
|
2476
2923
|
return createAgenticBrowserCore();
|
|
2477
2924
|
}
|
|
2478
|
-
|
|
2479
2925
|
//#endregion
|
|
2480
|
-
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|
|
2926
|
+
export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };
|