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