camel-ai 0.2.71a7__py3-none-any.whl → 0.2.71a9__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/societies/workforce/single_agent_worker.py +53 -9
- camel/societies/workforce/task_channel.py +4 -1
- camel/societies/workforce/workforce.py +146 -14
- camel/tasks/task.py +104 -4
- camel/toolkits/file_write_toolkit.py +19 -8
- camel/toolkits/hybrid_browser_toolkit/actions.py +28 -18
- camel/toolkits/hybrid_browser_toolkit/agent.py +7 -1
- camel/toolkits/hybrid_browser_toolkit/browser_session.py +48 -18
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +447 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +272 -85
- camel/toolkits/hybrid_browser_toolkit/snapshot.py +5 -4
- camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +572 -17
- camel/toolkits/note_taking_toolkit.py +24 -9
- camel/toolkits/pptx_toolkit.py +21 -8
- camel/toolkits/search_toolkit.py +15 -5
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/METADATA +1 -1
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/RECORD +20 -20
- camel/toolkits/hybrid_browser_toolkit/stealth_config.py +0 -116
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.71a7.dist-info → camel_ai-0.2.71a9.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,9 +2,302 @@
|
|
|
2
2
|
// Unified analyzer that combines visual and structural analysis
|
|
3
3
|
// Preserves complete snapshot.js logic while adding visual coordinate information
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Memory management constants and configuration
|
|
6
|
+
const MAX_REFS = 2000; // Maximum number of refs to keep in memory
|
|
7
|
+
const MAX_UNUSED_AGE_MS = 90000; // Remove refs unused for more than xx seconds
|
|
8
|
+
const CLEANUP_THRESHOLD = 0.8; // Start aggressive cleanup when 80% of max refs reached
|
|
9
|
+
|
|
10
|
+
// Persistent ref management across page analysis calls with memory leak prevention
|
|
11
|
+
let refCounter = window.__camelRefCounter || 1;
|
|
12
|
+
let elementRefMap = window.__camelElementRefMap || new WeakMap();
|
|
13
|
+
let refElementMap = window.__camelRefElementMap || new Map();
|
|
14
|
+
let elementSignatureMap = window.__camelElementSignatureMap || new Map();
|
|
15
|
+
|
|
16
|
+
// LRU tracking for ref access times
|
|
17
|
+
let refAccessTimes = window.__camelRefAccessTimes || new Map();
|
|
18
|
+
let lastNavigationUrl = window.__camelLastNavigationUrl || window.location.href;
|
|
19
|
+
|
|
20
|
+
// Initialize navigation event listeners for automatic cleanup
|
|
21
|
+
if (!window.__camelNavigationListenersInitialized) {
|
|
22
|
+
window.__camelNavigationListenersInitialized = true;
|
|
23
|
+
|
|
24
|
+
// Listen for page navigation events
|
|
25
|
+
window.addEventListener('beforeunload', clearAllRefs);
|
|
26
|
+
window.addEventListener('pagehide', clearAllRefs);
|
|
27
|
+
|
|
28
|
+
// Listen for pushState/replaceState navigation (SPA navigation)
|
|
29
|
+
const originalPushState = history.pushState;
|
|
30
|
+
const originalReplaceState = history.replaceState;
|
|
31
|
+
|
|
32
|
+
history.pushState = function(...args) {
|
|
33
|
+
clearAllRefs();
|
|
34
|
+
return originalPushState.apply(this, args);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
history.replaceState = function(...args) {
|
|
38
|
+
clearAllRefs();
|
|
39
|
+
return originalReplaceState.apply(this, args);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Listen for popstate (back/forward navigation)
|
|
43
|
+
window.addEventListener('popstate', clearAllRefs);
|
|
44
|
+
|
|
45
|
+
// Check for URL changes periodically (fallback for other navigation types)
|
|
46
|
+
setInterval(() => {
|
|
47
|
+
if (window.location.href !== lastNavigationUrl) {
|
|
48
|
+
clearAllRefs();
|
|
49
|
+
lastNavigationUrl = window.location.href;
|
|
50
|
+
window.__camelLastNavigationUrl = lastNavigationUrl;
|
|
51
|
+
}
|
|
52
|
+
}, 1000);
|
|
53
|
+
}
|
|
54
|
+
|
|
6
55
|
function generateRef() {
|
|
7
|
-
|
|
56
|
+
const ref = `e${refCounter++}`;
|
|
57
|
+
// Persist counter globally
|
|
58
|
+
window.__camelRefCounter = refCounter;
|
|
59
|
+
return ref;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Clear all refs and reset memory state
|
|
63
|
+
function clearAllRefs() {
|
|
64
|
+
try {
|
|
65
|
+
// Clear all DOM aria-ref attributes
|
|
66
|
+
document.querySelectorAll('[aria-ref]').forEach(element => {
|
|
67
|
+
element.removeAttribute('aria-ref');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Clear all maps and reset counters
|
|
71
|
+
elementRefMap.clear();
|
|
72
|
+
refElementMap.clear();
|
|
73
|
+
elementSignatureMap.clear();
|
|
74
|
+
refAccessTimes.clear();
|
|
75
|
+
|
|
76
|
+
// Reset global state
|
|
77
|
+
window.__camelElementRefMap = elementRefMap;
|
|
78
|
+
window.__camelRefElementMap = refElementMap;
|
|
79
|
+
window.__camelElementSignatureMap = elementSignatureMap;
|
|
80
|
+
window.__camelRefAccessTimes = refAccessTimes;
|
|
81
|
+
|
|
82
|
+
// Clear cached analysis results
|
|
83
|
+
delete window.__camelLastAnalysisResult;
|
|
84
|
+
delete window.__camelLastAnalysisTime;
|
|
85
|
+
|
|
86
|
+
console.log('CAMEL: Cleared all refs due to navigation');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn('CAMEL: Error clearing refs:', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// LRU eviction: Remove least recently used refs when limit exceeded
|
|
93
|
+
function evictLRURefs() {
|
|
94
|
+
const refsToEvict = refAccessTimes.size - MAX_REFS + Math.floor(MAX_REFS * 0.1); // Remove 10% extra for breathing room
|
|
95
|
+
if (refsToEvict <= 0) return 0;
|
|
96
|
+
|
|
97
|
+
// Sort refs by access time (oldest first)
|
|
98
|
+
const sortedRefs = Array.from(refAccessTimes.entries())
|
|
99
|
+
.sort((a, b) => a[1] - b[1])
|
|
100
|
+
.slice(0, refsToEvict);
|
|
101
|
+
|
|
102
|
+
let evictedCount = 0;
|
|
103
|
+
for (const [ref, _] of sortedRefs) {
|
|
104
|
+
const element = refElementMap.get(ref);
|
|
105
|
+
if (element) {
|
|
106
|
+
// Remove aria-ref attribute from DOM
|
|
107
|
+
try {
|
|
108
|
+
element.removeAttribute('aria-ref');
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Element might be detached from DOM
|
|
111
|
+
}
|
|
112
|
+
elementRefMap.delete(element);
|
|
113
|
+
|
|
114
|
+
// Remove from signature map
|
|
115
|
+
const signature = generateElementSignature(element);
|
|
116
|
+
if (signature && elementSignatureMap.get(signature) === ref) {
|
|
117
|
+
elementSignatureMap.delete(signature);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
refElementMap.delete(ref);
|
|
122
|
+
refAccessTimes.delete(ref);
|
|
123
|
+
evictedCount++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Persist updated maps
|
|
127
|
+
window.__camelElementRefMap = elementRefMap;
|
|
128
|
+
window.__camelRefElementMap = refElementMap;
|
|
129
|
+
window.__camelElementSignatureMap = elementSignatureMap;
|
|
130
|
+
window.__camelRefAccessTimes = refAccessTimes;
|
|
131
|
+
|
|
132
|
+
return evictedCount;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Update ref access time for LRU tracking
|
|
136
|
+
function updateRefAccessTime(ref) {
|
|
137
|
+
refAccessTimes.set(ref, Date.now());
|
|
138
|
+
window.__camelRefAccessTimes = refAccessTimes;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Generate a unique signature for an element based on its characteristics
|
|
142
|
+
function generateElementSignature(element) {
|
|
143
|
+
if (!element || !element.tagName) return null;
|
|
144
|
+
|
|
145
|
+
const tagName = element.tagName.toLowerCase();
|
|
146
|
+
const textContent = (element.textContent || '').trim().substring(0, 50);
|
|
147
|
+
const className = element.className || '';
|
|
148
|
+
const id = element.id || '';
|
|
149
|
+
const href = element.href || '';
|
|
150
|
+
const src = element.src || '';
|
|
151
|
+
const value = element.value || '';
|
|
152
|
+
const type = element.type || '';
|
|
153
|
+
const placeholder = element.placeholder || '';
|
|
154
|
+
|
|
155
|
+
// Include position in DOM tree for uniqueness
|
|
156
|
+
let pathElements = [];
|
|
157
|
+
let current = element;
|
|
158
|
+
let depth = 0;
|
|
159
|
+
while (current && current.parentElement && depth < 5) {
|
|
160
|
+
const siblings = Array.from(current.parentElement.children);
|
|
161
|
+
const index = siblings.indexOf(current);
|
|
162
|
+
pathElements.unshift(`${current.tagName.toLowerCase()}[${index}]`);
|
|
163
|
+
current = current.parentElement;
|
|
164
|
+
depth++;
|
|
165
|
+
}
|
|
166
|
+
const domPath = pathElements.join('>');
|
|
167
|
+
|
|
168
|
+
return `${tagName}|${textContent}|${className}|${id}|${href}|${src}|${value}|${type}|${placeholder}|${domPath}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get or assign a persistent ref for an element
|
|
172
|
+
function getOrAssignRef(element) {
|
|
173
|
+
// Check if element already has a ref assigned
|
|
174
|
+
if (elementRefMap.has(element)) {
|
|
175
|
+
const existingRef = elementRefMap.get(element);
|
|
176
|
+
// Verify the ref is still valid
|
|
177
|
+
if (refElementMap.get(existingRef) === element) {
|
|
178
|
+
updateRefAccessTime(existingRef);
|
|
179
|
+
return existingRef;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if element has aria-ref attribute (from previous analysis)
|
|
184
|
+
const existingAriaRef = element.getAttribute('aria-ref');
|
|
185
|
+
if (existingAriaRef && refElementMap.get(existingAriaRef) === element) {
|
|
186
|
+
// Re-establish mappings
|
|
187
|
+
elementRefMap.set(element, existingAriaRef);
|
|
188
|
+
updateRefAccessTime(existingAriaRef);
|
|
189
|
+
return existingAriaRef;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Try to find element by signature (in case DOM was modified)
|
|
193
|
+
const signature = generateElementSignature(element);
|
|
194
|
+
if (signature && elementSignatureMap.has(signature)) {
|
|
195
|
+
const existingRef = elementSignatureMap.get(signature);
|
|
196
|
+
// Verify the old element is no longer in DOM or has changed
|
|
197
|
+
const oldElement = refElementMap.get(existingRef);
|
|
198
|
+
if (!oldElement || !document.contains(oldElement) || generateElementSignature(oldElement) !== signature) {
|
|
199
|
+
// Reassign the ref to the new element
|
|
200
|
+
elementRefMap.set(element, existingRef);
|
|
201
|
+
refElementMap.set(existingRef, element);
|
|
202
|
+
elementSignatureMap.set(signature, existingRef);
|
|
203
|
+
element.setAttribute('aria-ref', existingRef);
|
|
204
|
+
updateRefAccessTime(existingRef);
|
|
205
|
+
return existingRef;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if we need to evict refs before creating new ones
|
|
210
|
+
if (refElementMap.size >= MAX_REFS) {
|
|
211
|
+
evictLRURefs();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generate new ref for new element
|
|
215
|
+
const newRef = generateRef();
|
|
216
|
+
elementRefMap.set(element, newRef);
|
|
217
|
+
refElementMap.set(newRef, element);
|
|
218
|
+
if (signature) {
|
|
219
|
+
elementSignatureMap.set(signature, newRef);
|
|
220
|
+
}
|
|
221
|
+
element.setAttribute('aria-ref', newRef);
|
|
222
|
+
updateRefAccessTime(newRef);
|
|
223
|
+
return newRef;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Enhanced cleanup function with aggressive stale ref removal
|
|
227
|
+
function cleanupStaleRefs() {
|
|
228
|
+
const staleRefs = [];
|
|
229
|
+
const currentTime = Date.now();
|
|
230
|
+
const isAggressiveCleanup = refElementMap.size > (MAX_REFS * CLEANUP_THRESHOLD);
|
|
231
|
+
|
|
232
|
+
// Check all mapped elements to see if they're still in DOM or too old
|
|
233
|
+
for (const [ref, element] of refElementMap.entries()) {
|
|
234
|
+
let shouldRemove = false;
|
|
235
|
+
|
|
236
|
+
// Standard checks: element not in DOM
|
|
237
|
+
if (!element || !document.contains(element)) {
|
|
238
|
+
shouldRemove = true;
|
|
239
|
+
}
|
|
240
|
+
// Aggressive cleanup: remove refs unused for too long
|
|
241
|
+
else if (isAggressiveCleanup) {
|
|
242
|
+
const lastAccess = refAccessTimes.get(ref) || 0;
|
|
243
|
+
const age = currentTime - lastAccess;
|
|
244
|
+
if (age > MAX_UNUSED_AGE_MS) {
|
|
245
|
+
shouldRemove = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Additional checks for aggressive cleanup
|
|
249
|
+
else if (isAggressiveCleanup) {
|
|
250
|
+
// Remove refs for elements that are hidden or have no meaningful content
|
|
251
|
+
try {
|
|
252
|
+
const style = window.getComputedStyle(element);
|
|
253
|
+
const hasNoVisibleContent = !element.textContent?.trim() &&
|
|
254
|
+
!element.value?.trim() &&
|
|
255
|
+
!element.src &&
|
|
256
|
+
!element.href;
|
|
257
|
+
|
|
258
|
+
if ((style.display === 'none' || style.visibility === 'hidden') && hasNoVisibleContent) {
|
|
259
|
+
shouldRemove = true;
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// If we can't get computed style, element might be detached
|
|
263
|
+
shouldRemove = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (shouldRemove) {
|
|
268
|
+
staleRefs.push(ref);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Remove stale mappings
|
|
273
|
+
for (const ref of staleRefs) {
|
|
274
|
+
const element = refElementMap.get(ref);
|
|
275
|
+
if (element) {
|
|
276
|
+
// Remove aria-ref attribute from DOM
|
|
277
|
+
try {
|
|
278
|
+
element.removeAttribute('aria-ref');
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// Element might be detached from DOM
|
|
281
|
+
}
|
|
282
|
+
elementRefMap.delete(element);
|
|
283
|
+
|
|
284
|
+
// Remove from signature map
|
|
285
|
+
const signature = generateElementSignature(element);
|
|
286
|
+
if (signature && elementSignatureMap.get(signature) === ref) {
|
|
287
|
+
elementSignatureMap.delete(signature);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
refElementMap.delete(ref);
|
|
291
|
+
refAccessTimes.delete(ref);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Persist maps globally
|
|
295
|
+
window.__camelElementRefMap = elementRefMap;
|
|
296
|
+
window.__camelRefElementMap = refElementMap;
|
|
297
|
+
window.__camelElementSignatureMap = elementSignatureMap;
|
|
298
|
+
window.__camelRefAccessTimes = refAccessTimes;
|
|
299
|
+
|
|
300
|
+
return staleRefs.length;
|
|
8
301
|
}
|
|
9
302
|
|
|
10
303
|
// === Complete snapshot.js logic preservation ===
|
|
@@ -15,7 +308,7 @@
|
|
|
15
308
|
if (node.nodeType !== Node.ELEMENT_NODE) return true;
|
|
16
309
|
|
|
17
310
|
try {
|
|
18
|
-
|
|
311
|
+
const style = window.getComputedStyle(node);
|
|
19
312
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
20
313
|
return false;
|
|
21
314
|
// An element with `display: contents` is not rendered itself, but its children are.
|
|
@@ -29,6 +322,55 @@
|
|
|
29
322
|
}
|
|
30
323
|
}
|
|
31
324
|
|
|
325
|
+
// Optimized occlusion detection with fewer test points
|
|
326
|
+
function isOccluded(element) {
|
|
327
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const rect = element.getBoundingClientRect();
|
|
331
|
+
if (rect.width === 0 || rect.height === 0) return true;
|
|
332
|
+
|
|
333
|
+
// Simplified: Use fewer test points for better performance
|
|
334
|
+
const testPoints = [
|
|
335
|
+
// Center point (most important)
|
|
336
|
+
{ x: rect.left + rect.width * 0.5, y: rect.top + rect.height * 0.5, weight: 4 },
|
|
337
|
+
// Only test 4 strategic points instead of 9
|
|
338
|
+
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25, weight: 1 },
|
|
339
|
+
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25, weight: 1 },
|
|
340
|
+
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75, weight: 1 },
|
|
341
|
+
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75, weight: 1 }
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
let totalWeight = 0;
|
|
345
|
+
let visibleWeight = 0;
|
|
346
|
+
|
|
347
|
+
for (const point of testPoints) {
|
|
348
|
+
// Skip points outside viewport
|
|
349
|
+
if (point.x < 0 || point.y < 0 ||
|
|
350
|
+
point.x >= window.innerWidth || point.y >= window.innerHeight) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const hitElement = document.elementFromPoint(point.x, point.y);
|
|
355
|
+
totalWeight += point.weight;
|
|
356
|
+
|
|
357
|
+
// Simplified visibility check
|
|
358
|
+
if (hitElement && (hitElement === element || element.contains(hitElement) || hitElement.contains(element))) {
|
|
359
|
+
visibleWeight += point.weight;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// If no valid test points, assume not occluded
|
|
364
|
+
if (totalWeight === 0) return false;
|
|
365
|
+
|
|
366
|
+
// Element is occluded if less than 40% of weighted points are visible
|
|
367
|
+
return (visibleWeight / totalWeight) < 0.4;
|
|
368
|
+
|
|
369
|
+
} catch (e) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
32
374
|
function getRole(node) {
|
|
33
375
|
// Check if node is null or doesn't have required properties
|
|
34
376
|
if (!node || !node.tagName || !node.getAttribute) {
|
|
@@ -39,6 +381,8 @@
|
|
|
39
381
|
if (role) return role;
|
|
40
382
|
|
|
41
383
|
const tagName = node.tagName.toLowerCase();
|
|
384
|
+
|
|
385
|
+
// Extended role mapping to better match Playwright
|
|
42
386
|
if (tagName === 'a') return 'link';
|
|
43
387
|
if (tagName === 'button') return 'button';
|
|
44
388
|
if (tagName === 'input') {
|
|
@@ -48,9 +392,83 @@
|
|
|
48
392
|
}
|
|
49
393
|
if (['select', 'textarea'].includes(tagName)) return tagName;
|
|
50
394
|
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) return 'heading';
|
|
395
|
+
|
|
396
|
+
// Additional roles for better Playwright compatibility
|
|
397
|
+
if (tagName === 'img') return 'img';
|
|
398
|
+
if (tagName === 'main') return 'main';
|
|
399
|
+
if (tagName === 'nav') return 'navigation';
|
|
400
|
+
if (tagName === 'ul' || tagName === 'ol') return 'list';
|
|
401
|
+
if (tagName === 'li') return 'listitem';
|
|
402
|
+
if (tagName === 'em') return 'emphasis';
|
|
403
|
+
if (tagName === 'form' && node.getAttribute('role') === 'search') return 'search';
|
|
404
|
+
if (tagName === 'section' || tagName === 'article') return 'region';
|
|
405
|
+
if (tagName === 'aside') return 'complementary';
|
|
406
|
+
if (tagName === 'header') return 'banner';
|
|
407
|
+
if (tagName === 'footer') return 'contentinfo';
|
|
408
|
+
if (tagName === 'fieldset') return 'group';
|
|
409
|
+
|
|
51
410
|
return 'generic';
|
|
52
411
|
}
|
|
53
412
|
|
|
413
|
+
// Playwright-inspired function to check if element receives pointer events
|
|
414
|
+
function receivesPointerEvents(element) {
|
|
415
|
+
if (!element || !element.nodeType || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
let e = element;
|
|
419
|
+
while (e) {
|
|
420
|
+
const style = window.getComputedStyle(e);
|
|
421
|
+
if (!style) break;
|
|
422
|
+
|
|
423
|
+
const pointerEvents = style.pointerEvents;
|
|
424
|
+
if (pointerEvents === 'none') return false;
|
|
425
|
+
if (pointerEvents && pointerEvents !== 'auto') return true;
|
|
426
|
+
|
|
427
|
+
e = e.parentElement;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Playwright-inspired function to check if element has pointer cursor
|
|
436
|
+
function hasPointerCursor(element) {
|
|
437
|
+
if (!element || !element.nodeType || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const style = window.getComputedStyle(element);
|
|
441
|
+
return style.cursor === 'pointer';
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Playwright-inspired function to get aria level
|
|
448
|
+
function getAriaLevel(element) {
|
|
449
|
+
if (!element || !element.tagName) return 0;
|
|
450
|
+
|
|
451
|
+
// Native HTML heading levels (H1=1, H2=2, etc.)
|
|
452
|
+
const tagName = element.tagName.toUpperCase();
|
|
453
|
+
const nativeLevel = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[tagName];
|
|
454
|
+
if (nativeLevel) return nativeLevel;
|
|
455
|
+
|
|
456
|
+
// Check aria-level attribute for roles that support it
|
|
457
|
+
const role = getRole(element);
|
|
458
|
+
const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
|
|
459
|
+
if (kAriaLevelRoles.includes(role)) {
|
|
460
|
+
const ariaLevel = element.getAttribute('aria-level');
|
|
461
|
+
if (ariaLevel !== null) {
|
|
462
|
+
const value = Number(ariaLevel);
|
|
463
|
+
if (Number.isInteger(value) && value >= 1) {
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
54
472
|
function getAccessibleName(node) {
|
|
55
473
|
// Check if node is null or doesn't have required methods
|
|
56
474
|
if (!node || !node.hasAttribute || !node.getAttribute) return '';
|
|
@@ -67,7 +485,7 @@
|
|
|
67
485
|
// Add a heuristic to ignore code-like text that might be in the DOM
|
|
68
486
|
if ((text.match(/[;:{}]/g)?.length || 0) > 2) return '';
|
|
69
487
|
return text;
|
|
70
|
-
|
|
488
|
+
}
|
|
71
489
|
|
|
72
490
|
const textCache = new Map();
|
|
73
491
|
function getVisibleTextContent(_node) {
|
|
@@ -116,22 +534,49 @@
|
|
|
116
534
|
|
|
117
535
|
const name = getAccessibleName(element);
|
|
118
536
|
|
|
537
|
+
// Get persistent ref for this element
|
|
538
|
+
const ref = getOrAssignRef(element);
|
|
539
|
+
|
|
119
540
|
// Create the node
|
|
120
541
|
const node = {
|
|
121
542
|
role,
|
|
122
543
|
name,
|
|
123
544
|
children: [],
|
|
124
545
|
element: element,
|
|
125
|
-
ref:
|
|
546
|
+
ref: ref,
|
|
126
547
|
};
|
|
127
548
|
|
|
128
549
|
// Add states for interactive elements, similar to Playwright
|
|
129
|
-
if (element.hasAttribute('disabled')) node.disabled = true;
|
|
130
|
-
|
|
131
|
-
if
|
|
550
|
+
if (element.hasAttribute('disabled') || element.disabled) node.disabled = true;
|
|
551
|
+
|
|
552
|
+
// NEW: Check if element is occluded and mark with occluded tag
|
|
553
|
+
if (isOccluded(element)) {
|
|
554
|
+
node.occluded = true; // Mark as occluded but don't disable
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Handle aria-checked and native checked
|
|
558
|
+
const ariaChecked = element.getAttribute('aria-checked');
|
|
559
|
+
if (ariaChecked) {
|
|
560
|
+
node.checked = ariaChecked;
|
|
561
|
+
} else if (element.type === 'checkbox' || element.type === 'radio') {
|
|
562
|
+
node.checked = element.checked;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Handle aria-expanded
|
|
566
|
+
const ariaExpanded = element.getAttribute('aria-expanded');
|
|
567
|
+
if (ariaExpanded) {
|
|
568
|
+
node.expanded = ariaExpanded === 'true';
|
|
569
|
+
}
|
|
132
570
|
|
|
133
|
-
//
|
|
134
|
-
element.
|
|
571
|
+
// Handle aria-selected
|
|
572
|
+
const ariaSelected = element.getAttribute('aria-selected');
|
|
573
|
+
if (ariaSelected === 'true') {
|
|
574
|
+
node.selected = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Add level support following Playwright's implementation
|
|
578
|
+
const level = getAriaLevel(element);
|
|
579
|
+
if (level > 0) node.level = level;
|
|
135
580
|
|
|
136
581
|
return node;
|
|
137
582
|
}
|
|
@@ -217,7 +662,7 @@
|
|
|
217
662
|
|
|
218
663
|
/**
|
|
219
664
|
* Phase 2: Normalize the tree by removing redundant generic wrappers.
|
|
220
|
-
* Complete preservation of snapshot.js normalizeTree logic
|
|
665
|
+
* Complete preservation of snapshot.js normalizeTree logic with cursor inheritance
|
|
221
666
|
*/
|
|
222
667
|
function normalizeTree(node) {
|
|
223
668
|
if (typeof node === 'string') return [node];
|
|
@@ -229,6 +674,7 @@
|
|
|
229
674
|
node.children = newChildren;
|
|
230
675
|
|
|
231
676
|
// Remove child elements that have the same name as their parent
|
|
677
|
+
// and inherit cursor=pointer property if child had it
|
|
232
678
|
const filteredChildren = [];
|
|
233
679
|
for (const child of node.children) {
|
|
234
680
|
if (typeof child !== 'string' && child.name && node.name) {
|
|
@@ -237,6 +683,15 @@
|
|
|
237
683
|
if (childName === parentName) {
|
|
238
684
|
// If child has same name as parent, merge its children into parent
|
|
239
685
|
filteredChildren.push(...(child.children || []));
|
|
686
|
+
|
|
687
|
+
// Inherit cursor=pointer from merged child
|
|
688
|
+
if (child.element && receivesPointerEvents(child.element) && hasPointerCursor(child.element)) {
|
|
689
|
+
node.inheritedCursor = true;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Also inherit other properties if needed
|
|
693
|
+
if (child.disabled && !node.disabled) node.disabled = child.disabled;
|
|
694
|
+
if (child.selected && !node.selected) node.selected = child.selected;
|
|
240
695
|
} else {
|
|
241
696
|
filteredChildren.push(child);
|
|
242
697
|
}
|
|
@@ -250,6 +705,15 @@
|
|
|
250
705
|
if (node.children.length === 1 && typeof node.children[0] !== 'string') {
|
|
251
706
|
const child = node.children[0];
|
|
252
707
|
if (child.name && node.name && child.name.trim() === node.name.trim()) {
|
|
708
|
+
// Inherit cursor=pointer from the child being merged
|
|
709
|
+
if (child.element && receivesPointerEvents(child.element) && hasPointerCursor(child.element)) {
|
|
710
|
+
node.inheritedCursor = true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Also inherit other properties
|
|
714
|
+
if (child.disabled && !node.disabled) node.disabled = child.disabled;
|
|
715
|
+
if (child.selected && !node.selected) node.selected = child.selected;
|
|
716
|
+
|
|
253
717
|
// Merge child's children into parent and remove the redundant child
|
|
254
718
|
node.children = child.children || [];
|
|
255
719
|
}
|
|
@@ -266,19 +730,36 @@
|
|
|
266
730
|
|
|
267
731
|
/**
|
|
268
732
|
* Phase 3: Render the normalized tree into the final string format.
|
|
269
|
-
* Complete preservation of snapshot.js renderTree logic
|
|
733
|
+
* Complete preservation of snapshot.js renderTree logic with Playwright enhancements
|
|
270
734
|
*/
|
|
271
735
|
function renderTree(node, indent = '') {
|
|
272
736
|
const lines = [];
|
|
273
737
|
let meaningfulProps = '';
|
|
274
738
|
if (node.disabled) meaningfulProps += ' disabled';
|
|
739
|
+
if (node.occluded) meaningfulProps += ' occluded';
|
|
275
740
|
if (node.checked !== undefined) meaningfulProps += ` checked=${node.checked}`;
|
|
276
741
|
if (node.expanded !== undefined) meaningfulProps += ` expanded=${node.expanded}`;
|
|
742
|
+
if (node.selected) meaningfulProps += ' selected';
|
|
743
|
+
|
|
744
|
+
// Add level attribute following Playwright's format
|
|
745
|
+
if (node.level !== undefined) meaningfulProps += ` [level=${node.level}]`;
|
|
277
746
|
|
|
278
747
|
const ref = node.ref ? ` [ref=${node.ref}]` : '';
|
|
748
|
+
|
|
749
|
+
// Add cursor=pointer detection following Playwright's implementation
|
|
750
|
+
// Check both direct cursor and inherited cursor from merged children
|
|
751
|
+
let cursor = '';
|
|
752
|
+
const hasDirectCursor = node.element && receivesPointerEvents(node.element) && hasPointerCursor(node.element);
|
|
753
|
+
const hasInheritedCursor = node.inheritedCursor;
|
|
754
|
+
|
|
755
|
+
// Only add cursor=pointer if element is not occluded
|
|
756
|
+
if ((hasDirectCursor || hasInheritedCursor) && !node.occluded) {
|
|
757
|
+
cursor = ' [cursor=pointer]';
|
|
758
|
+
}
|
|
759
|
+
|
|
279
760
|
const name = (node.name || '').replace(/\s+/g, ' ').trim();
|
|
280
761
|
|
|
281
|
-
// Skip elements with empty names and no meaningful props (ref
|
|
762
|
+
// Skip elements with empty names and no meaningful props (ref and cursor are not considered meaningful for this check)
|
|
282
763
|
if (!name && !meaningfulProps) {
|
|
283
764
|
// If element has no name and no meaningful props, render its children directly at current level
|
|
284
765
|
for (const child of node.children) {
|
|
@@ -294,7 +775,7 @@
|
|
|
294
775
|
return lines;
|
|
295
776
|
}
|
|
296
777
|
|
|
297
|
-
lines.push(`${indent}- ${node.role}${name ? ` "${name}"` : ''}${meaningfulProps}${ref}`);
|
|
778
|
+
lines.push(`${indent}- ${node.role}${name ? ` "${name}"` : ''}${meaningfulProps}${ref}${cursor}`);
|
|
298
779
|
|
|
299
780
|
for (const child of node.children) {
|
|
300
781
|
if (typeof child === 'string') {
|
|
@@ -390,6 +871,7 @@
|
|
|
390
871
|
disabled: node.disabled,
|
|
391
872
|
checked: node.checked,
|
|
392
873
|
expanded: node.expanded,
|
|
874
|
+
level: node.level,
|
|
393
875
|
|
|
394
876
|
// Visual information (from page_script.js)
|
|
395
877
|
coordinates: coordinates,
|
|
@@ -398,7 +880,11 @@
|
|
|
398
880
|
href: node.element.href || null,
|
|
399
881
|
value: node.element.value || null,
|
|
400
882
|
placeholder: node.element.placeholder || null,
|
|
401
|
-
scrollable: node.element.scrollHeight > node.element.clientHeight
|
|
883
|
+
scrollable: node.element.scrollHeight > node.element.clientHeight,
|
|
884
|
+
|
|
885
|
+
// Playwright-inspired properties
|
|
886
|
+
receivesPointerEvents: receivesPointerEvents(node.element),
|
|
887
|
+
hasPointerCursor: hasPointerCursor(node.element)
|
|
402
888
|
};
|
|
403
889
|
}
|
|
404
890
|
|
|
@@ -411,19 +897,64 @@
|
|
|
411
897
|
}
|
|
412
898
|
|
|
413
899
|
function analyzePageElements() {
|
|
900
|
+
// Clean up stale refs before analysis
|
|
901
|
+
const cleanedRefCount = cleanupStaleRefs();
|
|
902
|
+
|
|
903
|
+
// Performance optimization: Check if we can reuse recent analysis
|
|
904
|
+
const currentTime = Date.now();
|
|
905
|
+
const lastAnalysisTime = window.__camelLastAnalysisTime || 0;
|
|
906
|
+
const timeSinceLastAnalysis = currentTime - lastAnalysisTime;
|
|
907
|
+
|
|
908
|
+
// If less than 1 second since last analysis and page hasn't changed significantly
|
|
909
|
+
if (timeSinceLastAnalysis < 1000 && window.__camelLastAnalysisResult && cleanedRefCount === 0) {
|
|
910
|
+
const cachedResult = window.__camelLastAnalysisResult;
|
|
911
|
+
// Update timestamp and memory info in cached result
|
|
912
|
+
cachedResult.metadata.timestamp = new Date().toISOString();
|
|
913
|
+
cachedResult.metadata.memoryInfo = {
|
|
914
|
+
currentRefCount: refElementMap.size,
|
|
915
|
+
maxRefs: MAX_REFS,
|
|
916
|
+
memoryUtilization: (refElementMap.size / MAX_REFS * 100).toFixed(1) + '%',
|
|
917
|
+
lruAccessTimesCount: refAccessTimes.size
|
|
918
|
+
};
|
|
919
|
+
cachedResult.metadata.cacheHit = true;
|
|
920
|
+
return cachedResult;
|
|
921
|
+
}
|
|
922
|
+
|
|
414
923
|
// Generate the complete structured snapshot using original snapshot.js logic
|
|
415
924
|
const outputLines = processDocument(document);
|
|
416
925
|
const snapshotText = outputLines.join('\n');
|
|
417
926
|
|
|
418
927
|
// Build the tree again to collect element information with visual data
|
|
419
928
|
textCache.clear();
|
|
420
|
-
|
|
929
|
+
// Note: Don't reset refCounter anymore - use persistent counters
|
|
421
930
|
let tree = buildAriaTree(document.body);
|
|
422
931
|
[tree] = normalizeTree(tree);
|
|
423
932
|
|
|
424
933
|
const elementsMap = {};
|
|
425
934
|
collectElementsFromTree(tree, elementsMap);
|
|
426
935
|
|
|
936
|
+
// Verify uniqueness of aria-ref attributes (debugging aid)
|
|
937
|
+
const ariaRefCounts = {};
|
|
938
|
+
document.querySelectorAll('[aria-ref]').forEach(element => {
|
|
939
|
+
const ref = element.getAttribute('aria-ref');
|
|
940
|
+
ariaRefCounts[ref] = (ariaRefCounts[ref] || 0) + 1;
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// Log any duplicates for debugging
|
|
944
|
+
const duplicateRefs = Object.entries(ariaRefCounts).filter(([ref, count]) => count > 1);
|
|
945
|
+
if (duplicateRefs.length > 0) {
|
|
946
|
+
console.warn('Duplicate aria-ref attributes found:', duplicateRefs);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Validate ref consistency
|
|
950
|
+
const refValidationErrors = [];
|
|
951
|
+
for (const [ref, elementInfo] of Object.entries(elementsMap)) {
|
|
952
|
+
const mappedElement = refElementMap.get(ref);
|
|
953
|
+
if (!mappedElement || !document.contains(mappedElement)) {
|
|
954
|
+
refValidationErrors.push(`Ref ${ref} points to invalid or removed element`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
427
958
|
const result = {
|
|
428
959
|
url: window.location.href,
|
|
429
960
|
elements: elementsMap,
|
|
@@ -435,10 +966,34 @@
|
|
|
435
966
|
width: window.innerWidth,
|
|
436
967
|
height: window.innerHeight,
|
|
437
968
|
devicePixelRatio: window.devicePixelRatio || 1
|
|
438
|
-
}
|
|
969
|
+
},
|
|
970
|
+
// Enhanced debugging information
|
|
971
|
+
ariaRefCounts: ariaRefCounts,
|
|
972
|
+
duplicateRefsFound: duplicateRefs.length > 0,
|
|
973
|
+
staleRefsCleanedUp: cleanedRefCount,
|
|
974
|
+
refValidationErrors: refValidationErrors,
|
|
975
|
+
totalMappedRefs: refElementMap.size,
|
|
976
|
+
refCounterValue: refCounter,
|
|
977
|
+
// Memory management information
|
|
978
|
+
memoryInfo: {
|
|
979
|
+
currentRefCount: refElementMap.size,
|
|
980
|
+
maxRefs: MAX_REFS,
|
|
981
|
+
memoryUtilization: (refElementMap.size / MAX_REFS * 100).toFixed(1) + '%',
|
|
982
|
+
lruAccessTimesCount: refAccessTimes.size,
|
|
983
|
+
unusedAgeThreshold: MAX_UNUSED_AGE_MS + 'ms',
|
|
984
|
+
cleanupThreshold: (CLEANUP_THRESHOLD * 100).toFixed(0) + '%',
|
|
985
|
+
isAggressiveCleanup: refElementMap.size > (MAX_REFS * CLEANUP_THRESHOLD)
|
|
986
|
+
},
|
|
987
|
+
// Performance information
|
|
988
|
+
cacheHit: false,
|
|
989
|
+
analysisTime: Date.now() - currentTime
|
|
439
990
|
}
|
|
440
991
|
};
|
|
441
992
|
|
|
993
|
+
// Cache the result for potential reuse
|
|
994
|
+
window.__camelLastAnalysisResult = result;
|
|
995
|
+
window.__camelLastAnalysisTime = currentTime;
|
|
996
|
+
|
|
442
997
|
return result;
|
|
443
998
|
}
|
|
444
999
|
|