camel-ai 0.2.72a8__py3-none-any.whl → 0.2.73a0__py3-none-any.whl
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.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +113 -338
- camel/memories/agent_memories.py +18 -17
- camel/societies/workforce/prompts.py +10 -4
- camel/societies/workforce/single_agent_worker.py +7 -5
- camel/toolkits/__init__.py +4 -1
- camel/toolkits/base.py +57 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +796 -1631
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +916 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +210 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +533 -0
- camel/toolkits/message_integration.py +592 -0
- camel/toolkits/note_taking_toolkit.py +18 -29
- camel/toolkits/screenshot_toolkit.py +116 -31
- camel/toolkits/search_toolkit.py +20 -2
- camel/toolkits/terminal_toolkit.py +16 -2
- camel/toolkits/video_analysis_toolkit.py +13 -13
- camel/toolkits/video_download_toolkit.py +11 -11
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73a0.dist-info}/METADATA +10 -4
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73a0.dist-info}/RECORD +31 -25
- camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
- camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -740
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
- camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73a0.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.72a8.dist-info → camel_ai-0.2.73a0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import {HybridBrowserSession} from './browser-session';
|
|
2
|
+
import {ActionResult, BrowserAction, BrowserToolkitConfig, SnapshotResult, TabInfo, VisualMarkResult} from './types';
|
|
3
|
+
import {ConfigLoader} from './config-loader';
|
|
4
|
+
|
|
5
|
+
export class HybridBrowserToolkit {
|
|
6
|
+
private session: HybridBrowserSession;
|
|
7
|
+
private config: BrowserToolkitConfig;
|
|
8
|
+
private configLoader: ConfigLoader;
|
|
9
|
+
private viewportLimit: boolean;
|
|
10
|
+
|
|
11
|
+
constructor(config: BrowserToolkitConfig = {}) {
|
|
12
|
+
this.configLoader = ConfigLoader.fromPythonConfig(config);
|
|
13
|
+
this.config = config; // Store original config for backward compatibility
|
|
14
|
+
this.session = new HybridBrowserSession(this.configLoader.getBrowserConfig()); // Pass processed config
|
|
15
|
+
this.viewportLimit = this.configLoader.getWebSocketConfig().viewport_limit;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async openBrowser(startUrl?: string): Promise<ActionResult> {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await this.session.ensureBrowser();
|
|
23
|
+
|
|
24
|
+
const url = startUrl || this.config.defaultStartUrl || 'https://google.com/';
|
|
25
|
+
const result = await this.session.visitPage(url);
|
|
26
|
+
|
|
27
|
+
const snapshotStart = Date.now();
|
|
28
|
+
const snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
29
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
30
|
+
|
|
31
|
+
const totalTime = Date.now() - startTime;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
message: `Browser opened and navigated to ${url}`,
|
|
36
|
+
snapshot,
|
|
37
|
+
timing: {
|
|
38
|
+
total_time_ms: totalTime,
|
|
39
|
+
...result.timing,
|
|
40
|
+
snapshot_time_ms: snapshotTime,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const totalTime = Date.now() - startTime;
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
message: `Failed to open browser: ${error}`,
|
|
48
|
+
timing: {
|
|
49
|
+
total_time_ms: totalTime,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async closeBrowser(): Promise<ActionResult> {
|
|
56
|
+
try {
|
|
57
|
+
await this.session.close();
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
message: 'Browser closed successfully',
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
message: `Failed to close browser: ${error}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async visitPage(url: string): Promise<any> {
|
|
71
|
+
const result = await this.session.visitPage(url);
|
|
72
|
+
|
|
73
|
+
// Format response for Python layer compatibility
|
|
74
|
+
const response: any = {
|
|
75
|
+
result: result.message,
|
|
76
|
+
snapshot: '',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (result.success) {
|
|
80
|
+
const snapshotStart = Date.now();
|
|
81
|
+
response.snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
82
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
83
|
+
|
|
84
|
+
if (result.timing) {
|
|
85
|
+
result.timing.snapshot_time_ms = snapshotTime;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Include timing if available
|
|
90
|
+
if (result.timing) {
|
|
91
|
+
response.timing = result.timing;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Include newTabId if present
|
|
95
|
+
if (result.newTabId) {
|
|
96
|
+
response.newTabId = result.newTabId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getPageSnapshot(viewportLimit: boolean = false): Promise<string> {
|
|
103
|
+
try {
|
|
104
|
+
// If viewport limiting is enabled, we need coordinates for filtering
|
|
105
|
+
const snapshotResult = await this.session.getSnapshotForAI(viewportLimit, viewportLimit);
|
|
106
|
+
return snapshotResult.snapshot;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return `Error capturing snapshot: ${error}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async getSnapshotForAI(): Promise<SnapshotResult> {
|
|
114
|
+
return await this.session.getSnapshotForAI();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getSomScreenshot(): Promise<VisualMarkResult & { timing: any }> {
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const screenshotResult = await this.session.takeScreenshot();
|
|
122
|
+
const snapshotResult = await this.session.getSnapshotForAI(true); // Include coordinates for SOM_mark
|
|
123
|
+
|
|
124
|
+
// Add visual marks using improved method
|
|
125
|
+
const markingStart = Date.now();
|
|
126
|
+
const markedImageBuffer = await this.addVisualMarksOptimized(screenshotResult.buffer, snapshotResult);
|
|
127
|
+
const markingTime = Date.now() - markingStart;
|
|
128
|
+
|
|
129
|
+
const base64Image = markedImageBuffer.toString('base64');
|
|
130
|
+
const dataUrl = `data:image/png;base64,${base64Image}`;
|
|
131
|
+
|
|
132
|
+
const totalTime = Date.now() - startTime;
|
|
133
|
+
|
|
134
|
+
// Count elements with coordinates
|
|
135
|
+
const elementsWithCoords = Object.values(snapshotResult.elements).filter(el => el.coordinates).length;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
text: `Visual webpage screenshot captured with ${Object.keys(snapshotResult.elements).length} interactive elements (${elementsWithCoords} marked visually)`,
|
|
139
|
+
images: [dataUrl],
|
|
140
|
+
timing: {
|
|
141
|
+
total_time_ms: totalTime,
|
|
142
|
+
screenshot_time_ms: screenshotResult.timing.screenshot_time_ms,
|
|
143
|
+
snapshot_time_ms: snapshotResult.timing.snapshot_time_ms,
|
|
144
|
+
coordinate_enrichment_time_ms: snapshotResult.timing.coordinate_enrichment_time_ms,
|
|
145
|
+
visual_marking_time_ms: markingTime,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const totalTime = Date.now() - startTime;
|
|
150
|
+
return {
|
|
151
|
+
text: `Error capturing screenshot: ${error}`,
|
|
152
|
+
images: [],
|
|
153
|
+
timing: {
|
|
154
|
+
total_time_ms: totalTime,
|
|
155
|
+
screenshot_time_ms: 0,
|
|
156
|
+
snapshot_time_ms: 0,
|
|
157
|
+
coordinate_enrichment_time_ms: 0,
|
|
158
|
+
visual_marking_time_ms: 0,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async addVisualMarksOptimized(screenshotBuffer: Buffer, snapshotResult: SnapshotResult): Promise<Buffer> {
|
|
165
|
+
try {
|
|
166
|
+
|
|
167
|
+
// Check if we have any elements with coordinates
|
|
168
|
+
const elementsWithCoords = Object.entries(snapshotResult.elements)
|
|
169
|
+
.filter(([ref, element]) => element.coordinates);
|
|
170
|
+
|
|
171
|
+
if (elementsWithCoords.length === 0) {
|
|
172
|
+
return screenshotBuffer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parse clickable elements from snapshot text
|
|
176
|
+
const clickableElements = this.parseClickableElements(snapshotResult.snapshot);
|
|
177
|
+
|
|
178
|
+
// Use sharp for image processing
|
|
179
|
+
const sharp = require('sharp');
|
|
180
|
+
const page = await this.session.getCurrentPage();
|
|
181
|
+
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
|
182
|
+
|
|
183
|
+
// Filter elements visible in viewport
|
|
184
|
+
const visibleElements = elementsWithCoords.filter(([ref, element]) => {
|
|
185
|
+
const coords = element.coordinates!;
|
|
186
|
+
return coords.x < viewport.width &&
|
|
187
|
+
coords.y < viewport.height &&
|
|
188
|
+
coords.x + coords.width > 0 &&
|
|
189
|
+
coords.y + coords.height > 0;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Remove overlapped elements (only keep topmost)
|
|
193
|
+
const nonOverlappedElements = this.removeOverlappedElements(visibleElements);
|
|
194
|
+
|
|
195
|
+
// Create SVG overlay with all the marks
|
|
196
|
+
const marks = nonOverlappedElements.map(([ref, element]) => {
|
|
197
|
+
const coords = element.coordinates!;
|
|
198
|
+
const isClickable = clickableElements.has(ref);
|
|
199
|
+
|
|
200
|
+
// Use original coordinates for elements within viewport
|
|
201
|
+
// Clamp only to prevent marks from extending beyond screenshot bounds
|
|
202
|
+
const x = Math.max(0, coords.x);
|
|
203
|
+
const y = Math.max(0, coords.y);
|
|
204
|
+
const maxWidth = viewport.width - x;
|
|
205
|
+
const maxHeight = viewport.height - y;
|
|
206
|
+
const width = Math.min(coords.width, maxWidth);
|
|
207
|
+
const height = Math.min(coords.height, maxHeight);
|
|
208
|
+
|
|
209
|
+
// Position text to be visible even if element is partially cut off
|
|
210
|
+
const textX = Math.max(2, Math.min(x + 2, viewport.width - 40));
|
|
211
|
+
const textY = Math.max(14, Math.min(y + 14, viewport.height - 4));
|
|
212
|
+
|
|
213
|
+
// Different colors for clickable vs non-clickable elements
|
|
214
|
+
const colors = isClickable ? {
|
|
215
|
+
fill: 'rgba(0, 150, 255, 0.15)', // Blue for clickable
|
|
216
|
+
stroke: '#0096FF',
|
|
217
|
+
textFill: '#0096FF'
|
|
218
|
+
} : {
|
|
219
|
+
fill: 'rgba(255, 107, 107, 0.1)', // Red for non-clickable
|
|
220
|
+
stroke: '#FF6B6B',
|
|
221
|
+
textFill: '#FF6B6B'
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return `
|
|
225
|
+
<rect x="${x}" y="${y}" width="${width}" height="${height}"
|
|
226
|
+
fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="2" rx="2"/>
|
|
227
|
+
<text x="${textX}" y="${textY}" font-family="Arial, sans-serif"
|
|
228
|
+
font-size="12" fill="${colors.textFill}" font-weight="bold">${ref}</text>
|
|
229
|
+
`;
|
|
230
|
+
}).join('');
|
|
231
|
+
|
|
232
|
+
const svgOverlay = `
|
|
233
|
+
<svg width="${viewport.width}" height="${viewport.height}" xmlns="http://www.w3.org/2000/svg">
|
|
234
|
+
${marks}
|
|
235
|
+
</svg>
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
// Composite the overlay onto the screenshot
|
|
239
|
+
const markedImageBuffer = await sharp(screenshotBuffer)
|
|
240
|
+
.composite([{
|
|
241
|
+
input: Buffer.from(svgOverlay),
|
|
242
|
+
top: 0,
|
|
243
|
+
left: 0
|
|
244
|
+
}])
|
|
245
|
+
.png()
|
|
246
|
+
.toBuffer();
|
|
247
|
+
|
|
248
|
+
return markedImageBuffer;
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// Error adding visual marks, falling back to original screenshot
|
|
252
|
+
// Return original screenshot if marking fails
|
|
253
|
+
return screenshotBuffer;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Parse clickable elements from snapshot text
|
|
259
|
+
*/
|
|
260
|
+
private parseClickableElements(snapshotText: string): Set<string> {
|
|
261
|
+
const clickableElements = new Set<string>();
|
|
262
|
+
const lines = snapshotText.split('\n');
|
|
263
|
+
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
// Look for lines containing [cursor=pointer] and extract ref
|
|
266
|
+
if (line.includes('[cursor=pointer]')) {
|
|
267
|
+
const refMatch = line.match(/\[ref=([^\]]+)\]/);
|
|
268
|
+
if (refMatch) {
|
|
269
|
+
clickableElements.add(refMatch[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return clickableElements;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Remove overlapped elements, keeping only the topmost (last in DOM order)
|
|
279
|
+
*/
|
|
280
|
+
private removeOverlappedElements(elements: Array<[string, any]>): Array<[string, any]> {
|
|
281
|
+
const result: Array<[string, any]> = [];
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < elements.length; i++) {
|
|
284
|
+
const [refA, elementA] = elements[i];
|
|
285
|
+
const coordsA = elementA.coordinates!;
|
|
286
|
+
let isOverlapped = false;
|
|
287
|
+
|
|
288
|
+
// Check if this element is completely overlapped by any later element
|
|
289
|
+
for (let j = i + 1; j < elements.length; j++) {
|
|
290
|
+
const [refB, elementB] = elements[j];
|
|
291
|
+
const coordsB = elementB.coordinates!;
|
|
292
|
+
|
|
293
|
+
// Check if element A is completely covered by element B
|
|
294
|
+
if (this.isCompletelyOverlapped(coordsA, coordsB)) {
|
|
295
|
+
isOverlapped = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isOverlapped) {
|
|
301
|
+
result.push(elements[i]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if element A is completely overlapped by element B
|
|
310
|
+
*/
|
|
311
|
+
private isCompletelyOverlapped(
|
|
312
|
+
coordsA: { x: number; y: number; width: number; height: number },
|
|
313
|
+
coordsB: { x: number; y: number; width: number; height: number }
|
|
314
|
+
): boolean {
|
|
315
|
+
// A is completely overlapped by B if:
|
|
316
|
+
// B's left edge is <= A's left edge AND
|
|
317
|
+
// B's top edge is <= A's top edge AND
|
|
318
|
+
// B's right edge is >= A's right edge AND
|
|
319
|
+
// B's bottom edge is >= A's bottom edge
|
|
320
|
+
return (
|
|
321
|
+
coordsB.x <= coordsA.x &&
|
|
322
|
+
coordsB.y <= coordsA.y &&
|
|
323
|
+
coordsB.x + coordsB.width >= coordsA.x + coordsA.width &&
|
|
324
|
+
coordsB.y + coordsB.height >= coordsA.y + coordsA.height
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async executeActionWithSnapshot(action: BrowserAction): Promise<any> {
|
|
329
|
+
const result = await this.session.executeAction(action);
|
|
330
|
+
|
|
331
|
+
// Format response for Python layer compatibility
|
|
332
|
+
const response: any = {
|
|
333
|
+
result: result.message,
|
|
334
|
+
snapshot: '',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (result.success) {
|
|
338
|
+
const snapshotStart = Date.now();
|
|
339
|
+
response.snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
340
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
341
|
+
|
|
342
|
+
if (result.timing) {
|
|
343
|
+
result.timing.snapshot_time_ms = snapshotTime;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Include timing if available
|
|
348
|
+
if (result.timing) {
|
|
349
|
+
response.timing = result.timing;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Include newTabId if present
|
|
353
|
+
if (result.newTabId) {
|
|
354
|
+
response.newTabId = result.newTabId;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return response;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async click(ref: string): Promise<any> {
|
|
361
|
+
const action: BrowserAction = { type: 'click', ref };
|
|
362
|
+
return this.executeActionWithSnapshot(action);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async type(ref: string, text: string): Promise<any> {
|
|
366
|
+
const action: BrowserAction = { type: 'type', ref, text };
|
|
367
|
+
return this.executeActionWithSnapshot(action);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async select(ref: string, value: string): Promise<any> {
|
|
371
|
+
const action: BrowserAction = { type: 'select', ref, value };
|
|
372
|
+
return this.executeActionWithSnapshot(action);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async scroll(direction: 'up' | 'down', amount: number): Promise<any> {
|
|
376
|
+
const action: BrowserAction = { type: 'scroll', direction, amount };
|
|
377
|
+
return this.executeActionWithSnapshot(action);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async enter(): Promise<any> {
|
|
381
|
+
const action: BrowserAction = { type: 'enter' };
|
|
382
|
+
return this.executeActionWithSnapshot(action);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async back(): Promise<ActionResult> {
|
|
386
|
+
const startTime = Date.now();
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const page = await this.session.getCurrentPage();
|
|
390
|
+
|
|
391
|
+
const navigationStart = Date.now();
|
|
392
|
+
await page.goBack({ waitUntil: 'domcontentloaded' });
|
|
393
|
+
const navigationTime = Date.now() - navigationStart;
|
|
394
|
+
|
|
395
|
+
const snapshotStart = Date.now();
|
|
396
|
+
const snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
397
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
398
|
+
|
|
399
|
+
const totalTime = Date.now() - startTime;
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
message: 'Navigated back successfully',
|
|
404
|
+
snapshot,
|
|
405
|
+
timing: {
|
|
406
|
+
total_time_ms: totalTime,
|
|
407
|
+
navigation_time_ms: navigationTime,
|
|
408
|
+
snapshot_time_ms: snapshotTime,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const totalTime = Date.now() - startTime;
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
message: `Back navigation failed: ${error}`,
|
|
416
|
+
timing: {
|
|
417
|
+
total_time_ms: totalTime,
|
|
418
|
+
navigation_time_ms: 0,
|
|
419
|
+
snapshot_time_ms: 0,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async forward(): Promise<ActionResult> {
|
|
426
|
+
const startTime = Date.now();
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const page = await this.session.getCurrentPage();
|
|
430
|
+
|
|
431
|
+
const navigationStart = Date.now();
|
|
432
|
+
await page.goForward({ waitUntil: 'domcontentloaded' });
|
|
433
|
+
const navigationTime = Date.now() - navigationStart;
|
|
434
|
+
|
|
435
|
+
const snapshotStart = Date.now();
|
|
436
|
+
const snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
437
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
438
|
+
|
|
439
|
+
const totalTime = Date.now() - startTime;
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
success: true,
|
|
443
|
+
message: 'Navigated forward successfully',
|
|
444
|
+
snapshot,
|
|
445
|
+
timing: {
|
|
446
|
+
total_time_ms: totalTime,
|
|
447
|
+
navigation_time_ms: navigationTime,
|
|
448
|
+
snapshot_time_ms: snapshotTime,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const totalTime = Date.now() - startTime;
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
message: `Forward navigation failed: ${error}`,
|
|
456
|
+
timing: {
|
|
457
|
+
total_time_ms: totalTime,
|
|
458
|
+
navigation_time_ms: 0,
|
|
459
|
+
snapshot_time_ms: 0,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
async switchTab(tabId: string): Promise<any> {
|
|
467
|
+
const startTime = Date.now();
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const success = await this.session.switchToTab(tabId);
|
|
471
|
+
|
|
472
|
+
if (success) {
|
|
473
|
+
const snapshotStart = Date.now();
|
|
474
|
+
const snapshot = await this.getPageSnapshot(this.viewportLimit);
|
|
475
|
+
const snapshotTime = Date.now() - snapshotStart;
|
|
476
|
+
|
|
477
|
+
const totalTime = Date.now() - startTime;
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
result: `Switched to tab ${tabId}`,
|
|
481
|
+
snapshot: snapshot,
|
|
482
|
+
timing: {
|
|
483
|
+
total_time_ms: totalTime,
|
|
484
|
+
snapshot_time_ms: snapshotTime,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
} else {
|
|
488
|
+
return {
|
|
489
|
+
result: `Failed to switch to tab ${tabId}`,
|
|
490
|
+
snapshot: '',
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return {
|
|
495
|
+
result: `Error switching tab: ${error}`,
|
|
496
|
+
snapshot: '',
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async closeTab(tabId: string): Promise<ActionResult> {
|
|
502
|
+
const success = await this.session.closeTab(tabId);
|
|
503
|
+
|
|
504
|
+
if (success) {
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
message: `Closed tab ${tabId}`,
|
|
508
|
+
snapshot: await this.getPageSnapshot(this.viewportLimit),
|
|
509
|
+
};
|
|
510
|
+
} else {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
message: `Failed to close tab ${tabId}`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async getTabInfo(): Promise<TabInfo[]> {
|
|
519
|
+
return await this.session.getTabInfo();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { HybridBrowserToolkit } from './hybrid-browser-toolkit';
|
|
2
|
+
export { HybridBrowserSession } from './browser-session';
|
|
3
|
+
export { ConfigLoader, StealthConfig, BrowserConfig, WebSocketConfig } from './config-loader';
|
|
4
|
+
export * from './types';
|
|
5
|
+
|
|
6
|
+
// Default export for convenience
|
|
7
|
+
export { HybridBrowserToolkit as default } from './hybrid-browser-toolkit';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export interface SnapshotElement {
|
|
2
|
+
ref: string;
|
|
3
|
+
role: string;
|
|
4
|
+
name: string;
|
|
5
|
+
coordinates?: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
checked?: boolean;
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
tagName?: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export interface SnapshotResult {
|
|
20
|
+
snapshot: string;
|
|
21
|
+
elements: Record<string, SnapshotElement>;
|
|
22
|
+
metadata: {
|
|
23
|
+
elementCount: number;
|
|
24
|
+
url: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DetailedTiming {
|
|
30
|
+
total_time_ms: number;
|
|
31
|
+
navigation_time_ms?: number;
|
|
32
|
+
page_load_time_ms?: number;
|
|
33
|
+
stability_wait_time_ms?: number;
|
|
34
|
+
dom_content_loaded_time_ms?: number;
|
|
35
|
+
network_idle_time_ms?: number;
|
|
36
|
+
snapshot_time_ms?: number;
|
|
37
|
+
element_search_time_ms?: number;
|
|
38
|
+
action_execution_time_ms?: number;
|
|
39
|
+
screenshot_time_ms?: number;
|
|
40
|
+
coordinate_enrichment_time_ms?: number;
|
|
41
|
+
visual_marking_time_ms?: number;
|
|
42
|
+
aria_mapping_time_ms?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ActionResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
snapshot?: string;
|
|
49
|
+
details?: Record<string, any>;
|
|
50
|
+
timing?: DetailedTiming;
|
|
51
|
+
newTabId?: string; // ID of newly opened tab if click opened a new tab
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TabInfo {
|
|
55
|
+
tab_id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
url: string;
|
|
58
|
+
is_current: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
import { StealthConfig } from './config-loader';
|
|
62
|
+
|
|
63
|
+
export interface BrowserToolkitConfig {
|
|
64
|
+
headless?: boolean;
|
|
65
|
+
userDataDir?: string;
|
|
66
|
+
stealth?: boolean | StealthConfig; // Support both legacy boolean and new object format
|
|
67
|
+
defaultStartUrl?: string;
|
|
68
|
+
navigationTimeout?: number;
|
|
69
|
+
networkIdleTimeout?: number;
|
|
70
|
+
screenshotTimeout?: number;
|
|
71
|
+
pageStabilityTimeout?: number;
|
|
72
|
+
useNativePlaywrightMapping?: boolean; // New option to control mapping implementation
|
|
73
|
+
connectOverCdp?: boolean; // Whether to connect to existing browser via CDP
|
|
74
|
+
cdpUrl?: string; // WebSocket endpoint URL for CDP connection
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ClickAction {
|
|
78
|
+
type: 'click';
|
|
79
|
+
ref: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TypeAction {
|
|
83
|
+
type: 'type';
|
|
84
|
+
ref: string;
|
|
85
|
+
text: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SelectAction {
|
|
89
|
+
type: 'select';
|
|
90
|
+
ref: string;
|
|
91
|
+
value: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ScrollAction {
|
|
95
|
+
type: 'scroll';
|
|
96
|
+
direction: 'up' | 'down';
|
|
97
|
+
amount: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface EnterAction {
|
|
101
|
+
type: 'enter';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type BrowserAction = ClickAction | TypeAction | SelectAction | ScrollAction | EnterAction;
|
|
105
|
+
|
|
106
|
+
export interface VisualMarkResult {
|
|
107
|
+
text: string;
|
|
108
|
+
images: string[];
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*"
|
|
20
|
+
],
|
|
21
|
+
"exclude": [
|
|
22
|
+
"node_modules",
|
|
23
|
+
"dist",
|
|
24
|
+
"**/*.test.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|