aether-mcp-server 2.0.2 → 2.1.1
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/bridge/debugging.js +133 -0
- package/dist/bridge/inspection.js +302 -0
- package/dist/bridge/interaction.js +586 -0
- package/dist/bridge/navigation.js +146 -0
- package/dist/bridge/session.js +287 -0
- package/dist/cdp-bridge.js +598 -1981
- package/dist/cdp-client.js +232 -366
- package/dist/element-collector.js +198 -0
- package/dist/eval-scripts.js +1024 -0
- package/dist/index.js +16 -28
- package/dist/locator-engine.js +21 -259
- package/dist/logger.js +105 -0
- package/dist/mcp-server.js +59 -0
- package/dist/page-snapshot-cache.js +17 -2
- package/dist/types.js +267 -0
- package/package.json +1 -1
package/dist/cdp-bridge.js
CHANGED
|
@@ -1,724 +1,399 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.CdpBridge = void 0;
|
|
4
37
|
exports.getCdpBridge = getCdpBridge;
|
|
38
|
+
/**
|
|
39
|
+
* CdpBridge — thin command router that delegates to focused bridge modules.
|
|
40
|
+
*
|
|
41
|
+
* Previously this was a 2,761-line God object. Now it's ~350 lines of routing
|
|
42
|
+
* logic. All actual browser operations live in the bridge/ modules.
|
|
43
|
+
*/
|
|
5
44
|
const cdp_client_1 = require("./cdp-client");
|
|
6
|
-
const captcha_solver_1 = require("./captcha-solver");
|
|
7
45
|
const locator_engine_1 = require("./locator-engine");
|
|
8
46
|
const page_snapshot_cache_1 = require("./page-snapshot-cache");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
47
|
+
const captcha_solver_1 = require("./captcha-solver");
|
|
48
|
+
const logger_1 = require("./logger");
|
|
49
|
+
// Bridge modules
|
|
50
|
+
const Navigation = __importStar(require("./bridge/navigation"));
|
|
51
|
+
const Interaction = __importStar(require("./bridge/interaction"));
|
|
52
|
+
const Inspection = __importStar(require("./bridge/inspection"));
|
|
53
|
+
const Session = __importStar(require("./bridge/session"));
|
|
54
|
+
const Debugging = __importStar(require("./bridge/debugging"));
|
|
55
|
+
// Eval scripts (for methods not yet fully extracted)
|
|
56
|
+
const eval_scripts_1 = require("./eval-scripts");
|
|
13
57
|
class CdpBridge {
|
|
14
58
|
client;
|
|
15
59
|
locator;
|
|
16
60
|
snapshotCache;
|
|
61
|
+
logger;
|
|
62
|
+
// Screencast state (kept here until extracted to module)
|
|
63
|
+
screencastFrames = [];
|
|
64
|
+
screencastFrameListener = null;
|
|
65
|
+
mockRoutes = [];
|
|
66
|
+
mockRouteListener = null;
|
|
17
67
|
constructor() {
|
|
18
68
|
this.client = (0, cdp_client_1.getCdpClient)();
|
|
19
69
|
this.locator = new locator_engine_1.LocatorEngine(this.client);
|
|
20
70
|
this.snapshotCache = new page_snapshot_cache_1.PageSnapshotCache(this.client, this.locator);
|
|
71
|
+
this.logger = (0, logger_1.createLogger)("bridge");
|
|
21
72
|
}
|
|
73
|
+
// ─── Speed Control ────────────────────────────────────────────────
|
|
74
|
+
getSpeedMultiplier() {
|
|
75
|
+
return this.client.getSpeedMultiplier();
|
|
76
|
+
}
|
|
77
|
+
setSpeed(m) {
|
|
78
|
+
this.client.setSpeed(m);
|
|
79
|
+
}
|
|
80
|
+
// ─── Main Command Router ──────────────────────────────────────────
|
|
81
|
+
async sendCommand(method, params = {}) {
|
|
82
|
+
const log = this.logger.child(method);
|
|
83
|
+
try {
|
|
84
|
+
switch (method) {
|
|
85
|
+
// ─── Navigation ────────────────────────────────────────
|
|
86
|
+
case "connect":
|
|
87
|
+
return Navigation.connect(this.client, params);
|
|
88
|
+
case "navigate":
|
|
89
|
+
await this.ensureConnected();
|
|
90
|
+
return Navigation.navigate(this.client, params, this.snapshotCache);
|
|
91
|
+
case "smart_navigate":
|
|
92
|
+
await this.ensureConnected();
|
|
93
|
+
return Navigation.smartNavigate(this.client, params, this.snapshotCache, log);
|
|
94
|
+
case "new_tab":
|
|
95
|
+
await this.ensureConnected();
|
|
96
|
+
return Navigation.newTab(this.client, params);
|
|
97
|
+
case "switch_tab":
|
|
98
|
+
await this.ensureConnected();
|
|
99
|
+
return Navigation.switchTab(this.client, params);
|
|
100
|
+
case "close_tab":
|
|
101
|
+
await this.ensureConnected();
|
|
102
|
+
return Navigation.closeTab(this.client, params);
|
|
103
|
+
case "get_tabs":
|
|
104
|
+
await this.ensureConnected();
|
|
105
|
+
return Navigation.getTabs(this.client, params);
|
|
106
|
+
// ─── Inspection ────────────────────────────────────────
|
|
107
|
+
case "browser_status":
|
|
108
|
+
return Inspection.browserStatus(this.client, params);
|
|
109
|
+
case "snapshot_compact":
|
|
110
|
+
await this.ensureConnected();
|
|
111
|
+
return Inspection.snapshotCompact(this.client, this.snapshotCache, params);
|
|
112
|
+
case "list_interactive_elements":
|
|
113
|
+
await this.ensureConnected();
|
|
114
|
+
return Inspection.listInteractiveElements(this.client, this.snapshotCache, this.locator, params);
|
|
115
|
+
case "get_state":
|
|
116
|
+
await this.ensureConnected();
|
|
117
|
+
return Inspection.getState(this.client, this.snapshotCache, this.locator, params);
|
|
118
|
+
case "page_snapshot":
|
|
119
|
+
await this.ensureConnected();
|
|
120
|
+
return Inspection.pageSnapshot(this.client, params);
|
|
121
|
+
case "get_page_text":
|
|
122
|
+
await this.ensureConnected();
|
|
123
|
+
return Inspection.getPageText(this.client, params);
|
|
124
|
+
case "get_tree":
|
|
125
|
+
await this.ensureConnected();
|
|
126
|
+
return Inspection.getAccessibilityTree(this.client);
|
|
127
|
+
case "get_dom_tree":
|
|
128
|
+
case "get_dom_snapshot":
|
|
129
|
+
await this.ensureConnected();
|
|
130
|
+
return Inspection.getDOMTree(this.client);
|
|
131
|
+
case "highlight_elements":
|
|
132
|
+
await this.ensureConnected();
|
|
133
|
+
return Inspection.highlightElements(this.client);
|
|
134
|
+
case "verify_ui_state":
|
|
135
|
+
await this.ensureConnected();
|
|
136
|
+
return Inspection.verifyUIState(this.client, params);
|
|
137
|
+
case "get_computed_style":
|
|
138
|
+
await this.ensureConnected();
|
|
139
|
+
return Inspection.getComputedStyle(this.client, params);
|
|
140
|
+
case "get_event_listeners":
|
|
141
|
+
await this.ensureConnected();
|
|
142
|
+
return Inspection.getEventListeners(this.client, params);
|
|
143
|
+
// ─── Interaction ───────────────────────────────────────
|
|
144
|
+
case "click_by_ref":
|
|
145
|
+
await this.ensureConnected();
|
|
146
|
+
return Interaction.clickByRef(this.client, this.locator, this.snapshotCache, params, log);
|
|
147
|
+
case "click_by_selector":
|
|
148
|
+
await this.ensureConnected();
|
|
149
|
+
return Interaction.clickBySelector(this.client, this.locator, this.snapshotCache, params, log);
|
|
150
|
+
case "fill_by_selector":
|
|
151
|
+
await this.ensureConnected();
|
|
152
|
+
return Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, params, log);
|
|
153
|
+
case "wait_for_selector":
|
|
154
|
+
await this.ensureConnected();
|
|
155
|
+
return this.waitForSelectorCompact(params);
|
|
156
|
+
case "wait_for_text":
|
|
157
|
+
await this.ensureConnected();
|
|
158
|
+
return this.waitForText(params);
|
|
159
|
+
case "press_key":
|
|
160
|
+
case "key_combo":
|
|
161
|
+
await this.ensureConnected();
|
|
162
|
+
return Interaction.pressKey(this.client, this.locator, this.snapshotCache, params, log);
|
|
163
|
+
case "click_text":
|
|
164
|
+
await this.ensureConnected();
|
|
165
|
+
return Interaction.clickText(this.client, this.locator, this.snapshotCache, params, log);
|
|
166
|
+
case "click_role":
|
|
167
|
+
await this.ensureConnected();
|
|
168
|
+
return Interaction.clickRole(this.client, this.locator, this.snapshotCache, params, log);
|
|
169
|
+
case "fill_label":
|
|
170
|
+
await this.ensureConnected();
|
|
171
|
+
return Interaction.fillLabel(this.client, this.locator, this.snapshotCache, params, log);
|
|
172
|
+
case "click":
|
|
173
|
+
await this.ensureConnected();
|
|
174
|
+
return Interaction.click(this.client, this.locator, this.snapshotCache, params, log);
|
|
175
|
+
case "click_element":
|
|
176
|
+
await this.ensureConnected();
|
|
177
|
+
return Interaction.clickElement(this.client, this.locator, this.snapshotCache, params, log);
|
|
178
|
+
case "click_element_by_selector":
|
|
179
|
+
await this.ensureConnected();
|
|
180
|
+
return Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, params, log);
|
|
181
|
+
case "type":
|
|
182
|
+
await this.ensureConnected();
|
|
183
|
+
return Interaction.type(this.client, this.locator, this.snapshotCache, params, log);
|
|
184
|
+
case "fill":
|
|
185
|
+
await this.ensureConnected();
|
|
186
|
+
return Interaction.fillInput(this.client, this.locator, this.snapshotCache, params, log);
|
|
187
|
+
case "select":
|
|
188
|
+
await this.ensureConnected();
|
|
189
|
+
return Interaction.selectOption(this.client, this.locator, this.snapshotCache, params, log);
|
|
190
|
+
case "check":
|
|
191
|
+
await this.ensureConnected();
|
|
192
|
+
return Interaction.checkElement(this.client, this.locator, this.snapshotCache, params, log);
|
|
193
|
+
case "hover":
|
|
194
|
+
await this.ensureConnected();
|
|
195
|
+
return Interaction.hover(this.client, this.locator, this.snapshotCache, params, log);
|
|
196
|
+
case "drag_and_drop":
|
|
197
|
+
await this.ensureConnected();
|
|
198
|
+
return Interaction.dragAndDrop(this.client, this.locator, this.snapshotCache, params, log);
|
|
199
|
+
case "scroll":
|
|
200
|
+
await this.ensureConnected();
|
|
201
|
+
return Interaction.scroll(this.client, this.locator, this.snapshotCache, params, log);
|
|
202
|
+
case "wait":
|
|
203
|
+
await this.ensureConnected();
|
|
204
|
+
return Interaction.wait(this.client, this.locator, this.snapshotCache, params, log);
|
|
205
|
+
case "element_at_point":
|
|
206
|
+
await this.ensureConnected();
|
|
207
|
+
return Interaction.elementAtPoint(this.client, this.locator, this.snapshotCache, params, log);
|
|
208
|
+
case "browser_intent":
|
|
209
|
+
await this.ensureConnected();
|
|
210
|
+
return this.browserIntent(params);
|
|
211
|
+
case "screenshot":
|
|
212
|
+
case "screenshot_region":
|
|
213
|
+
await this.ensureConnected();
|
|
214
|
+
return this.screenshot(params);
|
|
215
|
+
case "evaluate":
|
|
216
|
+
await this.ensureConnected();
|
|
217
|
+
return Debugging.evaluate(this.client, params);
|
|
218
|
+
// ─── Session ───────────────────────────────────────────
|
|
219
|
+
case "save_auth_state":
|
|
220
|
+
await this.ensureConnected();
|
|
221
|
+
return Session.saveAuthState(this.client, params, this.snapshotCache);
|
|
222
|
+
case "load_auth_state":
|
|
223
|
+
await this.ensureConnected();
|
|
224
|
+
return Session.loadAuthState(this.client, params, this.snapshotCache);
|
|
225
|
+
case "get_cookies":
|
|
226
|
+
await this.ensureConnected();
|
|
227
|
+
return Session.getCookies(this.client);
|
|
228
|
+
case "set_cookie":
|
|
229
|
+
await this.ensureConnected();
|
|
230
|
+
return Session.setCookie(this.client, params);
|
|
231
|
+
case "clear_cache":
|
|
232
|
+
await this.ensureConnected();
|
|
233
|
+
return Session.clearCache(this.client);
|
|
234
|
+
// ─── Debugging ──────────────────────────────────────────
|
|
235
|
+
case "get_logs":
|
|
236
|
+
await this.ensureConnected();
|
|
237
|
+
return Debugging.getLogs(this.client, params);
|
|
238
|
+
case "get_network_errors":
|
|
239
|
+
await this.ensureConnected();
|
|
240
|
+
return Debugging.getNetworkErrors(this.client, params);
|
|
241
|
+
case "get_network_traffic":
|
|
242
|
+
await this.ensureConnected();
|
|
243
|
+
return Debugging.getNetworkTraffic(this.client);
|
|
244
|
+
case "get_network_response":
|
|
245
|
+
await this.ensureConnected();
|
|
246
|
+
return Debugging.getNetworkResponse(this.client, params);
|
|
247
|
+
case "get_performance_metrics":
|
|
248
|
+
await this.ensureConnected();
|
|
249
|
+
return Debugging.getPerformanceMetrics(this.client);
|
|
250
|
+
case "start_tracing":
|
|
251
|
+
await this.ensureConnected();
|
|
252
|
+
return Debugging.startTracing(this.client, params);
|
|
253
|
+
case "stop_tracing":
|
|
254
|
+
await this.ensureConnected();
|
|
255
|
+
return Debugging.stopTracing(this.client);
|
|
256
|
+
case "cdp_command":
|
|
257
|
+
await this.ensureConnected();
|
|
258
|
+
return Debugging.cdpCommand(this.client, params);
|
|
259
|
+
case "assert":
|
|
260
|
+
await this.ensureConnected();
|
|
261
|
+
return Debugging.assertCondition(this.client, params);
|
|
262
|
+
case "get_dom_storage":
|
|
263
|
+
await this.ensureConnected();
|
|
264
|
+
return Debugging.getDOMStorage(this.client, params);
|
|
265
|
+
// ─── CAPTCHA ───────────────────────────────────────────
|
|
266
|
+
case "detect_captcha":
|
|
267
|
+
await this.ensureConnected();
|
|
268
|
+
return this.detectCaptcha();
|
|
269
|
+
case "solve_captcha":
|
|
270
|
+
await this.ensureConnected();
|
|
271
|
+
return this.solveCaptchaAction(params);
|
|
272
|
+
// ─── Configuration ─────────────────────────────────────
|
|
273
|
+
case "configure":
|
|
274
|
+
await this.ensureConnected();
|
|
275
|
+
return this.configureBrowser(params);
|
|
276
|
+
case "emulate_network":
|
|
277
|
+
await this.ensureConnected();
|
|
278
|
+
return this.emulateNetworkConditions(params);
|
|
279
|
+
case "set_geolocation":
|
|
280
|
+
await this.ensureConnected();
|
|
281
|
+
return this.setGeolocation(params);
|
|
282
|
+
case "set_timezone":
|
|
283
|
+
await this.ensureConnected();
|
|
284
|
+
return this.setTimezone(params);
|
|
285
|
+
case "print_pdf":
|
|
286
|
+
await this.ensureConnected();
|
|
287
|
+
return this.printPDF(params);
|
|
288
|
+
case "upload_file":
|
|
289
|
+
await this.ensureConnected();
|
|
290
|
+
return this.uploadFile(params);
|
|
291
|
+
case "mock_network_request":
|
|
292
|
+
await this.ensureConnected();
|
|
293
|
+
return this.mockNetworkRequest(params);
|
|
294
|
+
// ─── Screencast ────────────────────────────────────────
|
|
295
|
+
case "start_screencast":
|
|
296
|
+
await this.ensureConnected();
|
|
297
|
+
return this.startScreencast(params);
|
|
298
|
+
case "stop_screencast":
|
|
299
|
+
await this.ensureConnected();
|
|
300
|
+
return this.stopScreencast(params);
|
|
301
|
+
case "record_session":
|
|
302
|
+
await this.ensureConnected();
|
|
303
|
+
return this.recordSession(params);
|
|
304
|
+
case "sample_visual_frames":
|
|
305
|
+
await this.ensureConnected();
|
|
306
|
+
return this.sampleVisualFrames(params);
|
|
307
|
+
case "get_screencast_frames":
|
|
308
|
+
await this.ensureConnected();
|
|
309
|
+
return this.getScreencastFrames(params);
|
|
310
|
+
// ─── Agent APIs ────────────────────────────────────────
|
|
311
|
+
case "agent_action":
|
|
312
|
+
await this.ensureConnected();
|
|
313
|
+
return this.agentAction(params);
|
|
314
|
+
case "observe_and_act":
|
|
315
|
+
await this.ensureConnected();
|
|
316
|
+
return this.observeAndAct(params);
|
|
317
|
+
case "agent_form_fill":
|
|
318
|
+
await this.ensureConnected();
|
|
319
|
+
return this.agentFormFill(params);
|
|
320
|
+
default:
|
|
321
|
+
await this.ensureConnected();
|
|
322
|
+
return this.client.sendCommand(method, params);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
log.error("Command failed", { error: err.message, method });
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// ─── Connection helpers ───────────────────────────────────────────
|
|
22
331
|
async ensureConnected() {
|
|
23
332
|
if (!this.client.isConnected()) {
|
|
24
|
-
// Try to connect to existing Chrome on default port
|
|
25
333
|
try {
|
|
26
334
|
await this.client.connect(9222);
|
|
27
335
|
}
|
|
28
336
|
catch {
|
|
29
|
-
// Chrome not running, launch it
|
|
30
337
|
await this.client.launch({ headless: false });
|
|
31
338
|
}
|
|
32
339
|
}
|
|
33
340
|
}
|
|
34
|
-
async
|
|
35
|
-
|
|
36
|
-
if (type === "type" || type === "fill") {
|
|
37
|
-
delay += 150 + Math.random() * 250; // typing prep delay (300-650ms total)
|
|
38
|
-
}
|
|
39
|
-
else if (type === "click") {
|
|
40
|
-
delay += 50 + Math.random() * 150; // click prep delay (200-550ms total)
|
|
41
|
-
}
|
|
42
|
-
console.error(`[Bridge] Emulating cognitive pause: ${Math.round(delay)}ms for action type: ${type}`);
|
|
43
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
44
|
-
}
|
|
45
|
-
async sendCommand(method, params = {}) {
|
|
46
|
-
// Cognitive pause mapping
|
|
47
|
-
const interactionTypes = {
|
|
48
|
-
click_by_ref: "click",
|
|
49
|
-
click_by_selector: "click",
|
|
50
|
-
fill_by_selector: "fill",
|
|
51
|
-
press_key: "key",
|
|
52
|
-
key_combo: "key",
|
|
53
|
-
click_text: "click",
|
|
54
|
-
click_role: "click",
|
|
55
|
-
fill_label: "fill",
|
|
56
|
-
click: "click",
|
|
57
|
-
click_element: "click",
|
|
58
|
-
click_element_by_selector: "click",
|
|
59
|
-
type: "type",
|
|
60
|
-
fill: "fill",
|
|
61
|
-
select: "click",
|
|
62
|
-
check: "click",
|
|
63
|
-
hover: "click",
|
|
64
|
-
drag_and_drop: "click"
|
|
65
|
-
};
|
|
66
|
-
if (interactionTypes[method]) {
|
|
67
|
-
await this.applyCognitivePause(interactionTypes[method]);
|
|
68
|
-
}
|
|
69
|
-
switch (method) {
|
|
70
|
-
case "browser_status":
|
|
71
|
-
return this.browserStatus(params);
|
|
72
|
-
case "snapshot_compact":
|
|
73
|
-
await this.ensureConnected();
|
|
74
|
-
return this.snapshotCompact(params);
|
|
75
|
-
case "list_interactive_elements":
|
|
76
|
-
await this.ensureConnected();
|
|
77
|
-
return this.listInteractiveElements(params);
|
|
78
|
-
case "click_by_ref":
|
|
79
|
-
await this.ensureConnected();
|
|
80
|
-
return this.clickByRef(params);
|
|
81
|
-
case "click_by_selector":
|
|
82
|
-
await this.ensureConnected();
|
|
83
|
-
return this.clickBySelector(params);
|
|
84
|
-
case "fill_by_selector":
|
|
85
|
-
await this.ensureConnected();
|
|
86
|
-
return this.fillBySelector(params);
|
|
87
|
-
case "wait_for_selector":
|
|
88
|
-
await this.ensureConnected();
|
|
89
|
-
return this.waitForSelectorCompact(params);
|
|
90
|
-
case "wait_for_text":
|
|
91
|
-
await this.ensureConnected();
|
|
92
|
-
return this.waitForText(params);
|
|
93
|
-
case "get_network_errors":
|
|
94
|
-
await this.ensureConnected();
|
|
95
|
-
return this.getNetworkErrors(params);
|
|
96
|
-
case "detect_captcha":
|
|
97
|
-
await this.ensureConnected();
|
|
98
|
-
return this.detectCaptcha();
|
|
99
|
-
case "solve_captcha":
|
|
100
|
-
await this.ensureConnected();
|
|
101
|
-
return this.solveCaptchaAction(params);
|
|
102
|
-
case "browser_intent":
|
|
103
|
-
await this.ensureConnected();
|
|
104
|
-
await this.ensureNoCaptcha("browser_intent");
|
|
105
|
-
return this.browserIntent(params);
|
|
106
|
-
case "get_logs":
|
|
107
|
-
await this.ensureConnected();
|
|
108
|
-
return this.getLogs(params);
|
|
109
|
-
case "press_key":
|
|
110
|
-
case "key_combo":
|
|
111
|
-
await this.ensureConnected();
|
|
112
|
-
await this.ensureNoCaptcha("press_key");
|
|
113
|
-
return this.pressKey(params);
|
|
114
|
-
case "click_text":
|
|
115
|
-
await this.ensureConnected();
|
|
116
|
-
await this.ensureNoCaptcha("click_text");
|
|
117
|
-
return this.clickText(params);
|
|
118
|
-
case "click_role":
|
|
119
|
-
await this.ensureConnected();
|
|
120
|
-
await this.ensureNoCaptcha("click_role");
|
|
121
|
-
return this.clickRole(params);
|
|
122
|
-
case "fill_label":
|
|
123
|
-
await this.ensureConnected();
|
|
124
|
-
await this.ensureNoCaptcha("fill_label");
|
|
125
|
-
return this.fillLabel(params);
|
|
126
|
-
case "element_at_point":
|
|
127
|
-
await this.ensureConnected();
|
|
128
|
-
return this.elementAtPoint(params);
|
|
129
|
-
case "connect":
|
|
130
|
-
return this.connect(params);
|
|
131
|
-
case "get_state":
|
|
132
|
-
await this.ensureConnected();
|
|
133
|
-
return this.getState(params);
|
|
134
|
-
case "navigate":
|
|
135
|
-
await this.ensureConnected();
|
|
136
|
-
return this.navigate(params);
|
|
137
|
-
case "click":
|
|
138
|
-
await this.ensureConnected();
|
|
139
|
-
await this.ensureNoCaptcha("click");
|
|
140
|
-
return this.click(params);
|
|
141
|
-
case "click_element":
|
|
142
|
-
await this.ensureConnected();
|
|
143
|
-
await this.ensureNoCaptcha("click_element");
|
|
144
|
-
return this.clickElement(params);
|
|
145
|
-
case "click_element_by_selector":
|
|
146
|
-
await this.ensureConnected();
|
|
147
|
-
await this.ensureNoCaptcha("click_element_by_selector");
|
|
148
|
-
return this.clickElementBySelector(params);
|
|
149
|
-
case "type":
|
|
150
|
-
await this.ensureConnected();
|
|
151
|
-
await this.ensureNoCaptcha("type");
|
|
152
|
-
return this.type(params);
|
|
153
|
-
case "evaluate":
|
|
154
|
-
await this.ensureConnected();
|
|
155
|
-
return this.evaluate(params);
|
|
156
|
-
case "screenshot":
|
|
157
|
-
case "screenshot_region":
|
|
158
|
-
await this.ensureConnected();
|
|
159
|
-
return this.screenshot(params);
|
|
160
|
-
case "scroll":
|
|
161
|
-
await this.ensureConnected();
|
|
162
|
-
return this.scroll(params);
|
|
163
|
-
case "wait":
|
|
164
|
-
await this.ensureConnected();
|
|
165
|
-
return this.wait(params);
|
|
166
|
-
case "cdp_command":
|
|
167
|
-
await this.ensureConnected();
|
|
168
|
-
return this.cdpCommand(params);
|
|
169
|
-
case "get_dom_snapshot":
|
|
170
|
-
await this.ensureConnected();
|
|
171
|
-
return this.getDomSnapshot(params);
|
|
172
|
-
case "get_tabs":
|
|
173
|
-
await this.ensureConnected();
|
|
174
|
-
return this.getTabs(params);
|
|
175
|
-
case "new_tab":
|
|
176
|
-
await this.ensureConnected();
|
|
177
|
-
return this.newTab(params);
|
|
178
|
-
case "switch_tab":
|
|
179
|
-
await this.ensureConnected();
|
|
180
|
-
return this.switchTab(params);
|
|
181
|
-
case "close_tab":
|
|
182
|
-
await this.ensureConnected();
|
|
183
|
-
return this.closeTab(params);
|
|
184
|
-
case "start_screencast":
|
|
185
|
-
await this.ensureConnected();
|
|
186
|
-
return this.startScreencast(params);
|
|
187
|
-
case "stop_screencast":
|
|
188
|
-
await this.ensureConnected();
|
|
189
|
-
return this.stopScreencast(params);
|
|
190
|
-
case "record_session":
|
|
191
|
-
await this.ensureConnected();
|
|
192
|
-
return this.recordSession(params);
|
|
193
|
-
case "sample_visual_frames":
|
|
194
|
-
await this.ensureConnected();
|
|
195
|
-
return this.sampleVisualFrames(params);
|
|
196
|
-
case "start_tracing":
|
|
197
|
-
await this.ensureConnected();
|
|
198
|
-
return this.startTracing(params);
|
|
199
|
-
case "stop_tracing":
|
|
200
|
-
await this.ensureConnected();
|
|
201
|
-
return this.stopTracing(params);
|
|
202
|
-
case "get_performance_metrics":
|
|
203
|
-
await this.ensureConnected();
|
|
204
|
-
return this.getPerformanceMetrics(params);
|
|
205
|
-
case "hover":
|
|
206
|
-
await this.ensureConnected();
|
|
207
|
-
await this.ensureNoCaptcha("hover");
|
|
208
|
-
return this.hover(params);
|
|
209
|
-
case "drag_and_drop":
|
|
210
|
-
await this.ensureConnected();
|
|
211
|
-
await this.ensureNoCaptcha("drag_and_drop");
|
|
212
|
-
return this.dragAndDrop(params);
|
|
213
|
-
// ==================== MISSING ACT TOOL ACTIONS ====================
|
|
214
|
-
case "fill":
|
|
215
|
-
await this.ensureConnected();
|
|
216
|
-
await this.ensureNoCaptcha("fill");
|
|
217
|
-
return this.fillInput(params);
|
|
218
|
-
case "select":
|
|
219
|
-
await this.ensureConnected();
|
|
220
|
-
await this.ensureNoCaptcha("select");
|
|
221
|
-
return this.selectOption(params);
|
|
222
|
-
case "check":
|
|
223
|
-
await this.ensureConnected();
|
|
224
|
-
await this.ensureNoCaptcha("check");
|
|
225
|
-
return this.checkElement(params);
|
|
226
|
-
case "get_tree":
|
|
227
|
-
await this.ensureConnected();
|
|
228
|
-
return this.getAccessibilityTree(params);
|
|
229
|
-
case "get_dom_tree":
|
|
230
|
-
await this.ensureConnected();
|
|
231
|
-
return this.getDOMTree(params);
|
|
232
|
-
case "assert":
|
|
233
|
-
await this.ensureConnected();
|
|
234
|
-
return this.assertCondition(params);
|
|
235
|
-
case "get_cookies":
|
|
236
|
-
await this.ensureConnected();
|
|
237
|
-
return this.getCookies(params);
|
|
238
|
-
case "set_cookie":
|
|
239
|
-
await this.ensureConnected();
|
|
240
|
-
return this.setCookie(params);
|
|
241
|
-
case "clear_cache":
|
|
242
|
-
await this.ensureConnected();
|
|
243
|
-
return this.clearCache(params);
|
|
244
|
-
case "set_geolocation":
|
|
245
|
-
await this.ensureConnected();
|
|
246
|
-
return this.setGeolocation(params);
|
|
247
|
-
case "set_timezone":
|
|
248
|
-
await this.ensureConnected();
|
|
249
|
-
return this.setTimezone(params);
|
|
250
|
-
case "emulate_network":
|
|
251
|
-
await this.ensureConnected();
|
|
252
|
-
return this.emulateNetworkConditions(params);
|
|
253
|
-
case "print_pdf":
|
|
254
|
-
await this.ensureConnected();
|
|
255
|
-
return this.printPDF(params);
|
|
256
|
-
case "highlight_elements":
|
|
257
|
-
await this.ensureConnected();
|
|
258
|
-
return this.highlightElements(params);
|
|
259
|
-
case "verify_ui_state":
|
|
260
|
-
await this.ensureConnected();
|
|
261
|
-
return this.verifyUIState(params);
|
|
262
|
-
case "get_dom_storage":
|
|
263
|
-
await this.ensureConnected();
|
|
264
|
-
return this.getDOMStorage(params);
|
|
265
|
-
case "get_network_traffic":
|
|
266
|
-
await this.ensureConnected();
|
|
267
|
-
return this.getNetworkTraffic(params);
|
|
268
|
-
case "get_network_response":
|
|
269
|
-
await this.ensureConnected();
|
|
270
|
-
return this.getNetworkResponse(params);
|
|
271
|
-
case "mock_network_request":
|
|
272
|
-
await this.ensureConnected();
|
|
273
|
-
return this.mockNetworkRequest(params);
|
|
274
|
-
case "get_computed_style":
|
|
275
|
-
await this.ensureConnected();
|
|
276
|
-
return this.getComputedStyle(params);
|
|
277
|
-
case "get_event_listeners":
|
|
278
|
-
await this.ensureConnected();
|
|
279
|
-
return this.getEventListeners(params);
|
|
280
|
-
case "get_screencast_frames":
|
|
281
|
-
await this.ensureConnected();
|
|
282
|
-
return this.getScreencastFrames(params);
|
|
283
|
-
case "upload_file":
|
|
284
|
-
await this.ensureConnected();
|
|
285
|
-
await this.ensureNoCaptcha("upload_file");
|
|
286
|
-
return this.uploadFile(params);
|
|
287
|
-
case "configure":
|
|
288
|
-
await this.ensureConnected();
|
|
289
|
-
return this.configureBrowser(params);
|
|
290
|
-
// ==================== AGENT-CENTRIC APIs ====================
|
|
291
|
-
case "agent_action":
|
|
292
|
-
await this.ensureConnected();
|
|
293
|
-
await this.ensureNoCaptcha("agent_action");
|
|
294
|
-
return this.agentAction(params);
|
|
295
|
-
case "smart_navigate":
|
|
296
|
-
await this.ensureConnected();
|
|
297
|
-
return this.smartNavigate(params);
|
|
298
|
-
case "observe_and_act":
|
|
299
|
-
await this.ensureConnected();
|
|
300
|
-
await this.ensureNoCaptcha("observe_and_act");
|
|
301
|
-
return this.observeAndAct(params);
|
|
302
|
-
case "agent_form_fill":
|
|
303
|
-
await this.ensureConnected();
|
|
304
|
-
await this.ensureNoCaptcha("agent_form_fill");
|
|
305
|
-
return this.agentFormFill(params);
|
|
306
|
-
case "page_snapshot":
|
|
307
|
-
await this.ensureConnected();
|
|
308
|
-
return this.pageSnapshot(params);
|
|
309
|
-
default:
|
|
310
|
-
await this.ensureConnected();
|
|
311
|
-
// Try as raw CDP command
|
|
312
|
-
return this.client.sendCommand(method, params);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
async connect(params) {
|
|
316
|
-
const port = params.port || 9222;
|
|
317
|
-
await this.client.connect(port);
|
|
318
|
-
return "Connected to browser";
|
|
319
|
-
}
|
|
320
|
-
async browserStatus(params) {
|
|
321
|
-
const connected = this.client.isConnected();
|
|
322
|
-
const activeTarget = this.client.getActiveTarget();
|
|
323
|
-
let targets;
|
|
324
|
-
if (connected && params.includeTargets) {
|
|
325
|
-
targets = await this.client.getTabs().catch(() => []);
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
connected,
|
|
329
|
-
activeTarget: activeTarget ? {
|
|
330
|
-
id: activeTarget.id,
|
|
331
|
-
type: activeTarget.type,
|
|
332
|
-
title: activeTarget.title,
|
|
333
|
-
url: activeTarget.url
|
|
334
|
-
} : null,
|
|
335
|
-
targets
|
|
336
|
-
};
|
|
341
|
+
async launchBrowser(options) {
|
|
342
|
+
return Session.launchBrowser(this.client, options);
|
|
337
343
|
}
|
|
338
|
-
async
|
|
339
|
-
return this.
|
|
340
|
-
maxElements: params.maxElements ?? 30,
|
|
341
|
-
includeText: params.includeText !== false,
|
|
342
|
-
});
|
|
344
|
+
async killBrowser() {
|
|
345
|
+
return Session.killBrowser(this.client);
|
|
343
346
|
}
|
|
344
|
-
async
|
|
345
|
-
|
|
346
|
-
const snapshot = await this.snapshotCache.compact({
|
|
347
|
-
maxElements,
|
|
348
|
-
includeText: true,
|
|
349
|
-
withOverlay: !!params.withOverlay,
|
|
350
|
-
});
|
|
351
|
-
return {
|
|
352
|
-
count: snapshot.elements.length,
|
|
353
|
-
cache: snapshot.cache,
|
|
354
|
-
elements: snapshot.elements
|
|
355
|
-
};
|
|
347
|
+
async listBrowsers() {
|
|
348
|
+
return Session.listBrowsers(this.client);
|
|
356
349
|
}
|
|
357
|
-
async
|
|
358
|
-
|
|
359
|
-
expression: `
|
|
360
|
-
(function() {
|
|
361
|
-
const max = ${JSON.stringify(maxElements)};
|
|
362
|
-
const includeText = ${JSON.stringify(includeText)};
|
|
363
|
-
const selectors = [
|
|
364
|
-
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
365
|
-
'[onclick]', '[role="button"]', '[role="link"]', '[role="checkbox"]',
|
|
366
|
-
'[tabindex]:not([tabindex="-1"])', 'label', 'summary'
|
|
367
|
-
].join(', ');
|
|
368
|
-
|
|
369
|
-
function cssPath(el) {
|
|
370
|
-
if (el.id) return '#' + CSS.escape(el.id);
|
|
371
|
-
const path = [];
|
|
372
|
-
while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
|
|
373
|
-
let selector = el.nodeName.toLowerCase();
|
|
374
|
-
if (el.classList && el.classList.length) {
|
|
375
|
-
selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
|
|
376
|
-
}
|
|
377
|
-
const parent = el.parentElement;
|
|
378
|
-
if (parent) {
|
|
379
|
-
const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
|
|
380
|
-
if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
|
|
381
|
-
}
|
|
382
|
-
path.unshift(selector);
|
|
383
|
-
el = parent;
|
|
384
|
-
}
|
|
385
|
-
return path.length ? path.join(' > ') : '';
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return Array.from(document.querySelectorAll(selectors)).map((el, index) => {
|
|
389
|
-
const rect = el.getBoundingClientRect();
|
|
390
|
-
const computed = window.getComputedStyle(el);
|
|
391
|
-
const visible = computed.display !== 'none' && computed.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
392
|
-
if (!visible) return null;
|
|
393
|
-
const selector = cssPath(el);
|
|
394
|
-
if (!selector) return null;
|
|
395
|
-
const text = ((el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '') + '').trim().replace(/\\s+/g, ' ').substring(0, 120);
|
|
396
|
-
return {
|
|
397
|
-
ref: 'css:' + selector,
|
|
398
|
-
index: index + 1,
|
|
399
|
-
tag: el.tagName.toLowerCase(),
|
|
400
|
-
role: el.getAttribute('role') || '',
|
|
401
|
-
type: el.getAttribute('type') || '',
|
|
402
|
-
name: el.getAttribute('name') || '',
|
|
403
|
-
text: includeText ? text : undefined,
|
|
404
|
-
bounds: {
|
|
405
|
-
x: Math.round(rect.left),
|
|
406
|
-
y: Math.round(rect.top),
|
|
407
|
-
width: Math.round(rect.width),
|
|
408
|
-
height: Math.round(rect.height)
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
}).filter(Boolean).slice(0, max);
|
|
412
|
-
})()
|
|
413
|
-
`,
|
|
414
|
-
returnByValue: true,
|
|
415
|
-
awaitPromise: true
|
|
416
|
-
});
|
|
417
|
-
const elements = result.result?.value || [];
|
|
418
|
-
if (withOverlay && elements.length > 0) {
|
|
419
|
-
await this.client.getInteractiveElements(true).catch(() => ({ elements: [], somInjected: false }));
|
|
420
|
-
}
|
|
421
|
-
return elements;
|
|
350
|
+
async listBrowserProfiles(browser) {
|
|
351
|
+
return Session.listBrowserProfiles(this.client, browser);
|
|
422
352
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const [x, y] = ref.slice(6).split(",").map(Number);
|
|
432
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
433
|
-
throw new Error(`Invalid point ref: ${ref}`);
|
|
434
|
-
const before = await this.captureActionFacts();
|
|
435
|
-
await this.client.click(x, y);
|
|
436
|
-
this.snapshotCache.invalidate("click_by_ref");
|
|
437
|
-
const after = await this.captureActionFacts();
|
|
438
|
-
return { success: true, ref, facts: this.diffActionFacts(before, after) };
|
|
439
|
-
}
|
|
440
|
-
if (ref.startsWith("@") || ref.startsWith("som:")) {
|
|
441
|
-
const id = ref.replace(/^som:/, "").replace(/^@/, "");
|
|
442
|
-
await this.clickElement({ id });
|
|
443
|
-
return { success: true, ref };
|
|
353
|
+
// ─── Self-healing selector resolution ─────────────────────────────
|
|
354
|
+
async resolveSelector(params) {
|
|
355
|
+
const { originalSelector, text, fuzzyMatch } = params;
|
|
356
|
+
if (originalSelector) {
|
|
357
|
+
const exists = await this.client.evaluate(`!!document.querySelector(${JSON.stringify(originalSelector)})`);
|
|
358
|
+
if (exists) {
|
|
359
|
+
return { selector: originalSelector, method: "exact", confidence: 1.0 };
|
|
360
|
+
}
|
|
444
361
|
}
|
|
445
|
-
throw new Error(`Unsupported element ref: ${ref}`);
|
|
446
|
-
}
|
|
447
|
-
async clickBySelector(params) {
|
|
448
|
-
const selector = params.selector;
|
|
449
|
-
if (!selector)
|
|
450
|
-
throw new Error("selector required");
|
|
451
|
-
const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible !== false, stable: params.stable === true });
|
|
452
|
-
if (!found)
|
|
453
|
-
return { success: false, selector, message: "Selector not found before timeout" };
|
|
454
|
-
const before = await this.captureActionFacts();
|
|
455
|
-
await this.clickElementBySelector({ selector });
|
|
456
|
-
this.snapshotCache.invalidate("click_by_selector");
|
|
457
|
-
const after = await this.captureActionFacts(selector);
|
|
458
|
-
return { success: true, selector, facts: this.diffActionFacts(before, after) };
|
|
459
|
-
}
|
|
460
|
-
async fillBySelector(params) {
|
|
461
|
-
const selector = params.selector;
|
|
462
|
-
const value = params.value ?? "";
|
|
463
|
-
if (!selector)
|
|
464
|
-
throw new Error("selector required");
|
|
465
|
-
const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible !== false, stable: params.stable === true });
|
|
466
|
-
if (!found)
|
|
467
|
-
return { success: false, selector, message: "Selector not found before timeout" };
|
|
468
|
-
await this.client.moveMouseToSelector(selector).catch(() => { });
|
|
469
|
-
const focused = await this.client.evaluate(`
|
|
470
|
-
(function() {
|
|
471
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
472
|
-
if (!el) return false;
|
|
473
|
-
el.focus();
|
|
474
|
-
if ('value' in el) {
|
|
475
|
-
el.value = '';
|
|
476
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
477
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
478
|
-
}
|
|
479
|
-
return true;
|
|
480
|
-
})()
|
|
481
|
-
`);
|
|
482
|
-
if (!focused)
|
|
483
|
-
return { success: false, selector, message: "Selector could not be focused" };
|
|
484
|
-
const before = await this.captureActionFacts(selector);
|
|
485
|
-
await this.client.typeText(String(value));
|
|
486
|
-
this.snapshotCache.invalidate("fill_by_selector");
|
|
487
|
-
const after = await this.captureActionFacts(selector);
|
|
488
|
-
return { success: true, selector, length: String(value).length, facts: this.diffActionFacts(before, after) };
|
|
489
|
-
}
|
|
490
|
-
async waitForSelectorCompact(params) {
|
|
491
|
-
const selector = params.selector;
|
|
492
|
-
if (!selector)
|
|
493
|
-
throw new Error("selector required");
|
|
494
|
-
const found = await this.client.waitForSelector(selector, params.timeout || 5000, { visible: params.visible === true, stable: params.stable === true });
|
|
495
|
-
return { success: found, selector };
|
|
496
|
-
}
|
|
497
|
-
async getLogs(params) {
|
|
498
|
-
const limit = Math.max(1, Math.min(Number(params.limit ?? 50), 100));
|
|
499
|
-
const logs = await this.client.getConsoleLogs(limit);
|
|
500
|
-
return { count: logs.length, logs };
|
|
501
|
-
}
|
|
502
|
-
async pressKey(params) {
|
|
503
|
-
const key = String(params.key || params.value || "");
|
|
504
|
-
if (!key)
|
|
505
|
-
throw new Error("key required");
|
|
506
|
-
const modifiers = Array.isArray(params.modifiers) ? params.modifiers.map(String) : [];
|
|
507
|
-
const before = await this.captureActionFacts();
|
|
508
|
-
await this.client.pressKey(key, modifiers);
|
|
509
|
-
this.snapshotCache.invalidate("press_key");
|
|
510
|
-
const after = await this.captureActionFacts();
|
|
511
|
-
return { success: true, key, modifiers, facts: this.diffActionFacts(before, after) };
|
|
512
|
-
}
|
|
513
|
-
async clickText(params) {
|
|
514
|
-
const resolved = await this.locator.resolve({
|
|
515
|
-
target: params.text || params.value || params.target,
|
|
516
|
-
role: params.role,
|
|
517
|
-
timeout: params.timeout || 5000,
|
|
518
|
-
includeCandidates: params.includeCandidates
|
|
519
|
-
});
|
|
520
|
-
if (!resolved.success)
|
|
521
|
-
return resolved;
|
|
522
|
-
const before = await this.captureActionFacts();
|
|
523
|
-
await this.clickResolvedLocator(resolved.candidate);
|
|
524
|
-
this.snapshotCache.invalidate("click_text");
|
|
525
|
-
const after = await this.captureActionFacts(resolved.selector);
|
|
526
|
-
return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, facts: this.diffActionFacts(before, after) };
|
|
527
|
-
}
|
|
528
|
-
async clickRole(params) {
|
|
529
|
-
const resolved = await this.locator.resolve({
|
|
530
|
-
target: params.name || params.text || params.target || "",
|
|
531
|
-
role: params.role,
|
|
532
|
-
timeout: params.timeout || 5000,
|
|
533
|
-
includeCandidates: params.includeCandidates
|
|
534
|
-
});
|
|
535
|
-
if (!resolved.success)
|
|
536
|
-
return resolved;
|
|
537
|
-
const before = await this.captureActionFacts();
|
|
538
|
-
await this.clickResolvedLocator(resolved.candidate);
|
|
539
|
-
this.snapshotCache.invalidate("click_role");
|
|
540
|
-
const after = await this.captureActionFacts(resolved.selector);
|
|
541
|
-
return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, facts: this.diffActionFacts(before, after) };
|
|
542
|
-
}
|
|
543
|
-
async fillLabel(params) {
|
|
544
362
|
const resolved = await this.locator.resolve({
|
|
545
|
-
target:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const after = await this.captureActionFacts();
|
|
560
|
-
return { success: true, selector: resolved.selector, ref: resolved.ref, matchedBy: resolved.matchedBy, confidence: resolved.confidence, length: String(params.value ?? "").length, facts: this.diffActionFacts(before, after) };
|
|
561
|
-
}
|
|
562
|
-
async clickResolvedLocator(candidate) {
|
|
563
|
-
if (!candidate)
|
|
564
|
-
throw new Error("Resolved locator missing candidate details");
|
|
565
|
-
if (candidate.scope === "document" && candidate.framePath.length === 0 && candidate.shadowDepth === 0 && candidate.selector) {
|
|
566
|
-
await this.clickElementBySelector({ selector: candidate.selector });
|
|
567
|
-
return;
|
|
363
|
+
target: text,
|
|
364
|
+
timeout: params.timeout || 1500,
|
|
365
|
+
includeCandidates: false,
|
|
366
|
+
}).catch(() => null);
|
|
367
|
+
if (resolved?.success && (resolved.selector || resolved.ref)) {
|
|
368
|
+
return {
|
|
369
|
+
selector: resolved.candidate?.scope === "document" &&
|
|
370
|
+
resolved.candidate.framePath.length === 0 &&
|
|
371
|
+
resolved.candidate.shadowDepth === 0
|
|
372
|
+
? resolved.selector || ""
|
|
373
|
+
: resolved.ref || "",
|
|
374
|
+
method: resolved.matchedBy || "locator",
|
|
375
|
+
confidence: resolved.confidence || 0.7,
|
|
376
|
+
};
|
|
568
377
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
throw new Error("x/y or coordinate required");
|
|
576
|
-
return await this.client.evaluate(`
|
|
577
|
-
(function() {
|
|
578
|
-
const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
|
|
579
|
-
if (!el) return { found: false };
|
|
580
|
-
const rect = el.getBoundingClientRect();
|
|
581
|
-
function cssPath(node) {
|
|
582
|
-
if (node.id) return '#' + CSS.escape(node.id);
|
|
583
|
-
const path = [];
|
|
584
|
-
while (node && node.nodeType === Node.ELEMENT_NODE && node !== document.body) {
|
|
585
|
-
let selector = node.nodeName.toLowerCase();
|
|
586
|
-
if (node.classList && node.classList.length) {
|
|
587
|
-
selector += '.' + Array.from(node.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
|
|
588
|
-
}
|
|
589
|
-
const parent = node.parentElement;
|
|
590
|
-
if (parent) {
|
|
591
|
-
const siblings = Array.from(parent.children).filter(child => child.nodeName === node.nodeName);
|
|
592
|
-
if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
|
|
593
|
-
}
|
|
594
|
-
path.unshift(selector);
|
|
595
|
-
node = parent;
|
|
596
|
-
}
|
|
597
|
-
return path.join(' > ');
|
|
598
|
-
}
|
|
599
|
-
return {
|
|
600
|
-
found: true,
|
|
601
|
-
selector: cssPath(el),
|
|
602
|
-
tag: el.tagName.toLowerCase(),
|
|
603
|
-
text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
|
|
604
|
-
role: el.getAttribute('role') || '',
|
|
605
|
-
bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
|
|
606
|
-
};
|
|
607
|
-
})()
|
|
608
|
-
`);
|
|
609
|
-
}
|
|
610
|
-
async waitForText(params) {
|
|
611
|
-
const text = String(params.text || "");
|
|
612
|
-
if (!text)
|
|
613
|
-
throw new Error("text required");
|
|
614
|
-
const timeout = params.timeout || 5000;
|
|
615
|
-
const start = Date.now();
|
|
616
|
-
while (Date.now() - start < timeout) {
|
|
617
|
-
const found = await this.client.evaluate(`
|
|
618
|
-
(document.body && document.body.innerText || '').includes(${JSON.stringify(text)})
|
|
619
|
-
`).catch(() => false);
|
|
620
|
-
if (found)
|
|
621
|
-
return { success: true, text };
|
|
622
|
-
await new Promise(r => setTimeout(r, 200));
|
|
378
|
+
if (fuzzyMatch !== false && text) {
|
|
379
|
+
const { makeFuzzyMatchScript } = await Promise.resolve().then(() => __importStar(require("./eval-scripts")));
|
|
380
|
+
const result = await this.client.evaluate(makeFuzzyMatchScript(text));
|
|
381
|
+
if (result) {
|
|
382
|
+
return { selector: result.selector, method: "fuzzy", confidence: result.confidence };
|
|
383
|
+
}
|
|
623
384
|
}
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
async getNetworkErrors(params) {
|
|
627
|
-
const limit = Math.max(1, Math.min(Number(params.limit ?? 20), 100));
|
|
628
|
-
const errors = (await this.client.getNetworkTraffic())
|
|
629
|
-
.filter((entry) => entry.type === "error" || entry.status >= 400)
|
|
630
|
-
.slice(-limit);
|
|
631
|
-
return {
|
|
632
|
-
count: errors.length,
|
|
633
|
-
errors
|
|
634
|
-
};
|
|
385
|
+
throw new Error(`Could not resolve selector. Original: ${originalSelector}, Text: ${text}`);
|
|
635
386
|
}
|
|
387
|
+
// ─── CAPTCHA ──────────────────────────────────────────────────────
|
|
636
388
|
async detectCaptcha() {
|
|
637
|
-
return await this.client.evaluate(
|
|
638
|
-
(function() {
|
|
639
|
-
const selectors = [
|
|
640
|
-
'iframe[src*="recaptcha"]',
|
|
641
|
-
'iframe[src*="hcaptcha"]',
|
|
642
|
-
'iframe[src*="challenges.cloudflare.com"]',
|
|
643
|
-
'iframe[src*="arkoselabs"]',
|
|
644
|
-
'iframe[src*="funcaptcha"]',
|
|
645
|
-
'[class*="g-recaptcha"]',
|
|
646
|
-
'[class*="h-captcha"]',
|
|
647
|
-
'[data-sitekey]',
|
|
648
|
-
'[id*="captcha" i]',
|
|
649
|
-
'[class*="captcha" i]',
|
|
650
|
-
'[aria-label*="captcha" i]'
|
|
651
|
-
];
|
|
652
|
-
const textPatterns = [
|
|
653
|
-
/captcha/i,
|
|
654
|
-
/i am not a robot/i,
|
|
655
|
-
/verify you are human/i,
|
|
656
|
-
/verify that you are human/i,
|
|
657
|
-
/security check/i,
|
|
658
|
-
/human verification/i,
|
|
659
|
-
/cloudflare.*verify/i
|
|
660
|
-
];
|
|
661
|
-
|
|
662
|
-
function visible(el) {
|
|
663
|
-
const rect = el.getBoundingClientRect();
|
|
664
|
-
const style = window.getComputedStyle(el);
|
|
665
|
-
return style.display !== 'none' &&
|
|
666
|
-
style.visibility !== 'hidden' &&
|
|
667
|
-
rect.width > 0 &&
|
|
668
|
-
rect.height > 0;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const selectorMatches = selectors.flatMap((selector) =>
|
|
672
|
-
Array.from(document.querySelectorAll(selector))
|
|
673
|
-
.filter(visible)
|
|
674
|
-
.slice(0, 5)
|
|
675
|
-
.map((el) => {
|
|
676
|
-
const rect = el.getBoundingClientRect();
|
|
677
|
-
return {
|
|
678
|
-
selector,
|
|
679
|
-
tag: el.tagName.toLowerCase(),
|
|
680
|
-
text: String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' ').substring(0, 160),
|
|
681
|
-
src: el.getAttribute('src') || '',
|
|
682
|
-
bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }
|
|
683
|
-
};
|
|
684
|
-
})
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
const bodyText = String(document.body?.innerText || '').replace(/\\s+/g, ' ').substring(0, 5000);
|
|
688
|
-
const textMatches = textPatterns
|
|
689
|
-
.filter((pattern) => pattern.test(bodyText))
|
|
690
|
-
.map((pattern) => pattern.toString());
|
|
691
|
-
|
|
692
|
-
const detected = selectorMatches.length > 0 || textMatches.length > 0;
|
|
693
|
-
return {
|
|
694
|
-
detected,
|
|
695
|
-
captchaRequired: detected,
|
|
696
|
-
message: detected ? 'CAPTCHA detected. Manual solve required before continuing.' : 'No CAPTCHA detected.',
|
|
697
|
-
matches: selectorMatches,
|
|
698
|
-
textMatches,
|
|
699
|
-
url: window.location.href,
|
|
700
|
-
title: document.title
|
|
701
|
-
};
|
|
702
|
-
})()
|
|
703
|
-
`).catch((error) => ({
|
|
389
|
+
return await this.client.evaluate(eval_scripts_1.CAPTCHA_DETECTION_SCRIPT).catch((error) => ({
|
|
704
390
|
detected: false,
|
|
705
391
|
captchaRequired: false,
|
|
706
|
-
message: `CAPTCHA detection failed: ${error.message}
|
|
392
|
+
message: `CAPTCHA detection failed: ${error.message}`,
|
|
707
393
|
}));
|
|
708
394
|
}
|
|
709
|
-
async ensureNoCaptcha(action) {
|
|
710
|
-
const result = await this.detectCaptcha();
|
|
711
|
-
if (result.detected) {
|
|
712
|
-
const error = new Error("CAPTCHA detected. Manual solve required before continuing.");
|
|
713
|
-
error.captcha = {
|
|
714
|
-
...result,
|
|
715
|
-
blockedAction: action
|
|
716
|
-
};
|
|
717
|
-
throw error;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
395
|
async solveCaptchaAction(params) {
|
|
721
|
-
const pageUrl = params.pageUrl || await this.client.evaluate("window.location.href").catch(() => "");
|
|
396
|
+
const pageUrl = params.pageUrl || (await this.client.evaluate("window.location.href").catch(() => ""));
|
|
722
397
|
const opts = {
|
|
723
398
|
useService: params.useService,
|
|
724
399
|
service: params.service,
|
|
@@ -732,6 +407,54 @@ class CdpBridge {
|
|
|
732
407
|
const mouse = this.client.getMousePosition();
|
|
733
408
|
return await (0, captcha_solver_1.detectAndSolve)(evaluate, sendCommand, pageUrl, mouse, opts);
|
|
734
409
|
}
|
|
410
|
+
// ─── Screenshot ───────────────────────────────────────────────────
|
|
411
|
+
async screenshot(params) {
|
|
412
|
+
const format = params.format === "png" ? "png" : "jpeg";
|
|
413
|
+
const quality = params.quality || 80;
|
|
414
|
+
if (params.x !== undefined) {
|
|
415
|
+
const result = await this.client.sendCommand("Page.captureScreenshot", {
|
|
416
|
+
format,
|
|
417
|
+
quality,
|
|
418
|
+
clip: {
|
|
419
|
+
x: params.x,
|
|
420
|
+
y: params.y,
|
|
421
|
+
width: params.width || 100,
|
|
422
|
+
height: params.height || 100,
|
|
423
|
+
scale: 1,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
return result.data;
|
|
427
|
+
}
|
|
428
|
+
return await this.client.screenshot(format, quality);
|
|
429
|
+
}
|
|
430
|
+
// ─── Wait helpers (compact versions kept here) ────────────────────
|
|
431
|
+
async waitForSelectorCompact(params) {
|
|
432
|
+
const selector = params.selector;
|
|
433
|
+
if (!selector)
|
|
434
|
+
throw new Error("selector required");
|
|
435
|
+
const found = await this.client.waitForSelector(selector, params.timeout || 5000, {
|
|
436
|
+
visible: params.visible === true,
|
|
437
|
+
stable: params.stable === true,
|
|
438
|
+
});
|
|
439
|
+
return { success: found, selector };
|
|
440
|
+
}
|
|
441
|
+
async waitForText(params) {
|
|
442
|
+
const text = String(params.text || "");
|
|
443
|
+
if (!text)
|
|
444
|
+
throw new Error("text required");
|
|
445
|
+
const timeout = params.timeout || 5000;
|
|
446
|
+
const start = Date.now();
|
|
447
|
+
while (Date.now() - start < timeout) {
|
|
448
|
+
const found = await this.client
|
|
449
|
+
.evaluate(`(document.body && document.body.innerText || '').includes(${JSON.stringify(text)})`)
|
|
450
|
+
.catch(() => false);
|
|
451
|
+
if (found)
|
|
452
|
+
return { success: true, text };
|
|
453
|
+
await new Promise((r) => setTimeout(r, 200 * Math.max(0.1, this.getSpeedMultiplier())));
|
|
454
|
+
}
|
|
455
|
+
return { success: false, text, message: "Text not found before timeout" };
|
|
456
|
+
}
|
|
457
|
+
// ─── Browser Intent ───────────────────────────────────────────────
|
|
735
458
|
async browserIntent(params) {
|
|
736
459
|
const intent = String(params.intent || "").toLowerCase();
|
|
737
460
|
const timeout = params.timeout || 7000;
|
|
@@ -739,495 +462,101 @@ class CdpBridge {
|
|
|
739
462
|
const url = params.value || params.target;
|
|
740
463
|
if (!url)
|
|
741
464
|
throw new Error("value or target required for navigate intent");
|
|
742
|
-
await
|
|
743
|
-
return
|
|
465
|
+
await Navigation.navigate(this.client, { url: String(url), timeout }, this.snapshotCache);
|
|
466
|
+
return { success: true, intent, url };
|
|
744
467
|
}
|
|
745
468
|
if (intent === "inspect") {
|
|
746
|
-
const snapshot = await
|
|
747
|
-
|
|
469
|
+
const snapshot = await Inspection.snapshotCompact(this.client, this.snapshotCache, {
|
|
470
|
+
maxElements: params.maxElements ?? 30,
|
|
471
|
+
includeText: true,
|
|
472
|
+
});
|
|
473
|
+
return { success: true, intent, ...snapshot };
|
|
748
474
|
}
|
|
749
475
|
if (intent === "wait_for") {
|
|
750
476
|
const expected = params.value || params.target;
|
|
751
477
|
if (!expected)
|
|
752
478
|
throw new Error("value or target required for wait_for intent");
|
|
753
479
|
const result = await this.waitForText({ text: expected, timeout });
|
|
754
|
-
return
|
|
480
|
+
return { success: result.success, intent, ...result };
|
|
755
481
|
}
|
|
756
482
|
const resolved = await this.locator.resolve({
|
|
757
483
|
target: params.target,
|
|
758
484
|
role: params.role,
|
|
759
485
|
timeout,
|
|
760
|
-
includeCandidates: params.includeCandidates
|
|
486
|
+
includeCandidates: params.includeCandidates,
|
|
761
487
|
});
|
|
762
488
|
if (!resolved.success) {
|
|
763
|
-
return
|
|
489
|
+
return {
|
|
490
|
+
success: false,
|
|
491
|
+
intent,
|
|
764
492
|
message: resolved.message,
|
|
765
|
-
candidates: params.includeCandidates ? resolved.candidates : undefined
|
|
766
|
-
}
|
|
493
|
+
candidates: params.includeCandidates ? resolved.candidates : undefined,
|
|
494
|
+
};
|
|
767
495
|
}
|
|
768
496
|
const selector = resolved.selector;
|
|
497
|
+
const candidate = resolved.candidate;
|
|
769
498
|
if (intent === "click") {
|
|
770
|
-
await
|
|
499
|
+
await Interaction.clickResolvedLocator(this.client, this.locator, this.snapshotCache, candidate);
|
|
771
500
|
}
|
|
772
501
|
else if (intent === "fill") {
|
|
773
|
-
if (
|
|
774
|
-
await
|
|
502
|
+
if (candidate?.scope === "document" && candidate.framePath.length === 0 && candidate.shadowDepth === 0 && selector) {
|
|
503
|
+
await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, {
|
|
504
|
+
selector: selector,
|
|
505
|
+
value: params.value ?? "",
|
|
506
|
+
timeout,
|
|
507
|
+
}, this.logger);
|
|
775
508
|
}
|
|
776
509
|
else {
|
|
777
|
-
await this.locator.focusAndClear(
|
|
510
|
+
await this.locator.focusAndClear(candidate);
|
|
778
511
|
await this.client.typeText(String(params.value ?? ""));
|
|
779
512
|
}
|
|
780
513
|
}
|
|
781
514
|
else if (intent === "select") {
|
|
782
|
-
await
|
|
515
|
+
await Interaction.selectOption(this.client, this.locator, this.snapshotCache, {
|
|
516
|
+
selector: selector,
|
|
517
|
+
value: params.value ?? "",
|
|
518
|
+
}, this.logger);
|
|
783
519
|
}
|
|
784
520
|
else if (intent === "check") {
|
|
785
|
-
await
|
|
521
|
+
await Interaction.checkElement(this.client, this.locator, this.snapshotCache, {
|
|
522
|
+
selector: selector,
|
|
523
|
+
}, this.logger);
|
|
786
524
|
}
|
|
787
525
|
else {
|
|
788
526
|
throw new Error(`Unsupported browser intent: ${intent}`);
|
|
789
527
|
}
|
|
790
|
-
this.snapshotCache.invalidate(`browser_intent:${intent}`);
|
|
791
528
|
let verification = undefined;
|
|
792
529
|
if (params.verify) {
|
|
793
530
|
verification = await this.waitForText({ text: params.verify, timeout }).catch((error) => ({
|
|
794
531
|
success: false,
|
|
795
|
-
error: error.message
|
|
532
|
+
error: error.message,
|
|
796
533
|
}));
|
|
797
534
|
}
|
|
798
|
-
return
|
|
535
|
+
return {
|
|
536
|
+
success: true,
|
|
537
|
+
intent,
|
|
538
|
+
target: resolved.target,
|
|
539
|
+
matchedBy: resolved.matchedBy,
|
|
540
|
+
confidence: resolved.confidence,
|
|
799
541
|
selector,
|
|
800
542
|
ref: resolved.ref || (selector ? `css:${selector}` : undefined),
|
|
801
543
|
verification,
|
|
802
|
-
candidates: params.includeCandidates ? resolved.candidates : undefined
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
intentResult(success, intent, resolved, extra = {}) {
|
|
806
|
-
return {
|
|
807
|
-
success,
|
|
808
|
-
intent,
|
|
809
|
-
target: resolved?.target,
|
|
810
|
-
matchedBy: resolved?.matchedBy,
|
|
811
|
-
confidence: resolved?.confidence,
|
|
812
|
-
...extra
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
async captureActionFacts(selector) {
|
|
816
|
-
return await this.client.evaluate(`
|
|
817
|
-
(function() {
|
|
818
|
-
const selector = ${JSON.stringify(selector || "")};
|
|
819
|
-
const active = document.activeElement;
|
|
820
|
-
const target = selector ? document.querySelector(selector) : active;
|
|
821
|
-
const visibleErrors = Array.from(document.querySelectorAll('[role="alert"], .error, .errors, [aria-invalid="true"]'))
|
|
822
|
-
.filter((el) => {
|
|
823
|
-
const rect = el.getBoundingClientRect();
|
|
824
|
-
const style = window.getComputedStyle(el);
|
|
825
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
826
|
-
})
|
|
827
|
-
.map((el) => String(el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().replace(/\\s+/g, ' '))
|
|
828
|
-
.filter(Boolean)
|
|
829
|
-
.slice(0, 5);
|
|
830
|
-
|
|
831
|
-
function describe(el) {
|
|
832
|
-
if (!el) return null;
|
|
833
|
-
return {
|
|
834
|
-
tag: el.tagName.toLowerCase(),
|
|
835
|
-
id: el.id || '',
|
|
836
|
-
name: el.getAttribute('name') || '',
|
|
837
|
-
role: el.getAttribute('role') || '',
|
|
838
|
-
type: el.getAttribute('type') || '',
|
|
839
|
-
value: 'value' in el ? String(el.value || '') : '',
|
|
840
|
-
checked: 'checked' in el ? !!el.checked : undefined,
|
|
841
|
-
selectedIndex: 'selectedIndex' in el ? el.selectedIndex : undefined,
|
|
842
|
-
text: String(el.innerText || el.textContent || '').trim().replace(/\\s+/g, ' ').substring(0, 160)
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
return {
|
|
847
|
-
url: window.location.href,
|
|
848
|
-
title: document.title,
|
|
849
|
-
readyState: document.readyState,
|
|
850
|
-
focused: describe(active),
|
|
851
|
-
target: describe(target),
|
|
852
|
-
visibleErrors
|
|
853
|
-
};
|
|
854
|
-
})()
|
|
855
|
-
`).catch(() => ({}));
|
|
856
|
-
}
|
|
857
|
-
diffActionFacts(before, after) {
|
|
858
|
-
return {
|
|
859
|
-
urlChanged: before?.url !== after?.url,
|
|
860
|
-
titleChanged: before?.title !== after?.title,
|
|
861
|
-
focused: after?.focused,
|
|
862
|
-
target: after?.target,
|
|
863
|
-
valueChanged: before?.target?.value !== after?.target?.value,
|
|
864
|
-
checkedChanged: before?.target?.checked !== after?.target?.checked,
|
|
865
|
-
selectedIndexChanged: before?.target?.selectedIndex !== after?.target?.selectedIndex,
|
|
866
|
-
visibleErrors: after?.visibleErrors || []
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
async resolveNaturalTarget(params) {
|
|
870
|
-
const target = String(params.target || "").trim();
|
|
871
|
-
const role = params.role ? String(params.role).toLowerCase() : "";
|
|
872
|
-
const timeout = params.timeout || 7000;
|
|
873
|
-
const start = Date.now();
|
|
874
|
-
if (!target && !role) {
|
|
875
|
-
return { success: false, message: "target or role required" };
|
|
876
|
-
}
|
|
877
|
-
while (Date.now() - start < timeout) {
|
|
878
|
-
const result = await this.client.sendCommand("Runtime.evaluate", {
|
|
879
|
-
expression: `
|
|
880
|
-
(function() {
|
|
881
|
-
const target = ${JSON.stringify(target)};
|
|
882
|
-
const targetLower = target.toLowerCase();
|
|
883
|
-
const roleHint = ${JSON.stringify(role)};
|
|
884
|
-
const selectors = [
|
|
885
|
-
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
886
|
-
'[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary'
|
|
887
|
-
].join(', ');
|
|
888
|
-
|
|
889
|
-
function cssPath(el) {
|
|
890
|
-
if (el.id) return '#' + CSS.escape(el.id);
|
|
891
|
-
const path = [];
|
|
892
|
-
while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
|
|
893
|
-
let selector = el.nodeName.toLowerCase();
|
|
894
|
-
if (el.classList && el.classList.length) {
|
|
895
|
-
selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
|
|
896
|
-
}
|
|
897
|
-
const parent = el.parentElement;
|
|
898
|
-
if (parent) {
|
|
899
|
-
const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
|
|
900
|
-
if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
|
|
901
|
-
}
|
|
902
|
-
path.unshift(selector);
|
|
903
|
-
el = parent;
|
|
904
|
-
}
|
|
905
|
-
return path.length ? path.join(' > ') : '';
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function visible(el) {
|
|
909
|
-
const rect = el.getBoundingClientRect();
|
|
910
|
-
const computed = window.getComputedStyle(el);
|
|
911
|
-
return computed.display !== 'none' &&
|
|
912
|
-
computed.visibility !== 'hidden' &&
|
|
913
|
-
computed.opacity !== '0' &&
|
|
914
|
-
rect.width > 0 &&
|
|
915
|
-
rect.height > 0;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function inferRole(el) {
|
|
919
|
-
const explicit = (el.getAttribute('role') || '').toLowerCase();
|
|
920
|
-
if (explicit) return explicit;
|
|
921
|
-
const tag = el.tagName.toLowerCase();
|
|
922
|
-
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
923
|
-
if (tag === 'button' || type === 'button' || type === 'submit') return 'button';
|
|
924
|
-
if (tag === 'a') return 'link';
|
|
925
|
-
if (tag === 'textarea') return 'textbox';
|
|
926
|
-
if (tag === 'select') return 'combobox';
|
|
927
|
-
if (tag === 'input' && ['checkbox', 'radio'].includes(type)) return type;
|
|
928
|
-
if (tag === 'input') return 'textbox';
|
|
929
|
-
return tag;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function labelFor(el) {
|
|
933
|
-
if (el.id) {
|
|
934
|
-
const label = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
935
|
-
if (label) return label.innerText || label.textContent || '';
|
|
936
|
-
}
|
|
937
|
-
const wrappingLabel = el.closest('label');
|
|
938
|
-
return wrappingLabel ? (wrappingLabel.innerText || wrappingLabel.textContent || '') : '';
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function norm(value) {
|
|
942
|
-
return String(value || '').trim().replace(/\\s+/g, ' ');
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function scoreField(value, weightExact, weightIncludes) {
|
|
946
|
-
const text = norm(value);
|
|
947
|
-
const lower = text.toLowerCase();
|
|
948
|
-
if (!targetLower) return { score: 0, by: '' };
|
|
949
|
-
if (lower === targetLower) return { score: weightExact, by: 'exact' };
|
|
950
|
-
if (lower.includes(targetLower)) return { score: weightIncludes, by: 'contains' };
|
|
951
|
-
if (targetLower.includes(lower) && lower.length >= 3) return { score: Math.max(1, weightIncludes - 1), by: 'contained_by_target' };
|
|
952
|
-
return { score: 0, by: '' };
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const candidates = Array.from(document.querySelectorAll(selectors)).map((el) => {
|
|
956
|
-
if (!visible(el)) return null;
|
|
957
|
-
const selector = cssPath(el);
|
|
958
|
-
if (!selector) return null;
|
|
959
|
-
|
|
960
|
-
const inferredRole = inferRole(el);
|
|
961
|
-
const fields = [
|
|
962
|
-
['selector', selector, 12, 10],
|
|
963
|
-
['aria-label', el.getAttribute('aria-label'), 11, 9],
|
|
964
|
-
['label', labelFor(el), 11, 9],
|
|
965
|
-
['placeholder', el.getAttribute('placeholder'), 10, 8],
|
|
966
|
-
['name', el.getAttribute('name'), 9, 7],
|
|
967
|
-
['text', el.innerText || el.textContent, 8, 6],
|
|
968
|
-
['value', el.getAttribute('value'), 7, 5],
|
|
969
|
-
['title', el.getAttribute('title'), 6, 4]
|
|
970
|
-
];
|
|
971
|
-
|
|
972
|
-
let score = 0;
|
|
973
|
-
let matchedBy = '';
|
|
974
|
-
for (const [field, value, exact, includes] of fields) {
|
|
975
|
-
const match = scoreField(value, exact, includes);
|
|
976
|
-
if (match.score > score) {
|
|
977
|
-
score = match.score;
|
|
978
|
-
matchedBy = field + ':' + match.by;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (roleHint) {
|
|
983
|
-
if (inferredRole === roleHint) score += 4;
|
|
984
|
-
else if (roleHint === 'textbox' && ['input', 'textarea'].includes(el.tagName.toLowerCase())) score += 3;
|
|
985
|
-
else score -= 2;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
const rect = el.getBoundingClientRect();
|
|
989
|
-
return {
|
|
990
|
-
selector,
|
|
991
|
-
role: inferredRole,
|
|
992
|
-
tag: el.tagName.toLowerCase(),
|
|
993
|
-
type: el.getAttribute('type') || '',
|
|
994
|
-
text: norm(el.innerText || el.textContent || el.getAttribute('aria-label') || el.getAttribute('placeholder')).substring(0, 120),
|
|
995
|
-
matchedBy,
|
|
996
|
-
score,
|
|
997
|
-
bounds: {
|
|
998
|
-
x: Math.round(rect.left),
|
|
999
|
-
y: Math.round(rect.top),
|
|
1000
|
-
width: Math.round(rect.width),
|
|
1001
|
-
height: Math.round(rect.height)
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
}).filter(Boolean).sort((a, b) => b.score - a.score);
|
|
1005
|
-
|
|
1006
|
-
return candidates;
|
|
1007
|
-
})()
|
|
1008
|
-
`,
|
|
1009
|
-
returnByValue: true,
|
|
1010
|
-
awaitPromise: true
|
|
1011
|
-
});
|
|
1012
|
-
const candidates = result.result?.value || [];
|
|
1013
|
-
const best = candidates[0];
|
|
1014
|
-
if (best && best.score > 0) {
|
|
1015
|
-
return {
|
|
1016
|
-
success: true,
|
|
1017
|
-
target,
|
|
1018
|
-
selector: best.selector,
|
|
1019
|
-
matchedBy: best.matchedBy,
|
|
1020
|
-
confidence: Math.min(1, best.score / 16),
|
|
1021
|
-
candidates: params.includeCandidates ? candidates.slice(0, 10) : undefined
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
await new Promise(r => setTimeout(r, 200));
|
|
1025
|
-
}
|
|
1026
|
-
return { success: false, target, message: "No matching visible element found" };
|
|
1027
|
-
}
|
|
1028
|
-
async getState(params) {
|
|
1029
|
-
const includeScreenshot = params.screenshot === true;
|
|
1030
|
-
const includeDomSnapshot = params.domSnapshot === true || params.includeDOMSnapshot === true;
|
|
1031
|
-
const includeElements = params.elements !== false;
|
|
1032
|
-
const includeSoM = params.som === true || params.withOverlay === true;
|
|
1033
|
-
const includeTabs = params.tabs === true;
|
|
1034
|
-
const compact = includeElements
|
|
1035
|
-
? await this.snapshotCache.compact({ maxElements: 200, includeText: true, withOverlay: includeSoM })
|
|
1036
|
-
: await this.snapshotCache.compact({ maxElements: 0, includeText: false });
|
|
1037
|
-
const [screenshot, domSnapshot, tabs] = await Promise.all([
|
|
1038
|
-
includeScreenshot ? this.client.screenshot(params.format, params.quality).catch(() => null) : Promise.resolve(null),
|
|
1039
|
-
includeDomSnapshot ? this.client.getDOMSnapshot().catch(() => null) : Promise.resolve(null),
|
|
1040
|
-
includeTabs ? this.client.getTabs().catch(() => []) : Promise.resolve([]),
|
|
1041
|
-
]);
|
|
1042
|
-
if (includeSoM) {
|
|
1043
|
-
await this.client.removeSoMOverlay().catch(() => { });
|
|
1044
|
-
}
|
|
1045
|
-
return {
|
|
1046
|
-
title: compact.title,
|
|
1047
|
-
url: compact.url,
|
|
1048
|
-
screenshot,
|
|
1049
|
-
domSnapshot,
|
|
1050
|
-
elements: includeElements ? compact.elements : [],
|
|
1051
|
-
somInjected: includeSoM,
|
|
1052
|
-
cache: compact.cache,
|
|
1053
|
-
tabs,
|
|
544
|
+
candidates: params.includeCandidates ? resolved.candidates : undefined,
|
|
1054
545
|
};
|
|
1055
546
|
}
|
|
1056
|
-
|
|
1057
|
-
await this.client.navigateAndWait(params.url, params.timeout || 10000);
|
|
1058
|
-
this.snapshotCache.invalidate("navigate");
|
|
1059
|
-
return "Navigated";
|
|
1060
|
-
}
|
|
1061
|
-
async waitForPageSettled(timeout = 10000) {
|
|
1062
|
-
try {
|
|
1063
|
-
await this.client.waitForNavigation(Math.min(timeout, 10000));
|
|
1064
|
-
}
|
|
1065
|
-
catch {
|
|
1066
|
-
await this.client.waitForNetworkIdle(300, Math.min(timeout, 2500)).catch(() => { });
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
async click(params) {
|
|
1070
|
-
const x = params.x || params.coordinate?.split(',')[0] || 100;
|
|
1071
|
-
const y = params.y || params.coordinate?.split(',')[1] || 100;
|
|
1072
|
-
await this.client.click(x, y);
|
|
1073
|
-
this.snapshotCache.invalidate("click");
|
|
1074
|
-
return "Clicked";
|
|
1075
|
-
}
|
|
1076
|
-
async clickElement(params) {
|
|
1077
|
-
// Click by element ID (from SoM) - resolves ID to coordinates
|
|
1078
|
-
if (params.id !== undefined) {
|
|
1079
|
-
const result = await this.client.evaluate(`
|
|
1080
|
-
(function() {
|
|
1081
|
-
const targetId = Number(${JSON.stringify(String(params.id).replace(/@/g, ''))});
|
|
1082
|
-
const selectors = [
|
|
1083
|
-
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
1084
|
-
'[onclick]', '[role="button"]', '[role="link"]', '[role="checkbox"]',
|
|
1085
|
-
'[tabindex]:not([tabindex="-1"])', 'label', 'summary'
|
|
1086
|
-
].join(', ');
|
|
1087
|
-
const elements = Array.from(document.querySelectorAll(selectors)).filter((el) => {
|
|
1088
|
-
const rect = el.getBoundingClientRect();
|
|
1089
|
-
const computed = window.getComputedStyle(el);
|
|
1090
|
-
return computed.display !== 'none' &&
|
|
1091
|
-
computed.visibility !== 'hidden' &&
|
|
1092
|
-
rect.width > 0 &&
|
|
1093
|
-
rect.height > 0;
|
|
1094
|
-
});
|
|
1095
|
-
const el = elements[targetId - 1];
|
|
1096
|
-
if (el) {
|
|
1097
|
-
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
1098
|
-
const rect = el.getBoundingClientRect();
|
|
1099
|
-
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, w: rect.width };
|
|
1100
|
-
}
|
|
1101
|
-
return null;
|
|
1102
|
-
})()
|
|
1103
|
-
`);
|
|
1104
|
-
if (result) {
|
|
1105
|
-
await this.client.click(result.x, result.y, params.button, result.w);
|
|
1106
|
-
this.snapshotCache.invalidate("click_element");
|
|
1107
|
-
return `Clicked element @${params.id}`;
|
|
1108
|
-
}
|
|
1109
|
-
// Fallback: try to find element by selector or text
|
|
1110
|
-
if (params.selector) {
|
|
1111
|
-
return this.clickElementBySelector({ selector: params.selector });
|
|
1112
|
-
}
|
|
1113
|
-
if (params.text) {
|
|
1114
|
-
return this.clickElementByText({ text: params.text });
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
// Fallback to coordinate click
|
|
1118
|
-
if (params.x !== undefined && params.y !== undefined) {
|
|
1119
|
-
await this.client.click(params.x, params.y);
|
|
1120
|
-
this.snapshotCache.invalidate("click_element");
|
|
1121
|
-
return "Clicked at coordinates";
|
|
1122
|
-
}
|
|
1123
|
-
throw new Error("Element not found: no valid id, selector, text, or coordinates provided");
|
|
1124
|
-
}
|
|
1125
|
-
async clickElementByText(params) {
|
|
1126
|
-
const result = await this.locator.resolve({ target: params.text, timeout: params.timeout || 5000 });
|
|
1127
|
-
if (result.success && result.candidate) {
|
|
1128
|
-
await this.clickResolvedLocator(result.candidate);
|
|
1129
|
-
this.snapshotCache.invalidate("click_element_by_text");
|
|
1130
|
-
return `Clicked element with text: ${params.text}`;
|
|
1131
|
-
}
|
|
1132
|
-
throw new Error(`Element with text not found: ${params.text}`);
|
|
1133
|
-
}
|
|
1134
|
-
async clickElementBySelector(params) {
|
|
1135
|
-
const selector = params.selector;
|
|
1136
|
-
if (!selector)
|
|
1137
|
-
throw new Error("Selector required");
|
|
1138
|
-
if (String(selector).startsWith("point:")) {
|
|
1139
|
-
const [x, y] = String(selector).slice(6).split(",").map(Number);
|
|
1140
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
1141
|
-
throw new Error(`Invalid point selector: ${selector}`);
|
|
1142
|
-
await this.client.click(x, y);
|
|
1143
|
-
this.snapshotCache.invalidate("click_element_by_selector");
|
|
1144
|
-
return "Clicked element by point";
|
|
1145
|
-
}
|
|
1146
|
-
// Get element bounds via CDP
|
|
1147
|
-
const result = await this.client.sendCommand("Runtime.evaluate", {
|
|
1148
|
-
expression: `
|
|
1149
|
-
(function() {
|
|
1150
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1151
|
-
if (!el) return null;
|
|
1152
|
-
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
1153
|
-
const rect = el.getBoundingClientRect();
|
|
1154
|
-
const computed = window.getComputedStyle(el);
|
|
1155
|
-
if (computed.display === 'none' || computed.visibility === 'hidden' || rect.width === 0 || rect.height === 0) return null;
|
|
1156
|
-
return { x: rect.left + rect.width/2, y: rect.top + rect.height/2, w: rect.width };
|
|
1157
|
-
})()
|
|
1158
|
-
`,
|
|
1159
|
-
returnByValue: true,
|
|
1160
|
-
});
|
|
1161
|
-
if (result.result?.value) {
|
|
1162
|
-
const { x, y, w } = result.result.value;
|
|
1163
|
-
await this.client.click(x, y, params.button, w);
|
|
1164
|
-
this.snapshotCache.invalidate("click_element_by_selector");
|
|
1165
|
-
return "Clicked element by selector";
|
|
1166
|
-
}
|
|
1167
|
-
throw new Error(`Element not found: ${selector}`);
|
|
1168
|
-
}
|
|
1169
|
-
async type(params) {
|
|
1170
|
-
const text = params.text || params.value || "";
|
|
1171
|
-
await this.client.typeText(text);
|
|
1172
|
-
return "Typed text";
|
|
1173
|
-
}
|
|
1174
|
-
async evaluate(params) {
|
|
1175
|
-
return await this.client.evaluate(params.script);
|
|
1176
|
-
}
|
|
1177
|
-
async screenshot(params) {
|
|
1178
|
-
const format = params.format === "png" ? "png" : "jpeg";
|
|
1179
|
-
const quality = params.quality || 80;
|
|
1180
|
-
if (params.x !== undefined) {
|
|
1181
|
-
// Region screenshot - use CDP clipping
|
|
1182
|
-
const result = await this.client.sendCommand("Page.captureScreenshot", {
|
|
1183
|
-
format,
|
|
1184
|
-
quality,
|
|
1185
|
-
clip: {
|
|
1186
|
-
x: params.x,
|
|
1187
|
-
y: params.y,
|
|
1188
|
-
width: params.width || 100,
|
|
1189
|
-
height: params.height || 100,
|
|
1190
|
-
scale: 1,
|
|
1191
|
-
},
|
|
1192
|
-
});
|
|
1193
|
-
return result.data;
|
|
1194
|
-
}
|
|
1195
|
-
return await this.client.screenshot(format, quality);
|
|
1196
|
-
}
|
|
1197
|
-
async scroll(params) {
|
|
1198
|
-
const x = params.x || 0;
|
|
1199
|
-
const y = params.y || 0;
|
|
1200
|
-
const originX = params.originX ?? params.mouseX ?? params.options?.originX;
|
|
1201
|
-
const originY = params.originY ?? params.mouseY ?? params.options?.originY;
|
|
1202
|
-
await this.client.wheel(x, y, originX, originY);
|
|
1203
|
-
return "Scrolled";
|
|
1204
|
-
}
|
|
1205
|
-
async wait(params) {
|
|
1206
|
-
const ms = params.ms || params.timeout || 1000;
|
|
1207
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
1208
|
-
return "Waited";
|
|
1209
|
-
}
|
|
1210
|
-
async cdpCommand(params) {
|
|
1211
|
-
return await this.client.sendCommand(params.command, params.args || {});
|
|
1212
|
-
}
|
|
1213
|
-
async getDomSnapshot(params) {
|
|
1214
|
-
return await this.client.getDOMSnapshot();
|
|
1215
|
-
}
|
|
1216
|
-
// ==================== AGENT-CENTRIC APIs ====================
|
|
547
|
+
// ─── Agent APIs ───────────────────────────────────────────────────
|
|
1217
548
|
async agentAction(params) {
|
|
1218
549
|
const { action, target, verify, waitFor, timeout } = params;
|
|
1219
550
|
const timeoutMs = timeout || 10000;
|
|
1220
551
|
try {
|
|
1221
|
-
// Execute action with proper element resolution
|
|
1222
552
|
switch (action) {
|
|
1223
553
|
case "click":
|
|
1224
554
|
if (target.id) {
|
|
1225
|
-
await
|
|
555
|
+
await Interaction.clickElement(this.client, this.locator, this.snapshotCache, { id: target.id, button: target.button }, this.logger);
|
|
1226
556
|
}
|
|
1227
557
|
else if (target.selector) {
|
|
1228
|
-
// Wait for selector if needed
|
|
1229
558
|
await this.client.waitForSelector(target.selector, 5000);
|
|
1230
|
-
await
|
|
559
|
+
await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: target.selector, button: target.button }, this.logger);
|
|
1231
560
|
}
|
|
1232
561
|
else if (target.x !== undefined) {
|
|
1233
562
|
await this.client.click(target.x, target.y, target.button);
|
|
@@ -1237,28 +566,19 @@ class CdpBridge {
|
|
|
1237
566
|
if (target.selector) {
|
|
1238
567
|
await this.client.waitForSelector(target.selector, 5000);
|
|
1239
568
|
await this.client.moveMouseToSelector(target.selector).catch(() => { });
|
|
1240
|
-
await this.client.evaluate(`
|
|
1241
|
-
(function() {
|
|
1242
|
-
const el = document.querySelector(${JSON.stringify(target.selector)});
|
|
1243
|
-
if (el) { el.value = ''; el.focus(); }
|
|
1244
|
-
})()
|
|
1245
|
-
`);
|
|
569
|
+
await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(target.selector)});if(el){el.value='';el.focus();}})()`);
|
|
1246
570
|
}
|
|
1247
571
|
await this.client.typeText(target.text || target.value || "");
|
|
1248
572
|
break;
|
|
1249
573
|
case "scroll":
|
|
1250
574
|
await this.client.sendCommand("Input.dispatchMouseWheel", {
|
|
1251
575
|
x: target.x || 0, y: target.y || 0,
|
|
1252
|
-
deltaX: target.deltaX || 0, deltaY: target.deltaY || target.y || 0
|
|
576
|
+
deltaX: target.deltaX || 0, deltaY: target.deltaY || target.y || 0,
|
|
1253
577
|
});
|
|
1254
578
|
break;
|
|
1255
579
|
case "key_press":
|
|
1256
|
-
await this.client.sendCommand("Input.dispatchKeyEvent", {
|
|
1257
|
-
|
|
1258
|
-
});
|
|
1259
|
-
await this.client.sendCommand("Input.dispatchKeyEvent", {
|
|
1260
|
-
type: "keyUp", text: target.key, key: target.key
|
|
1261
|
-
});
|
|
580
|
+
await this.client.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: target.key, key: target.key });
|
|
581
|
+
await this.client.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", text: target.key, key: target.key });
|
|
1262
582
|
break;
|
|
1263
583
|
case "hover":
|
|
1264
584
|
await this.client.moveMouse(target.x || 0, target.y || 0);
|
|
@@ -1274,146 +594,56 @@ class CdpBridge {
|
|
|
1274
594
|
await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x: ex, y: ey, button: "left", clickCount: 1 });
|
|
1275
595
|
break;
|
|
1276
596
|
}
|
|
1277
|
-
// Wait for condition with proper waiting mechanisms
|
|
1278
597
|
if (waitFor) {
|
|
1279
598
|
if (waitFor.type === "network_idle") {
|
|
1280
599
|
await this.client.waitForNetworkIdle(500, waitFor.timeout || 3000);
|
|
1281
600
|
}
|
|
1282
|
-
else if (waitFor.type === "element") {
|
|
1283
|
-
|
|
1284
|
-
await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
|
|
1285
|
-
}
|
|
1286
|
-
else {
|
|
1287
|
-
await new Promise(r => setTimeout(r, waitFor.timeout || 3000));
|
|
1288
|
-
}
|
|
601
|
+
else if (waitFor.type === "element" && waitFor.selector) {
|
|
602
|
+
await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
|
|
1289
603
|
}
|
|
1290
604
|
else if (waitFor.type === "navigation") {
|
|
1291
|
-
|
|
1292
|
-
await this.client.waitForNavigation(waitFor.timeout || 10000);
|
|
1293
|
-
}
|
|
1294
|
-
catch {
|
|
1295
|
-
// Navigation might have already completed
|
|
1296
|
-
}
|
|
605
|
+
await this.client.waitForNavigation(waitFor.timeout || 10000).catch(() => { });
|
|
1297
606
|
}
|
|
1298
607
|
}
|
|
1299
608
|
else {
|
|
1300
|
-
|
|
1301
|
-
await new Promise(r => setTimeout(r, 500));
|
|
609
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1302
610
|
}
|
|
1303
|
-
// Verify if requested
|
|
1304
611
|
let verification = null;
|
|
1305
612
|
if (verify) {
|
|
1306
613
|
if (verify.type === "element_exists") {
|
|
1307
|
-
|
|
1308
|
-
(function() {
|
|
1309
|
-
const el = document.querySelector(${JSON.stringify(verify.selector)});
|
|
1310
|
-
return { success: !!el, exists: !!el, message: el ? "Element exists" : "Element not found" };
|
|
1311
|
-
})()
|
|
1312
|
-
`);
|
|
1313
|
-
verification = res;
|
|
614
|
+
verification = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(verify.selector)});return {success:!!el,exists:!!el,message:el?"Element exists":"Element not found"};})()`);
|
|
1314
615
|
}
|
|
1315
616
|
else if (verify.type === "element_contains_text") {
|
|
1316
|
-
|
|
1317
|
-
(function() {
|
|
1318
|
-
const el = document.querySelector(${JSON.stringify(verify.selector)});
|
|
1319
|
-
if (!el) return { success: false, message: "Element not found" };
|
|
1320
|
-
const text = (el.innerText || el.textContent || "").trim();
|
|
1321
|
-
const matches = text.includes(${JSON.stringify(verify.expectedText || verify.text || "")});
|
|
1322
|
-
return { success: matches, text, message: matches ? "Verified" : "Text mismatch" };
|
|
1323
|
-
})()
|
|
1324
|
-
`);
|
|
1325
|
-
verification = res;
|
|
617
|
+
verification = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(verify.selector)});if(!el)return {success:false,message:"Element not found"};var t=(el.innerText||el.textContent||"").trim();var m=t.indexOf(${JSON.stringify(verify.expectedText || verify.text || "")})>=0;return {success:m,text:t,message:m?"Verified":"Text mismatch"};})()`);
|
|
1326
618
|
}
|
|
1327
619
|
else if (verify.selector) {
|
|
1328
|
-
|
|
1329
|
-
const res = await this.client.evaluate(`
|
|
1330
|
-
(function() {
|
|
1331
|
-
return !!document.querySelector(${JSON.stringify(verify.selector)});
|
|
1332
|
-
})()
|
|
1333
|
-
`);
|
|
620
|
+
const res = await this.client.evaluate(`!!document.querySelector(${JSON.stringify(verify.selector)})`);
|
|
1334
621
|
verification = { success: !!res, selector: verify.selector };
|
|
1335
622
|
}
|
|
1336
623
|
}
|
|
1337
|
-
// Get screenshot
|
|
1338
624
|
const screenshot = params.screenshot === true ? await this.client.screenshot("jpeg", 70).catch(() => null) : null;
|
|
1339
625
|
const url = await this.client.evaluate("window.location.href").catch(() => "Unknown");
|
|
1340
626
|
const title = await this.client.evaluate("document.title").catch(() => "Unknown");
|
|
1341
|
-
return {
|
|
1342
|
-
success: true,
|
|
1343
|
-
action: `${action} completed`,
|
|
1344
|
-
verification,
|
|
1345
|
-
screenshot,
|
|
1346
|
-
url,
|
|
1347
|
-
title
|
|
1348
|
-
};
|
|
627
|
+
return { success: true, action: `${action} completed`, verification, screenshot, url, title };
|
|
1349
628
|
}
|
|
1350
629
|
catch (e) {
|
|
1351
630
|
return { success: false, error: e.message };
|
|
1352
631
|
}
|
|
1353
632
|
}
|
|
1354
|
-
async
|
|
1355
|
-
const {
|
|
1356
|
-
const timeoutMs = timeout || 30000;
|
|
633
|
+
async observeAndAct(params) {
|
|
634
|
+
const { action, observe, returnScreenshot } = params;
|
|
1357
635
|
try {
|
|
1358
|
-
await
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
'[aria-label*="close" i]', '[aria-label*="dismiss" i]',
|
|
1366
|
-
'.close', '.dismiss', '.modal-close',
|
|
1367
|
-
'button[class*="close"]', '[data-dismiss="modal"]'
|
|
1368
|
-
];
|
|
1369
|
-
for (const sel of selectors) {
|
|
1370
|
-
const el = document.querySelector(sel);
|
|
1371
|
-
if (el && el.offsetParent !== null) {
|
|
1372
|
-
el.click();
|
|
1373
|
-
return true;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
return false;
|
|
1377
|
-
})()
|
|
1378
|
-
`).catch(() => { });
|
|
636
|
+
const [beforeFacts, beforeScreenshot] = await Promise.all([
|
|
637
|
+
Interaction.captureActionFacts(this.client, action?.selector),
|
|
638
|
+
returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
|
|
639
|
+
]);
|
|
640
|
+
if (action.type === "click" && action.selector) {
|
|
641
|
+
await this.client.waitForSelector(action.selector, 5000, { visible: true, stable: true });
|
|
642
|
+
await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: action.selector }, this.logger);
|
|
1379
643
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
await this.client.waitForNetworkIdle(500, waitFor.timeout || 3000);
|
|
1384
|
-
}
|
|
1385
|
-
else if (waitFor.type === "element" && waitFor.selector) {
|
|
1386
|
-
await this.client.waitForSelector(waitFor.selector, waitFor.timeout || 5000);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
const currentUrl = await this.client.evaluate("window.location.href").catch(() => url);
|
|
1390
|
-
const title = await this.client.evaluate("document.title").catch(() => "Unknown");
|
|
1391
|
-
const screenshotData = screenshot === true ? await this.client.screenshot("jpeg", 70).catch(() => null) : null;
|
|
1392
|
-
return {
|
|
1393
|
-
success: true,
|
|
1394
|
-
url: currentUrl,
|
|
1395
|
-
title,
|
|
1396
|
-
screenshot: screenshotData
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
catch (e) {
|
|
1400
|
-
return { success: false, error: e.message };
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
async observeAndAct(params) {
|
|
1404
|
-
const { action, observe, returnScreenshot } = params;
|
|
1405
|
-
try {
|
|
1406
|
-
const [beforeFacts, beforeScreenshot] = await Promise.all([
|
|
1407
|
-
this.captureActionFacts(action.selector),
|
|
1408
|
-
returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
|
|
1409
|
-
]);
|
|
1410
|
-
if (action.type === "click" && action.selector) {
|
|
1411
|
-
await this.client.waitForSelector(action.selector, 5000, { visible: true, stable: true });
|
|
1412
|
-
await this.clickElementBySelector({ selector: action.selector });
|
|
1413
|
-
}
|
|
1414
|
-
else if (action.type === "type" && action.text) {
|
|
1415
|
-
if (action.selector) {
|
|
1416
|
-
await this.fillBySelector({ selector: action.selector, value: action.text, timeout: 5000 });
|
|
644
|
+
else if (action.type === "type" && action.text) {
|
|
645
|
+
if (action.selector) {
|
|
646
|
+
await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector: action.selector, value: action.text, timeout: 5000 }, this.logger);
|
|
1417
647
|
}
|
|
1418
648
|
else {
|
|
1419
649
|
await this.client.typeText(action.text);
|
|
@@ -1426,20 +656,19 @@ class CdpBridge {
|
|
|
1426
656
|
await this.client.waitForNetworkIdle(500, 5000).catch(() => { });
|
|
1427
657
|
}
|
|
1428
658
|
else {
|
|
1429
|
-
await new Promise(r => setTimeout(r, 300));
|
|
659
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1430
660
|
}
|
|
1431
661
|
const [afterFacts, afterScreenshot] = await Promise.all([
|
|
1432
|
-
|
|
662
|
+
Interaction.captureActionFacts(this.client, action?.selector),
|
|
1433
663
|
returnScreenshot === true ? this.client.screenshot("jpeg", 70).catch(() => null) : Promise.resolve(null),
|
|
1434
664
|
]);
|
|
1435
|
-
const facts =
|
|
665
|
+
const facts = Interaction.diffActionFacts(beforeFacts, afterFacts);
|
|
1436
666
|
const changesDetected = facts.urlChanged || facts.titleChanged || facts.valueChanged || facts.checkedChanged || facts.selectedIndexChanged;
|
|
1437
667
|
return {
|
|
1438
668
|
success: true,
|
|
1439
669
|
before: { facts: beforeFacts, screenshot: beforeScreenshot },
|
|
1440
670
|
after: { facts: afterFacts, screenshot: afterScreenshot },
|
|
1441
|
-
changesDetected,
|
|
1442
|
-
facts,
|
|
671
|
+
changesDetected, facts,
|
|
1443
672
|
navigationOccurred: facts.urlChanged,
|
|
1444
673
|
};
|
|
1445
674
|
}
|
|
@@ -1447,18 +676,6 @@ class CdpBridge {
|
|
|
1447
676
|
return { success: false, error: e.message };
|
|
1448
677
|
}
|
|
1449
678
|
}
|
|
1450
|
-
simpleDiff(before, after) {
|
|
1451
|
-
// Simple Levenshtein distance for change detection
|
|
1452
|
-
if (before === after)
|
|
1453
|
-
return 0;
|
|
1454
|
-
const len = Math.max(before.length, after.length);
|
|
1455
|
-
let diff = 0;
|
|
1456
|
-
for (let i = 0; i < len; i++) {
|
|
1457
|
-
if (before[i] !== after[i])
|
|
1458
|
-
diff++;
|
|
1459
|
-
}
|
|
1460
|
-
return diff;
|
|
1461
|
-
}
|
|
1462
679
|
async agentFormFill(params) {
|
|
1463
680
|
const { fields, submitAfterFill, submitSelector } = params;
|
|
1464
681
|
const results = [];
|
|
@@ -1470,29 +687,29 @@ class CdpBridge {
|
|
|
1470
687
|
continue;
|
|
1471
688
|
}
|
|
1472
689
|
if (["text", "email", "password", "textarea"].includes(field.type) || !field.type) {
|
|
1473
|
-
await
|
|
690
|
+
await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "", timeout: field.timeout || 5000 }, this.logger);
|
|
1474
691
|
}
|
|
1475
692
|
else if (field.type === "select") {
|
|
1476
|
-
await
|
|
693
|
+
await Interaction.selectOption(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "" }, this.logger);
|
|
1477
694
|
}
|
|
1478
695
|
else if (field.type === "checkbox" || field.type === "radio") {
|
|
1479
696
|
if (field.checked === false) {
|
|
1480
|
-
await
|
|
697
|
+
await Interaction.setChecked(this.client, this.locator, this.snapshotCache, { selector, checked: false }, this.logger);
|
|
1481
698
|
}
|
|
1482
699
|
else {
|
|
1483
|
-
await
|
|
700
|
+
await Interaction.checkElement(this.client, this.locator, this.snapshotCache, { selector }, this.logger);
|
|
1484
701
|
}
|
|
1485
702
|
}
|
|
1486
703
|
else if (field.type === "file") {
|
|
1487
704
|
await this.uploadFile({ selector, files: field.files || [] });
|
|
1488
705
|
}
|
|
1489
706
|
else {
|
|
1490
|
-
await
|
|
707
|
+
await Interaction.fillBySelector(this.client, this.locator, this.snapshotCache, { selector, value: field.value ?? "", timeout: field.timeout || 5000 }, this.logger);
|
|
1491
708
|
}
|
|
1492
709
|
results.push({ field: field.id || field.selector, selector, success: true });
|
|
1493
710
|
}
|
|
1494
711
|
if (submitAfterFill && submitSelector) {
|
|
1495
|
-
await
|
|
712
|
+
await Interaction.clickElementBySelector(this.client, this.locator, this.snapshotCache, { selector: submitSelector }, this.logger);
|
|
1496
713
|
}
|
|
1497
714
|
return { success: true, fieldsFilled: results.length, results };
|
|
1498
715
|
}
|
|
@@ -1500,200 +717,129 @@ class CdpBridge {
|
|
|
1500
717
|
return { success: false, error: e.message, results };
|
|
1501
718
|
}
|
|
1502
719
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
this.client.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
inputs: inputs.map(input => ({
|
|
1524
|
-
type: input.type || 'text',
|
|
1525
|
-
name: input.name || '',
|
|
1526
|
-
id: input.id || '',
|
|
1527
|
-
required: input.required,
|
|
1528
|
-
placeholder: input.placeholder || ''
|
|
1529
|
-
}))
|
|
1530
|
-
};
|
|
1531
|
-
});
|
|
1532
|
-
})()
|
|
1533
|
-
`).catch(() => ({ value: [] })),
|
|
1534
|
-
includeCookies ? this.client.sendCommand("Network.getCookies", {}).catch(() => ({ cookies: [] })) : Promise.resolve({ cookies: [] }),
|
|
1535
|
-
includeAccessibilityTree ? this.client.getSimplifiedAccessibilityTree().catch(() => []) : Promise.resolve([]),
|
|
1536
|
-
]);
|
|
1537
|
-
return {
|
|
1538
|
-
title,
|
|
1539
|
-
url,
|
|
1540
|
-
screenshot,
|
|
1541
|
-
elements: elements.elements,
|
|
1542
|
-
accessibilityTree: axTree,
|
|
1543
|
-
forms: forms || [],
|
|
1544
|
-
cookies: cookies.cookies || [],
|
|
1545
|
-
domSnapshot: params.includeDOMSnapshot ? domSnapshot : undefined,
|
|
1546
|
-
metadata: {
|
|
1547
|
-
timestamp: Date.now(),
|
|
1548
|
-
elementCount: elements.elements.length,
|
|
1549
|
-
}
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
catch (e) {
|
|
1553
|
-
return { success: false, error: e.message };
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
// ==================== SELF-HEALING SELECTOR RESOLUTION ====================
|
|
1557
|
-
async resolveSelector(params) {
|
|
1558
|
-
const { originalSelector, text, fuzzyMatch } = params;
|
|
1559
|
-
// Try original selector first
|
|
1560
|
-
if (originalSelector) {
|
|
1561
|
-
const exists = await this.client.evaluate(`
|
|
1562
|
-
!!document.querySelector(${JSON.stringify(originalSelector)})
|
|
1563
|
-
`);
|
|
1564
|
-
if (exists) {
|
|
1565
|
-
return { selector: originalSelector, method: "exact", confidence: 1.0 };
|
|
720
|
+
// ─── Configuration ────────────────────────────────────────────────
|
|
721
|
+
async configureBrowser(params) {
|
|
722
|
+
const { network, emulation, script } = params;
|
|
723
|
+
const results = [];
|
|
724
|
+
if (network) {
|
|
725
|
+
if (network.blockImages) {
|
|
726
|
+
await this.client.sendCommand("Network.setBlockedURLs", {
|
|
727
|
+
urls: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.svg"],
|
|
728
|
+
});
|
|
729
|
+
results.push("Blocked images");
|
|
730
|
+
}
|
|
731
|
+
if (network.blockCSS) {
|
|
732
|
+
await this.client.sendCommand("Network.setBlockedURLs", { urls: ["*.css"] });
|
|
733
|
+
results.push("Blocked CSS");
|
|
734
|
+
}
|
|
735
|
+
if (network.blockAds) {
|
|
736
|
+
await this.client.sendCommand("Network.setBlockedURLs", {
|
|
737
|
+
urls: ["*doubleclick.net*", "*googlesyndication.com*", "*adservice.*"],
|
|
738
|
+
});
|
|
739
|
+
results.push("Blocked ads");
|
|
1566
740
|
}
|
|
1567
741
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
confidence: resolved.confidence || 0.7,
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
// Try fuzzy text matching if enabled
|
|
1583
|
-
if (fuzzyMatch !== false && text) {
|
|
1584
|
-
const result = await this.client.evaluate(`
|
|
1585
|
-
(function() {
|
|
1586
|
-
const searchText = ${JSON.stringify(text)};
|
|
1587
|
-
const searchLower = String(searchText || '').trim().toLowerCase();
|
|
1588
|
-
const elements = Array.from(document.querySelectorAll('a[href], button, input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [onclick]')).filter((el) => {
|
|
1589
|
-
const rect = el.getBoundingClientRect();
|
|
1590
|
-
const computed = window.getComputedStyle(el);
|
|
1591
|
-
return computed.display !== 'none' && computed.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
function cssPath(el) {
|
|
1595
|
-
if (el.id) return '#' + CSS.escape(el.id);
|
|
1596
|
-
const path = [];
|
|
1597
|
-
while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
|
|
1598
|
-
let selector = el.nodeName.toLowerCase();
|
|
1599
|
-
if (el.classList && el.classList.length) {
|
|
1600
|
-
selector += '.' + Array.from(el.classList).slice(0, 2).map(c => CSS.escape(c)).join('.');
|
|
1601
|
-
}
|
|
1602
|
-
const parent = el.parentElement;
|
|
1603
|
-
if (parent) {
|
|
1604
|
-
const siblings = Array.from(parent.children).filter(child => child.nodeName === el.nodeName);
|
|
1605
|
-
if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')';
|
|
1606
|
-
}
|
|
1607
|
-
path.unshift(selector);
|
|
1608
|
-
el = parent;
|
|
1609
|
-
}
|
|
1610
|
-
return path.length ? path.join(' > ') : '';
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function textFor(el) {
|
|
1614
|
-
return String(
|
|
1615
|
-
el.innerText ||
|
|
1616
|
-
el.textContent ||
|
|
1617
|
-
el.getAttribute('aria-label') ||
|
|
1618
|
-
el.getAttribute('placeholder') ||
|
|
1619
|
-
el.getAttribute('name') ||
|
|
1620
|
-
''
|
|
1621
|
-
).trim();
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
// Exact match first
|
|
1625
|
-
let best = elements.find(el => textFor(el).toLowerCase() === searchLower);
|
|
1626
|
-
if (best) return { selector: cssPath(best), confidence: 1.0 };
|
|
1627
|
-
|
|
1628
|
-
// Partial match
|
|
1629
|
-
best = elements.find(el => {
|
|
1630
|
-
const value = textFor(el).toLowerCase();
|
|
1631
|
-
return searchLower.length >= 3 && value.includes(searchLower);
|
|
1632
|
-
});
|
|
1633
|
-
if (best) return { selector: cssPath(best), confidence: 0.8 };
|
|
1634
|
-
|
|
1635
|
-
// Fuzzy match (Levenshtein distance)
|
|
1636
|
-
let minDist = Infinity;
|
|
1637
|
-
let bestEl = null;
|
|
1638
|
-
elements.forEach(el => {
|
|
1639
|
-
const elText = textFor(el);
|
|
1640
|
-
if (!elText || Math.abs(elText.length - searchText.length) > 8) return;
|
|
1641
|
-
const dist = levenshteinDistance(searchText, elText);
|
|
1642
|
-
if (dist < minDist && dist <= 3) {
|
|
1643
|
-
minDist = dist;
|
|
1644
|
-
bestEl = el;
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
|
-
if (bestEl) return { selector: cssPath(bestEl), confidence: 0.6 };
|
|
1649
|
-
|
|
1650
|
-
return null;
|
|
1651
|
-
|
|
1652
|
-
function levenshteinDistance(a, b) {
|
|
1653
|
-
if (a.length === 0) return b.length;
|
|
1654
|
-
if (b.length === 0) return a.length;
|
|
1655
|
-
const matrix = [];
|
|
1656
|
-
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
1657
|
-
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
1658
|
-
for (let i = 1; i <= b.length; i++) {
|
|
1659
|
-
for (let j = 1; j <= a.length; j++) {
|
|
1660
|
-
matrix[i][j] = b.charAt(i-1) === a.charAt(j-1) ? matrix[i-1][j-1] :
|
|
1661
|
-
Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
return matrix[b.length][a.length];
|
|
1665
|
-
}
|
|
1666
|
-
})()
|
|
1667
|
-
`);
|
|
1668
|
-
if (result) {
|
|
1669
|
-
return { selector: result.selector, method: "fuzzy", confidence: result.confidence };
|
|
742
|
+
if (emulation) {
|
|
743
|
+
if (emulation.width && emulation.height) {
|
|
744
|
+
await this.client.sendCommand("Emulation.setDeviceMetricsOverride", {
|
|
745
|
+
width: emulation.width, height: emulation.height,
|
|
746
|
+
deviceScaleFactor: emulation.scale || 1, mobile: emulation.mobile || false,
|
|
747
|
+
});
|
|
748
|
+
results.push(`Emulated device: ${emulation.width}x${emulation.height}`);
|
|
749
|
+
}
|
|
750
|
+
if (emulation.userAgent) {
|
|
751
|
+
await this.client.sendCommand("Emulation.setUserAgentOverride", { userAgent: emulation.userAgent });
|
|
752
|
+
results.push("Set custom user agent");
|
|
1670
753
|
}
|
|
1671
754
|
}
|
|
1672
|
-
|
|
755
|
+
if (script?.onLoad) {
|
|
756
|
+
await this.client.sendCommand("Page.addScriptToEvaluateOnNewDocument", { source: script.onLoad });
|
|
757
|
+
results.push("Added script to run on new documents");
|
|
758
|
+
}
|
|
759
|
+
return { success: true, configured: results, message: results.join(", ") || "No configuration applied" };
|
|
1673
760
|
}
|
|
1674
|
-
async
|
|
1675
|
-
|
|
1676
|
-
|
|
761
|
+
async emulateNetworkConditions(params) {
|
|
762
|
+
await this.client.sendCommand("Network.emulateNetworkConditions", {
|
|
763
|
+
offline: params.offline || false,
|
|
764
|
+
latency: params.latency || 0,
|
|
765
|
+
downloadThroughput: params.downloadThroughput || 0,
|
|
766
|
+
uploadThroughput: params.uploadThroughput || 0,
|
|
767
|
+
});
|
|
768
|
+
return "Network conditions emulated";
|
|
1677
769
|
}
|
|
1678
|
-
async
|
|
1679
|
-
|
|
1680
|
-
|
|
770
|
+
async setGeolocation(params) {
|
|
771
|
+
await this.client.sendCommand("Emulation.setGeolocationOverride", {
|
|
772
|
+
latitude: params.latitude, longitude: params.longitude, accuracy: params.accuracy || 100,
|
|
1681
773
|
});
|
|
1682
|
-
return
|
|
774
|
+
return `Geolocation set to ${params.latitude}, ${params.longitude}`;
|
|
1683
775
|
}
|
|
1684
|
-
async
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
await this.client.sendCommand("Target.activateTarget", { targetId: params.targetId });
|
|
1688
|
-
await this.client.switchToTarget(params.targetId, params.port || 9222);
|
|
1689
|
-
return `Switched to tab ${params.targetId}`;
|
|
776
|
+
async setTimezone(params) {
|
|
777
|
+
await this.client.sendCommand("Emulation.setTimezoneOverride", { timezoneId: params.timezoneId });
|
|
778
|
+
return `Timezone set to ${params.timezoneId}`;
|
|
1690
779
|
}
|
|
1691
|
-
async
|
|
1692
|
-
await this.client.sendCommand("
|
|
1693
|
-
|
|
780
|
+
async printPDF(params) {
|
|
781
|
+
const result = await this.client.sendCommand("Page.printToPDF", {
|
|
782
|
+
landscape: params.landscape || false,
|
|
783
|
+
printBackground: params.printBackground || false,
|
|
784
|
+
...params.options,
|
|
1694
785
|
});
|
|
1695
|
-
return
|
|
786
|
+
return result.data;
|
|
1696
787
|
}
|
|
788
|
+
async uploadFile(params) {
|
|
789
|
+
const selector = params.selector;
|
|
790
|
+
const files = params.files || [];
|
|
791
|
+
if (!selector)
|
|
792
|
+
throw new Error("Selector required for upload_file");
|
|
793
|
+
if (!files.length)
|
|
794
|
+
throw new Error("No files specified");
|
|
795
|
+
const result = await this.client.evaluate(`(function(){var el=document.querySelector(${JSON.stringify(selector)});if(!el)return {success:false,error:"Element not found"};if(el.type!=='file')return {success:false,error:"Element is not a file input"};el.style.display='block';el.click();return {success:true};})()`);
|
|
796
|
+
if (!result?.success)
|
|
797
|
+
throw new Error(result?.error || "Failed to activate file input");
|
|
798
|
+
const docResult = await this.client.sendCommand("DOM.getDocument", {});
|
|
799
|
+
const queryResult = await this.client.sendCommand("DOM.querySelector", {
|
|
800
|
+
nodeId: docResult.root.nodeId, selector,
|
|
801
|
+
});
|
|
802
|
+
if (!queryResult.nodeId)
|
|
803
|
+
throw new Error(`File input not found: ${selector}`);
|
|
804
|
+
await this.client.sendCommand("DOM.setFileInputFiles", { files, nodeId: queryResult.nodeId });
|
|
805
|
+
return "File upload completed";
|
|
806
|
+
}
|
|
807
|
+
// ─── Network Mocking ──────────────────────────────────────────────
|
|
808
|
+
async mockNetworkRequest(params) {
|
|
809
|
+
const urlPattern = params.urlPattern;
|
|
810
|
+
const mockResponse = params.mockResponse;
|
|
811
|
+
if (!urlPattern)
|
|
812
|
+
throw new Error("urlPattern required");
|
|
813
|
+
this.mockRoutes.push({ pattern: urlPattern, response: mockResponse || "{}" });
|
|
814
|
+
await this.client.sendCommand("Fetch.enable", {
|
|
815
|
+
patterns: this.mockRoutes.map((route) => ({ urlPattern: route.pattern })),
|
|
816
|
+
});
|
|
817
|
+
if (!this.mockRouteListener) {
|
|
818
|
+
this.mockRouteListener = async (event) => {
|
|
819
|
+
const route = this.mockRoutes.find((item) => this.matchesUrlPattern(event.request.url, item.pattern));
|
|
820
|
+
if (!route) {
|
|
821
|
+
await this.client.sendCommand("Fetch.continueRequest", { requestId: event.requestId }).catch(() => { });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
await this.client.sendCommand("Fetch.fulfillRequest", {
|
|
825
|
+
requestId: event.requestId,
|
|
826
|
+
responseCode: 200,
|
|
827
|
+
responseHeaders: [
|
|
828
|
+
{ name: "Content-Type", value: "application/json" },
|
|
829
|
+
{ name: "Access-Control-Allow-Origin", value: "*" },
|
|
830
|
+
],
|
|
831
|
+
body: Buffer.from(route.response).toString("base64"),
|
|
832
|
+
}).catch(() => { });
|
|
833
|
+
};
|
|
834
|
+
this.client.on("Fetch.requestPaused", this.mockRouteListener);
|
|
835
|
+
}
|
|
836
|
+
return `Mocking enabled for pattern: ${urlPattern}`;
|
|
837
|
+
}
|
|
838
|
+
matchesUrlPattern(url, pattern) {
|
|
839
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
840
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
841
|
+
}
|
|
842
|
+
// ─── Screencast ───────────────────────────────────────────────────
|
|
1697
843
|
async startScreencast(params) {
|
|
1698
844
|
this.screencastFrames = [];
|
|
1699
845
|
if (!this.screencastFrameListener) {
|
|
@@ -1713,7 +859,7 @@ class CdpBridge {
|
|
|
1713
859
|
await this.client.sendCommand("Page.startScreencast", {
|
|
1714
860
|
format: params.format || "jpeg",
|
|
1715
861
|
quality: params.quality || 80,
|
|
1716
|
-
everyNthFrame: params.everyNthFrame || 1
|
|
862
|
+
everyNthFrame: params.everyNthFrame || 1,
|
|
1717
863
|
});
|
|
1718
864
|
return "Started screencast";
|
|
1719
865
|
}
|
|
@@ -1737,14 +883,10 @@ class CdpBridge {
|
|
|
1737
883
|
};
|
|
1738
884
|
this.client.on("Page.screencastFrame", onFrame);
|
|
1739
885
|
await this.startScreencast(params);
|
|
1740
|
-
await new Promise(r => setTimeout(r, duration));
|
|
886
|
+
await new Promise((r) => setTimeout(r, duration));
|
|
1741
887
|
await this.stopScreencast(params);
|
|
1742
888
|
this.client.removeEventListener("Page.screencastFrame", onFrame);
|
|
1743
|
-
return {
|
|
1744
|
-
frames,
|
|
1745
|
-
frameCount: frames.length,
|
|
1746
|
-
duration
|
|
1747
|
-
};
|
|
889
|
+
return { frames, frameCount: frames.length, duration };
|
|
1748
890
|
}
|
|
1749
891
|
async sampleVisualFrames(params) {
|
|
1750
892
|
const duration = Math.max(250, Math.min(Number(params.duration ?? 1500), 10000));
|
|
@@ -1764,550 +906,25 @@ class CdpBridge {
|
|
|
1764
906
|
quality: Math.max(20, Math.min(Number(params.quality ?? 45), 80)),
|
|
1765
907
|
maxWidth: Math.max(320, Math.min(Number(params.maxWidth ?? 800), 1280)),
|
|
1766
908
|
maxHeight: Math.max(240, Math.min(Number(params.maxHeight ?? 600), 900)),
|
|
1767
|
-
everyNthFrame: Math.max(1, Math.min(Number(params.everyNthFrame ?? 3), 10))
|
|
909
|
+
everyNthFrame: Math.max(1, Math.min(Number(params.everyNthFrame ?? 3), 10)),
|
|
1768
910
|
});
|
|
1769
911
|
try {
|
|
1770
|
-
await new Promise(r => setTimeout(r, duration));
|
|
912
|
+
await new Promise((r) => setTimeout(r, duration));
|
|
1771
913
|
}
|
|
1772
914
|
finally {
|
|
1773
915
|
await this.client.sendCommand("Page.stopScreencast", {}).catch(() => { });
|
|
1774
916
|
this.client.removeEventListener("Page.screencastFrame", onFrame);
|
|
1775
917
|
}
|
|
1776
|
-
return {
|
|
1777
|
-
success: true,
|
|
1778
|
-
frameCount: frames.length,
|
|
1779
|
-
duration,
|
|
1780
|
-
format: params.format || "jpeg",
|
|
1781
|
-
timestamps,
|
|
1782
|
-
frames
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
async startTracing(params) {
|
|
1786
|
-
await this.client.sendCommand("Tracing.start", { categories: params.categories || "devtools.timeline" });
|
|
1787
|
-
return "Started tracing";
|
|
1788
|
-
}
|
|
1789
|
-
async stopTracing(params) {
|
|
1790
|
-
await this.client.sendCommand("Tracing.end", {});
|
|
1791
|
-
return "Stopped tracing";
|
|
1792
|
-
}
|
|
1793
|
-
async getPerformanceMetrics(params) {
|
|
1794
|
-
await this.client.sendCommand("Performance.enable", {});
|
|
1795
|
-
const result = await this.client.sendCommand("Performance.getMetrics", {});
|
|
1796
|
-
return result.metrics;
|
|
1797
|
-
}
|
|
1798
|
-
async hover(params) {
|
|
1799
|
-
const x = params.x || (params.coordinate ? Number(params.coordinate.split(',')[0]) : 100);
|
|
1800
|
-
const y = params.y || (params.coordinate ? Number(params.coordinate.split(',')[1]) : 100);
|
|
1801
|
-
await this.client.moveMouse(x, y);
|
|
1802
|
-
return "Hovered";
|
|
1803
|
-
}
|
|
1804
|
-
async dragAndDrop(params) {
|
|
1805
|
-
const startX = params.startX || 0;
|
|
1806
|
-
const startY = params.startY || 0;
|
|
1807
|
-
const endX = params.endX || 0;
|
|
1808
|
-
const endY = params.endY || 0;
|
|
1809
|
-
await this.client.moveMouse(startX, startY);
|
|
1810
|
-
await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x: startX, y: startY, button: "left", clickCount: 1 });
|
|
1811
|
-
await this.client.moveMouse(endX, endY);
|
|
1812
|
-
await this.client.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x: endX, y: endY, button: "left", clickCount: 1 });
|
|
1813
|
-
return "Dragged and dropped";
|
|
1814
|
-
}
|
|
1815
|
-
// ==================== MISSING ACT TOOL ACTION IMPLEMENTATIONS ====================
|
|
1816
|
-
async fillInput(params) {
|
|
1817
|
-
const selector = params.selector;
|
|
1818
|
-
const text = params.value || params.text || "";
|
|
1819
|
-
if (selector) {
|
|
1820
|
-
// Wait for element and clear it first
|
|
1821
|
-
await this.client.waitForSelector(selector);
|
|
1822
|
-
await this.client.moveMouseToSelector(selector).catch(() => { });
|
|
1823
|
-
await this.client.evaluate(`
|
|
1824
|
-
(function() {
|
|
1825
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1826
|
-
if (el) {
|
|
1827
|
-
el.value = '';
|
|
1828
|
-
el.focus();
|
|
1829
|
-
return true;
|
|
1830
|
-
}
|
|
1831
|
-
return false;
|
|
1832
|
-
})()
|
|
1833
|
-
`);
|
|
1834
|
-
}
|
|
1835
|
-
await this.client.typeText(text);
|
|
1836
|
-
return `Filled with: ${text}`;
|
|
1837
|
-
}
|
|
1838
|
-
async selectOption(params) {
|
|
1839
|
-
const selector = params.selector;
|
|
1840
|
-
const value = params.value || "";
|
|
1841
|
-
if (!selector)
|
|
1842
|
-
throw new Error("Selector required for select action");
|
|
1843
|
-
await this.client.waitForSelector(selector);
|
|
1844
|
-
const selectInfo = await this.client.evaluate(`
|
|
1845
|
-
(function() {
|
|
1846
|
-
const select = document.querySelector(${JSON.stringify(selector)});
|
|
1847
|
-
if (!select) return { success: false, error: "Element not found" };
|
|
1848
|
-
if (select.tagName.toLowerCase() !== 'select') return { success: false, error: "Element is not a select" };
|
|
1849
|
-
if (select.disabled) return { success: false, error: "Element is disabled" };
|
|
1850
|
-
const wanted = ${JSON.stringify(value)};
|
|
1851
|
-
const options = Array.from(select.options || []);
|
|
1852
|
-
const index = options.findIndex((option) =>
|
|
1853
|
-
option.value === wanted ||
|
|
1854
|
-
option.text === wanted ||
|
|
1855
|
-
option.label === wanted
|
|
1856
|
-
);
|
|
1857
|
-
return {
|
|
1858
|
-
success: true,
|
|
1859
|
-
selectedValue: select.value,
|
|
1860
|
-
index,
|
|
1861
|
-
optionCount: options.length,
|
|
1862
|
-
wantedValue: index >= 0 ? options[index].value : wanted,
|
|
1863
|
-
};
|
|
1864
|
-
})()
|
|
1865
|
-
`);
|
|
1866
|
-
if (!selectInfo?.success)
|
|
1867
|
-
throw new Error(selectInfo?.error || "Failed to inspect select");
|
|
1868
|
-
if (selectInfo.selectedValue === selectInfo.wantedValue) {
|
|
1869
|
-
return `Selected option: ${value}`;
|
|
1870
|
-
}
|
|
1871
|
-
if (selectInfo.index >= 0 && selectInfo.index <= 40) {
|
|
1872
|
-
try {
|
|
1873
|
-
await this.clickElementBySelector({ selector });
|
|
1874
|
-
await this.client.pressKey("Home");
|
|
1875
|
-
for (let i = 0; i < selectInfo.index; i++) {
|
|
1876
|
-
await this.client.pressKey("ArrowDown");
|
|
1877
|
-
}
|
|
1878
|
-
await this.client.pressKey("Enter");
|
|
1879
|
-
const verified = await this.client.evaluate(`
|
|
1880
|
-
(function() {
|
|
1881
|
-
const select = document.querySelector(${JSON.stringify(selector)});
|
|
1882
|
-
return select ? select.value : null;
|
|
1883
|
-
})()
|
|
1884
|
-
`);
|
|
1885
|
-
if (verified === selectInfo.wantedValue) {
|
|
1886
|
-
return `Selected option: ${value}`;
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
catch {
|
|
1890
|
-
// Fall back to direct value setting below for reliability.
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
const result = await this.client.evaluate(`
|
|
1894
|
-
(function() {
|
|
1895
|
-
const select = document.querySelector(${JSON.stringify(selector)});
|
|
1896
|
-
if (!select) return { success: false, error: "Element not found" };
|
|
1897
|
-
select.value = ${JSON.stringify(selectInfo.wantedValue)};
|
|
1898
|
-
select.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1899
|
-
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1900
|
-
return { success: true, selectedValue: select.value };
|
|
1901
|
-
})()
|
|
1902
|
-
`);
|
|
1903
|
-
if (result?.success)
|
|
1904
|
-
return `Selected option: ${value}`;
|
|
1905
|
-
throw new Error(result?.error || "Failed to select option");
|
|
1906
|
-
}
|
|
1907
|
-
async checkElement(params) {
|
|
1908
|
-
const selector = params.selector;
|
|
1909
|
-
if (!selector)
|
|
1910
|
-
throw new Error("Selector required for check action");
|
|
1911
|
-
return this.setChecked({ selector, checked: true });
|
|
1912
|
-
}
|
|
1913
|
-
async setChecked(params) {
|
|
1914
|
-
const selector = params.selector;
|
|
1915
|
-
if (!selector)
|
|
1916
|
-
throw new Error("Selector required for checked state");
|
|
1917
|
-
await this.client.waitForSelector(selector);
|
|
1918
|
-
const before = await this.client.evaluate(`
|
|
1919
|
-
(function() {
|
|
1920
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1921
|
-
if (!el) return { success: false, error: "Element not found" };
|
|
1922
|
-
if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
|
|
1923
|
-
if (el.disabled) return { success: false, error: "Element is disabled" };
|
|
1924
|
-
return { success: true, checked: !!el.checked, type: el.type };
|
|
1925
|
-
})()
|
|
1926
|
-
`);
|
|
1927
|
-
if (!before?.success)
|
|
1928
|
-
throw new Error(before?.error || "Failed to inspect checked state");
|
|
1929
|
-
const wanted = !!params.checked;
|
|
1930
|
-
if (before.checked === wanted)
|
|
1931
|
-
return `Checked state set to ${wanted}`;
|
|
1932
|
-
if (!(before.type === "radio" && !wanted)) {
|
|
1933
|
-
try {
|
|
1934
|
-
await this.clickElementBySelector({ selector });
|
|
1935
|
-
const afterClick = await this.client.evaluate(`
|
|
1936
|
-
(function() {
|
|
1937
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1938
|
-
return el ? !!el.checked : null;
|
|
1939
|
-
})()
|
|
1940
|
-
`);
|
|
1941
|
-
if (afterClick === wanted)
|
|
1942
|
-
return `Checked state set to ${wanted}`;
|
|
1943
|
-
}
|
|
1944
|
-
catch {
|
|
1945
|
-
// Fall back to direct state setting below for reliability.
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
const result = await this.client.evaluate(`
|
|
1949
|
-
(function() {
|
|
1950
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
1951
|
-
if (!el) return { success: false, error: "Element not found" };
|
|
1952
|
-
if (el.type !== 'checkbox' && el.type !== 'radio') return { success: false, error: "Element is not a checkbox or radio" };
|
|
1953
|
-
el.checked = ${JSON.stringify(!!params.checked)};
|
|
1954
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1955
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1956
|
-
return { success: true, checked: el.checked };
|
|
1957
|
-
})()
|
|
1958
|
-
`);
|
|
1959
|
-
if (result?.success)
|
|
1960
|
-
return `Checked state set to ${!!params.checked}`;
|
|
1961
|
-
throw new Error(result?.error || "Failed to set checked state");
|
|
1962
|
-
}
|
|
1963
|
-
async getAccessibilityTree(params) {
|
|
1964
|
-
return await this.client.getSimplifiedAccessibilityTree();
|
|
1965
|
-
}
|
|
1966
|
-
async getDOMTree(params) {
|
|
1967
|
-
const result = await this.client.getDOMSnapshot();
|
|
1968
|
-
return result;
|
|
1969
|
-
}
|
|
1970
|
-
async assertCondition(params) {
|
|
1971
|
-
const assertionType = params.assertionType || "element_exists";
|
|
1972
|
-
const selector = params.selector;
|
|
1973
|
-
const expectedText = params.expectedText || params.value || "";
|
|
1974
|
-
const result = await this.client.evaluate(`
|
|
1975
|
-
(function() {
|
|
1976
|
-
const selector = ${JSON.stringify(selector)};
|
|
1977
|
-
const type = ${JSON.stringify(assertionType)};
|
|
1978
|
-
const expectedText = ${JSON.stringify(expectedText)};
|
|
1979
|
-
|
|
1980
|
-
const el = selector ? document.querySelector(selector) : null;
|
|
1981
|
-
|
|
1982
|
-
switch(type) {
|
|
1983
|
-
case 'element_exists':
|
|
1984
|
-
return { success: !!el, message: el ? 'Element exists' : 'Element not found' };
|
|
1985
|
-
case 'element_not_exists':
|
|
1986
|
-
return { success: !el, message: !el ? 'Element does not exist' : 'Element found' };
|
|
1987
|
-
case 'element_contains_text':
|
|
1988
|
-
if (!el) return { success: false, message: 'Element not found' };
|
|
1989
|
-
const text = (el.innerText || el.textContent || '').trim();
|
|
1990
|
-
const matches = text.includes(expectedText);
|
|
1991
|
-
return { success: matches, message: matches ? 'Text matches' : 'Text does not match', actualText: text };
|
|
1992
|
-
case 'url_contains':
|
|
1993
|
-
const urlMatches = window.location.href.includes(expectedText);
|
|
1994
|
-
return { success: urlMatches, message: urlMatches ? 'URL contains text' : 'URL does not contain text' };
|
|
1995
|
-
default:
|
|
1996
|
-
return { success: false, message: 'Unknown assertion type' };
|
|
1997
|
-
}
|
|
1998
|
-
})()
|
|
1999
|
-
`);
|
|
2000
|
-
return result || { success: false, message: "Assertion failed" };
|
|
2001
|
-
}
|
|
2002
|
-
async getCookies(params) {
|
|
2003
|
-
const result = await this.client.sendCommand("Network.getCookies", {
|
|
2004
|
-
urls: [await this.client.evaluate("window.location.href").catch(() => "*") || "*"],
|
|
2005
|
-
});
|
|
2006
|
-
return result.cookies || [];
|
|
2007
|
-
}
|
|
2008
|
-
async setCookie(params) {
|
|
2009
|
-
const cookies = [{
|
|
2010
|
-
name: params.cookieName || params.name,
|
|
2011
|
-
value: params.cookieValue || params.value,
|
|
2012
|
-
url: params.url || await this.client.evaluate("window.location.href").catch(() => undefined),
|
|
2013
|
-
domain: params.domain,
|
|
2014
|
-
path: params.path || "/",
|
|
2015
|
-
secure: params.secure || false,
|
|
2016
|
-
httpOnly: params.httpOnly || false,
|
|
2017
|
-
}];
|
|
2018
|
-
await this.client.sendCommand("Network.setCookies", { cookies });
|
|
2019
|
-
return "Cookie set";
|
|
2020
|
-
}
|
|
2021
|
-
async clearCache(params) {
|
|
2022
|
-
await this.client.sendCommand("Network.clearBrowserCache", {});
|
|
2023
|
-
await this.client.sendCommand("Network.clearBrowserCookies", {});
|
|
2024
|
-
return "Cache cleared";
|
|
2025
|
-
}
|
|
2026
|
-
async setGeolocation(params) {
|
|
2027
|
-
await this.client.sendCommand("Emulation.setGeolocationOverride", {
|
|
2028
|
-
latitude: params.latitude,
|
|
2029
|
-
longitude: params.longitude,
|
|
2030
|
-
accuracy: params.accuracy || 100,
|
|
2031
|
-
});
|
|
2032
|
-
return `Geolocation set to ${params.latitude}, ${params.longitude}`;
|
|
2033
|
-
}
|
|
2034
|
-
async setTimezone(params) {
|
|
2035
|
-
await this.client.sendCommand("Emulation.setTimezoneOverride", {
|
|
2036
|
-
timezoneId: params.timezoneId,
|
|
2037
|
-
});
|
|
2038
|
-
return `Timezone set to ${params.timezoneId}`;
|
|
2039
|
-
}
|
|
2040
|
-
async emulateNetworkConditions(params) {
|
|
2041
|
-
await this.client.sendCommand("Network.emulateNetworkConditions", {
|
|
2042
|
-
offline: params.offline || false,
|
|
2043
|
-
latency: params.latency || 0,
|
|
2044
|
-
downloadThroughput: params.downloadThroughput || 0,
|
|
2045
|
-
uploadThroughput: params.uploadThroughput || 0,
|
|
2046
|
-
});
|
|
2047
|
-
return "Network conditions emulated";
|
|
2048
|
-
}
|
|
2049
|
-
async printPDF(params) {
|
|
2050
|
-
const result = await this.client.sendCommand("Page.printToPDF", {
|
|
2051
|
-
landscape: params.landscape || false,
|
|
2052
|
-
printBackground: params.printBackground || false,
|
|
2053
|
-
...params.options,
|
|
2054
|
-
});
|
|
2055
|
-
return result.data; // base64 PDF
|
|
2056
|
-
}
|
|
2057
|
-
async highlightElements(params) {
|
|
2058
|
-
const result = await this.client.getInteractiveElements(true);
|
|
2059
|
-
return {
|
|
2060
|
-
success: true,
|
|
2061
|
-
elements: result.elements,
|
|
2062
|
-
message: `Highlighted ${result.elements.length} elements`,
|
|
2063
|
-
};
|
|
2064
|
-
}
|
|
2065
|
-
async verifyUIState(params) {
|
|
2066
|
-
const selector = params.selector;
|
|
2067
|
-
const expectedText = params.expectedText || params.value || "";
|
|
2068
|
-
const result = await this.client.evaluate(`
|
|
2069
|
-
(function() {
|
|
2070
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2071
|
-
if (!el) return { exists: false, visible: false };
|
|
2072
|
-
|
|
2073
|
-
const rect = el.getBoundingClientRect();
|
|
2074
|
-
const computed = window.getComputedStyle(el);
|
|
2075
|
-
const visible = rect.width > 0 && rect.height > 0 &&
|
|
2076
|
-
computed.display !== 'none' &&
|
|
2077
|
-
computed.visibility !== 'hidden' &&
|
|
2078
|
-
computed.opacity !== '0';
|
|
2079
|
-
|
|
2080
|
-
return {
|
|
2081
|
-
exists: true,
|
|
2082
|
-
visible,
|
|
2083
|
-
text: (el.innerText || el.textContent || '').trim().substring(0, 200),
|
|
2084
|
-
bounds: { x: rect.left, y: rect.top, width: rect.width, height: rect.height }
|
|
2085
|
-
};
|
|
2086
|
-
})()
|
|
2087
|
-
`);
|
|
2088
|
-
return result || { exists: false, visible: false };
|
|
2089
|
-
}
|
|
2090
|
-
async getDOMStorage(params) {
|
|
2091
|
-
await this.client.sendCommand("DOMStorage.enable", {});
|
|
2092
|
-
const origin = params.origin || await this.client.evaluate("window.location.origin").catch(() => "");
|
|
2093
|
-
const result = await this.client.sendCommand("DOMStorage.getDOMStorageItems", {
|
|
2094
|
-
storageId: { securityOrigin: origin, isLocalStorage: params.type !== 'session' },
|
|
2095
|
-
});
|
|
2096
|
-
return result.entries || [];
|
|
918
|
+
return { success: true, frameCount: frames.length, duration, format: params.format || "jpeg", timestamps, frames };
|
|
2097
919
|
}
|
|
2098
|
-
async getNetworkTraffic(params) {
|
|
2099
|
-
return await this.client.getNetworkTraffic();
|
|
2100
|
-
}
|
|
2101
|
-
async getNetworkResponse(params) {
|
|
2102
|
-
const requestId = params.requestId;
|
|
2103
|
-
if (!requestId)
|
|
2104
|
-
throw new Error("requestId required");
|
|
2105
|
-
const result = await this.client.sendCommand("Network.getResponseBody", { requestId });
|
|
2106
|
-
return result;
|
|
2107
|
-
}
|
|
2108
|
-
async mockNetworkRequest(params) {
|
|
2109
|
-
const urlPattern = params.urlPattern;
|
|
2110
|
-
const mockResponse = params.mockResponse;
|
|
2111
|
-
if (!urlPattern)
|
|
2112
|
-
throw new Error("urlPattern required");
|
|
2113
|
-
this.mockRoutes.push({
|
|
2114
|
-
pattern: urlPattern,
|
|
2115
|
-
response: mockResponse || "{}"
|
|
2116
|
-
});
|
|
2117
|
-
await this.client.sendCommand("Fetch.enable", {
|
|
2118
|
-
patterns: this.mockRoutes.map(route => ({ urlPattern: route.pattern }))
|
|
2119
|
-
});
|
|
2120
|
-
if (!this.mockRouteListener) {
|
|
2121
|
-
this.mockRouteListener = async (event) => {
|
|
2122
|
-
const route = this.mockRoutes.find(item => this.matchesUrlPattern(event.request.url, item.pattern));
|
|
2123
|
-
if (!route) {
|
|
2124
|
-
await this.client.sendCommand("Fetch.continueRequest", { requestId: event.requestId }).catch(() => { });
|
|
2125
|
-
return;
|
|
2126
|
-
}
|
|
2127
|
-
await this.client.sendCommand("Fetch.fulfillRequest", {
|
|
2128
|
-
requestId: event.requestId,
|
|
2129
|
-
responseCode: 200,
|
|
2130
|
-
responseHeaders: [
|
|
2131
|
-
{ name: "Content-Type", value: "application/json" },
|
|
2132
|
-
{ name: "Access-Control-Allow-Origin", value: "*" }
|
|
2133
|
-
],
|
|
2134
|
-
body: Buffer.from(route.response).toString("base64")
|
|
2135
|
-
}).catch(() => { });
|
|
2136
|
-
};
|
|
2137
|
-
this.client.on("Fetch.requestPaused", this.mockRouteListener);
|
|
2138
|
-
}
|
|
2139
|
-
return `Mocking enabled for pattern: ${urlPattern}`;
|
|
2140
|
-
}
|
|
2141
|
-
matchesUrlPattern(url, pattern) {
|
|
2142
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2143
|
-
return new RegExp(`^${escaped}$`).test(url);
|
|
2144
|
-
}
|
|
2145
|
-
async getComputedStyle(params) {
|
|
2146
|
-
const selector = params.selector;
|
|
2147
|
-
const property = params.property;
|
|
2148
|
-
if (!selector)
|
|
2149
|
-
throw new Error("Selector required");
|
|
2150
|
-
const result = await this.client.evaluate(`
|
|
2151
|
-
(function() {
|
|
2152
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2153
|
-
if (!el) return null;
|
|
2154
|
-
const style = window.getComputedStyle(el);
|
|
2155
|
-
${property ? `return style.getPropertyValue(${JSON.stringify(property)});` : `return JSON.parse(JSON.stringify(style));`}
|
|
2156
|
-
})()
|
|
2157
|
-
`);
|
|
2158
|
-
return result;
|
|
2159
|
-
}
|
|
2160
|
-
async getEventListeners(params) {
|
|
2161
|
-
const selector = params.selector;
|
|
2162
|
-
if (!selector)
|
|
2163
|
-
throw new Error("Selector required");
|
|
2164
|
-
const result = await this.client.evaluate(`
|
|
2165
|
-
(function() {
|
|
2166
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2167
|
-
if (!el) return null;
|
|
2168
|
-
// getEventListeners is Chrome DevTools specific, not available in page context
|
|
2169
|
-
return { message: "getEventListeners requires DevTools protocol, not available in page context" };
|
|
2170
|
-
})()
|
|
2171
|
-
`);
|
|
2172
|
-
return result;
|
|
2173
|
-
}
|
|
2174
|
-
screencastFrames = [];
|
|
2175
|
-
screencastFrameListener = null;
|
|
2176
|
-
mockRoutes = [];
|
|
2177
|
-
mockRouteListener = null;
|
|
2178
920
|
async getScreencastFrames(params) {
|
|
2179
921
|
const maxFrames = params.maxFrames || this.screencastFrames.length;
|
|
2180
922
|
const frames = this.screencastFrames.slice(-maxFrames);
|
|
2181
|
-
return {
|
|
2182
|
-
frameCount: frames.length,
|
|
2183
|
-
frames,
|
|
2184
|
-
message: `Retrieved ${frames.length} frames`
|
|
2185
|
-
};
|
|
2186
|
-
}
|
|
2187
|
-
async uploadFile(params) {
|
|
2188
|
-
const selector = params.selector;
|
|
2189
|
-
const files = params.files || [];
|
|
2190
|
-
if (!selector)
|
|
2191
|
-
throw new Error("Selector required for upload_file action");
|
|
2192
|
-
if (!files.length)
|
|
2193
|
-
throw new Error("No files specified");
|
|
2194
|
-
// Click the file input to activate it
|
|
2195
|
-
const result = await this.client.evaluate(`
|
|
2196
|
-
(function() {
|
|
2197
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2198
|
-
if (!el) return { success: false, error: "Element not found" };
|
|
2199
|
-
if (el.type !== 'file') return { success: false, error: "Element is not a file input" };
|
|
2200
|
-
el.style.display = 'block'; // Make sure it's visible
|
|
2201
|
-
el.click();
|
|
2202
|
-
return { success: true };
|
|
2203
|
-
})()
|
|
2204
|
-
`);
|
|
2205
|
-
if (!result?.success) {
|
|
2206
|
-
throw new Error(result?.error || "Failed to activate file input");
|
|
2207
|
-
}
|
|
2208
|
-
const documentResult = await this.client.sendCommand("DOM.getDocument", {});
|
|
2209
|
-
const queryResult = await this.client.sendCommand("DOM.querySelector", {
|
|
2210
|
-
nodeId: documentResult.root.nodeId,
|
|
2211
|
-
selector
|
|
2212
|
-
});
|
|
2213
|
-
if (!queryResult.nodeId) {
|
|
2214
|
-
throw new Error(`File input not found: ${selector}`);
|
|
2215
|
-
}
|
|
2216
|
-
await this.client.sendCommand("DOM.setFileInputFiles", {
|
|
2217
|
-
files,
|
|
2218
|
-
nodeId: queryResult.nodeId
|
|
2219
|
-
});
|
|
2220
|
-
return "File upload completed";
|
|
2221
|
-
}
|
|
2222
|
-
async configureBrowser(params) {
|
|
2223
|
-
const { network, emulation, script } = params;
|
|
2224
|
-
const results = [];
|
|
2225
|
-
// Configure network settings
|
|
2226
|
-
if (network) {
|
|
2227
|
-
if (network.blockImages) {
|
|
2228
|
-
await this.client.sendCommand("Network.setBlockedURLs", {
|
|
2229
|
-
urls: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.webp", "*.svg"]
|
|
2230
|
-
});
|
|
2231
|
-
results.push("Blocked images");
|
|
2232
|
-
}
|
|
2233
|
-
if (network.blockCSS) {
|
|
2234
|
-
await this.client.sendCommand("Network.setBlockedURLs", {
|
|
2235
|
-
urls: ["*.css"]
|
|
2236
|
-
});
|
|
2237
|
-
results.push("Blocked CSS");
|
|
2238
|
-
}
|
|
2239
|
-
if (network.blockAds) {
|
|
2240
|
-
await this.client.sendCommand("Network.setBlockedURLs", {
|
|
2241
|
-
urls: ["*doubleclick.net*", "*googlesyndication.com*", "*adservice.*"]
|
|
2242
|
-
});
|
|
2243
|
-
results.push("Blocked ads");
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
// Configure emulation settings
|
|
2247
|
-
if (emulation) {
|
|
2248
|
-
if (emulation.width && emulation.height) {
|
|
2249
|
-
await this.client.sendCommand("Emulation.setDeviceMetricsOverride", {
|
|
2250
|
-
width: emulation.width,
|
|
2251
|
-
height: emulation.height,
|
|
2252
|
-
deviceScaleFactor: emulation.scale || 1,
|
|
2253
|
-
mobile: emulation.mobile || false,
|
|
2254
|
-
});
|
|
2255
|
-
results.push(`Emulated device: ${emulation.width}x${emulation.height}`);
|
|
2256
|
-
}
|
|
2257
|
-
if (emulation.userAgent) {
|
|
2258
|
-
await this.client.sendCommand("Emulation.setUserAgentOverride", {
|
|
2259
|
-
userAgent: emulation.userAgent,
|
|
2260
|
-
});
|
|
2261
|
-
results.push("Set custom user agent");
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
// Configure scripts
|
|
2265
|
-
if (script?.onLoad) {
|
|
2266
|
-
await this.client.sendCommand("Page.addScriptToEvaluateOnNewDocument", {
|
|
2267
|
-
source: script.onLoad,
|
|
2268
|
-
});
|
|
2269
|
-
results.push("Added script to run on new documents");
|
|
2270
|
-
}
|
|
2271
|
-
return {
|
|
2272
|
-
success: true,
|
|
2273
|
-
configured: results,
|
|
2274
|
-
message: results.join(", ") || "No configuration applied"
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
async launchBrowser(options) {
|
|
2278
|
-
const client = (0, cdp_client_1.getCdpClient)();
|
|
2279
|
-
await client.launchAuto({
|
|
2280
|
-
browser: options?.browser,
|
|
2281
|
-
headless: options?.headless,
|
|
2282
|
-
port: options?.port,
|
|
2283
|
-
profile: options?.profile,
|
|
2284
|
-
profileDirectory: options?.profileDirectory,
|
|
2285
|
-
userDataDir: options?.userDataDir,
|
|
2286
|
-
});
|
|
2287
|
-
const profileLabel = options?.profile || options?.profileDirectory;
|
|
2288
|
-
return profileLabel
|
|
2289
|
-
? `Browser launched successfully with profile "${profileLabel}"`
|
|
2290
|
-
: "Browser launched successfully";
|
|
2291
|
-
}
|
|
2292
|
-
async killBrowser() {
|
|
2293
|
-
const client = (0, cdp_client_1.getCdpClient)();
|
|
2294
|
-
await client.killBrowser();
|
|
2295
|
-
return "Browser killed";
|
|
2296
|
-
}
|
|
2297
|
-
async listBrowsers() {
|
|
2298
|
-
const client = (0, cdp_client_1.getCdpClient)();
|
|
2299
|
-
return await client.listAvailableBrowsers();
|
|
2300
|
-
}
|
|
2301
|
-
async listBrowserProfiles(browser) {
|
|
2302
|
-
const client = (0, cdp_client_1.getCdpClient)();
|
|
2303
|
-
return await client.listBrowserProfiles(browser || "brave");
|
|
2304
|
-
}
|
|
2305
|
-
async getTargetsViaHttp() {
|
|
2306
|
-
// This is a simplified version - in reality we'd need to know the port
|
|
2307
|
-
return [];
|
|
923
|
+
return { frameCount: frames.length, frames, message: `Retrieved ${frames.length} frames` };
|
|
2308
924
|
}
|
|
2309
925
|
}
|
|
2310
926
|
exports.CdpBridge = CdpBridge;
|
|
927
|
+
// Singleton
|
|
2311
928
|
let bridgeInstance = null;
|
|
2312
929
|
function getCdpBridge() {
|
|
2313
930
|
if (!bridgeInstance) {
|