@fastbrowse/linux-arm64 0.5.25 → 0.7.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/fastbrowse/_internal/base_library.zip +0 -0
- package/fastbrowse/_internal/fastbrowse/browser/snapshot.js +158 -36
- package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/METADATA +31 -19
- package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/RECORD +14 -0
- package/fastbrowse/_internal/fastbrowse-0.7.1.dist-info/uv_cache.json +1 -0
- package/fastbrowse/_internal/libpython3.13.so.1.0 +0 -0
- package/fastbrowse/fastbrowse +0 -0
- package/package.json +1 -1
- package/fastbrowse/_internal/fastbrowse-0.5.25.dist-info/RECORD +0 -14
- package/fastbrowse/_internal/fastbrowse-0.5.25.dist-info/uv_cache.json +0 -1
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/INSTALLER +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/REQUESTED +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/WHEEL +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/direct_url.json +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/entry_points.txt +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/licenses/LICENSE +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/licenses/NOTICE +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/uv_build.json +0 -0
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Adapted to fastbrowse's Control shape (role + operation set, not per-kind actions) and to traverse
|
|
4
4
|
// open shadow roots. Runs once per frame session (main frame or an OOPIF); the Python side merges frames.
|
|
5
5
|
// biome-ignore format: Python appends mode arguments to this parenthesized expression.
|
|
6
|
-
((mode, allowedOrigins = null) => {
|
|
6
|
+
((mode, allowedOrigins = null, limits = null) => {
|
|
7
7
|
const granted = doc => {
|
|
8
8
|
if (allowedOrigins === null) return true;
|
|
9
9
|
const frame = doc.defaultView.frameElement;
|
|
@@ -94,7 +94,17 @@
|
|
|
94
94
|
};
|
|
95
95
|
registry.track(document);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
const clock = () => ({
|
|
98
|
+
ready: document.readyState === 'interactive' || document.readyState === 'complete',
|
|
99
|
+
quietFor: performance.now() - registry.lastMutation,
|
|
100
|
+
hidden: document.hidden,
|
|
101
|
+
});
|
|
102
|
+
// The settle loop reads the clock alone until the page has gone quiet: the walk below reads every element and
|
|
103
|
+
// all the page's text, which on a large page costs more than the poll interval leaves room for.
|
|
104
|
+
if (mode === 'quiet') return clock();
|
|
105
|
+
// `loading` walks for indicators, and to watch new shadow roots and frames, but hashes nothing.
|
|
106
|
+
if (mode === 'fingerprint' || mode === 'loading') {
|
|
107
|
+
const hashing = mode === 'fingerprint';
|
|
98
108
|
let hash = 2166136261;
|
|
99
109
|
let loading = false;
|
|
100
110
|
const hashText = text => {
|
|
@@ -103,8 +113,7 @@
|
|
|
103
113
|
const include = root => {
|
|
104
114
|
registry.track(root);
|
|
105
115
|
loading ||= [...root.querySelectorAll(LOADING)].some(showing);
|
|
106
|
-
|
|
107
|
-
hashText(text);
|
|
116
|
+
if (hashing) hashText(root.body?.innerText ?? root.textContent ?? '');
|
|
108
117
|
// innerText omits shadow trees and child documents even when their content is visible.
|
|
109
118
|
for (const e of root.querySelectorAll('*')) {
|
|
110
119
|
// A filter can change only its checkmark, so text alone reported "Nonstop only" as a no-op.
|
|
@@ -112,6 +121,7 @@
|
|
|
112
121
|
// Only rendered controls: a hidden carousel's aria-selected dots churn on their own and would read as
|
|
113
122
|
// progress.
|
|
114
123
|
if (
|
|
124
|
+
hashing &&
|
|
115
125
|
('checked' in e || 'selected' in e || e.hasAttribute('aria-checked') || e.hasAttribute('aria-selected')) &&
|
|
116
126
|
e.checkVisibility({ checkOpacity: !nativeChoice(e), checkVisibilityCSS: true }) &&
|
|
117
127
|
(!nativeChoice(e) || shownThrough(e))
|
|
@@ -138,10 +148,8 @@
|
|
|
138
148
|
};
|
|
139
149
|
include(document);
|
|
140
150
|
return {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
quietFor: performance.now() - registry.lastMutation,
|
|
144
|
-
hidden: document.hidden,
|
|
151
|
+
...clock(),
|
|
152
|
+
fingerprint: hashing ? location.href + '|' + document.title + '|' + (hash >>> 0) + '|' + scrollY : null,
|
|
145
153
|
loading,
|
|
146
154
|
};
|
|
147
155
|
}
|
|
@@ -153,12 +161,32 @@
|
|
|
153
161
|
return id;
|
|
154
162
|
};
|
|
155
163
|
for (const [id, e] of registry.nodes) if (!e.isConnected) registry.nodes.delete(id);
|
|
164
|
+
// The DOM cannot change while this script runs, so a label, a text or a section title read once is reused for
|
|
165
|
+
// the rest of it. `registry.guard` reads them again in a later script, against a page that may have changed
|
|
166
|
+
// since, so the reuse ends as soon as this one does, whether it returns or throws.
|
|
167
|
+
const caches = [];
|
|
168
|
+
registry.nativeQueueMicrotask ||= window.queueMicrotask.bind(window);
|
|
169
|
+
registry.nativeQueueMicrotask(() => {
|
|
170
|
+
for (const cache of caches) cache.clear();
|
|
171
|
+
caches.length = 0;
|
|
172
|
+
});
|
|
173
|
+
const memo = read => {
|
|
174
|
+
const cache = new Map();
|
|
175
|
+
caches.push(cache);
|
|
176
|
+
return e => {
|
|
177
|
+
if (!caches.includes(cache)) return read(e);
|
|
178
|
+
if (!cache.has(e)) cache.set(e, read(e));
|
|
179
|
+
return cache.get(e);
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
const innerTextOf = memo(e => e.innerText);
|
|
156
183
|
|
|
157
184
|
const safe = e => e.type !== 'hidden';
|
|
158
185
|
// Password and agent-typed secret values never leave the page: only their length is observed.
|
|
159
186
|
const secret = e => e.type === 'password' || e.dataset?.fastbrowseSecret === '1';
|
|
160
187
|
const reveal = e => (typeof e.value !== 'string' ? null : secret(e) ? '•'.repeat(e.value.length) : e.value);
|
|
161
|
-
|
|
188
|
+
// Asked of each control by the walk and again by its guard, and of each text node's parent.
|
|
189
|
+
const visible = memo(e => {
|
|
162
190
|
// Todo lists paint the checkmark beside a transparent native input without associating a label.
|
|
163
191
|
// The input still receives clicks; opacity alone must not remove the only control for that row.
|
|
164
192
|
// A transparent input with a visible label of its own keeps the label as its click target: TodoMVC's
|
|
@@ -176,7 +204,7 @@
|
|
|
176
204
|
if (!choice) return true;
|
|
177
205
|
const r = e.getBoundingClientRect();
|
|
178
206
|
return r.width > 0 && r.height > 0 && !e.matches(':disabled') && !e.closest('[aria-disabled="true"]');
|
|
179
|
-
};
|
|
207
|
+
});
|
|
180
208
|
registry.visible = visible;
|
|
181
209
|
// Styled checkboxes and radios often hide the native input. Its visible label is the click
|
|
182
210
|
// target, but the input still owns the checked/disabled state and must participate in freshness.
|
|
@@ -230,21 +258,21 @@
|
|
|
230
258
|
.filter(Boolean)
|
|
231
259
|
.join(' ');
|
|
232
260
|
};
|
|
233
|
-
const
|
|
261
|
+
const labelFrom = (e, seen) => {
|
|
234
262
|
if (!e || seen.has(e)) return '';
|
|
235
263
|
seen.add(e);
|
|
236
264
|
const root = typeof e.getRootNode === 'function' ? e.getRootNode() : document;
|
|
237
265
|
const byId = id => (root.getElementById ? root.getElementById(id) : document.getElementById(id));
|
|
238
266
|
const referenced = (e.getAttribute('aria-labelledby') || '')
|
|
239
267
|
.split(/\s+/)
|
|
240
|
-
.map(id =>
|
|
268
|
+
.map(id => labelFrom(byId(id), seen))
|
|
241
269
|
.filter(Boolean)
|
|
242
270
|
.join(' ');
|
|
243
271
|
return (
|
|
244
272
|
referenced ||
|
|
245
273
|
e.getAttribute('aria-label') ||
|
|
246
274
|
[...(e.labels || [])]
|
|
247
|
-
.map(l =>
|
|
275
|
+
.map(l => labelFrom(l, seen))
|
|
248
276
|
.filter(Boolean)
|
|
249
277
|
.join(' ') ||
|
|
250
278
|
(['button', 'submit', 'reset'].includes(e.type) ? e.value : '') ||
|
|
@@ -256,7 +284,7 @@
|
|
|
256
284
|
n.nodeType === 3
|
|
257
285
|
? n.textContent
|
|
258
286
|
: n.nodeType === 1 && !CODE.has(n.tagName) && n.getAttribute('aria-hidden') !== 'true'
|
|
259
|
-
?
|
|
287
|
+
? labelFrom(n, seen)
|
|
260
288
|
: '',
|
|
261
289
|
)
|
|
262
290
|
.join(' ')
|
|
@@ -267,6 +295,8 @@
|
|
|
267
295
|
);
|
|
268
296
|
};
|
|
269
297
|
|
|
298
|
+
const labelOf = memo(e => labelFrom(e, new Set()));
|
|
299
|
+
|
|
270
300
|
const ARIA_ROLES = [
|
|
271
301
|
'button',
|
|
272
302
|
'link',
|
|
@@ -318,7 +348,7 @@
|
|
|
318
348
|
return null;
|
|
319
349
|
};
|
|
320
350
|
|
|
321
|
-
const submitSemantics = e => {
|
|
351
|
+
const submitSemantics = memo(e => {
|
|
322
352
|
const form = e.form;
|
|
323
353
|
const explicit = ['BUTTON', 'INPUT'].includes(e.tagName) && ['submit', 'image'].includes(e.type);
|
|
324
354
|
const implicit = [
|
|
@@ -346,9 +376,9 @@
|
|
|
346
376
|
label: submit ? labelOf(submit) : labelOf(form),
|
|
347
377
|
method: submit?.getAttribute('formmethod') || form.method,
|
|
348
378
|
action: submit?.getAttribute('formaction') || form.action,
|
|
349
|
-
text: excerpt(form
|
|
379
|
+
text: excerpt(innerTextOf(form), FORM_TEXT_CHARS),
|
|
350
380
|
});
|
|
351
|
-
};
|
|
381
|
+
});
|
|
352
382
|
|
|
353
383
|
const framePath = doc => {
|
|
354
384
|
if (doc === document) return null;
|
|
@@ -379,6 +409,23 @@
|
|
|
379
409
|
}
|
|
380
410
|
}
|
|
381
411
|
|
|
412
|
+
// Guards and the page key go to Python as digests: each comparison is equality, and the full guards cost more to
|
|
413
|
+
// send than the snapshot takes to compute. Not cryptographic, and it needs not be: a page that wants a guard to
|
|
414
|
+
// hold can simply leave its DOM alone.
|
|
415
|
+
const digest = text => {
|
|
416
|
+
let h1 = 0xdeadbeef,
|
|
417
|
+
h2 = 0x41c6ce57;
|
|
418
|
+
for (let i = 0; i < text.length; i++) {
|
|
419
|
+
const c = text.charCodeAt(i);
|
|
420
|
+
h1 = Math.imul(h1 ^ c, 2654435761);
|
|
421
|
+
h2 = Math.imul(h2 ^ c, 1597334677);
|
|
422
|
+
}
|
|
423
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
424
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
425
|
+
return (h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0');
|
|
426
|
+
};
|
|
427
|
+
// Sibling controls share their card, so its text is hashed once per card rather than once per control.
|
|
428
|
+
const scopeDigest = memo(scope => digest(innerTextOf(scope)?.slice(0, GUARD_TEXT_CHARS) || ''));
|
|
382
429
|
registry.guard = e => {
|
|
383
430
|
if (!e?.isConnected || !visible(e)) return null;
|
|
384
431
|
const source = sourceOf(e);
|
|
@@ -400,7 +447,7 @@
|
|
|
400
447
|
e.getAttribute('aria-checked'),
|
|
401
448
|
e.getAttribute('aria-selected'),
|
|
402
449
|
e.getAttribute('href'),
|
|
403
|
-
scope
|
|
450
|
+
scope ? scopeDigest(scope) : '',
|
|
404
451
|
e.ownerDocument.location.origin,
|
|
405
452
|
e.ownerDocument.defaultView.performance.timeOrigin,
|
|
406
453
|
submitSemantics(e),
|
|
@@ -409,6 +456,10 @@
|
|
|
409
456
|
identity(source),
|
|
410
457
|
];
|
|
411
458
|
};
|
|
459
|
+
registry.guardKey = e => {
|
|
460
|
+
const guard = registry.guard(e);
|
|
461
|
+
return guard && digest(JSON.stringify(guard));
|
|
462
|
+
};
|
|
412
463
|
|
|
413
464
|
const controls = [];
|
|
414
465
|
// Controls with no accessible name of their own, whose label is an icon's or only their role.
|
|
@@ -444,7 +495,8 @@
|
|
|
444
495
|
if (rname === 'gridcell' && e.querySelector('button,[role="button"]')) continue;
|
|
445
496
|
// A button-styled link offers the same label twice, but its inner button owns every useful click point.
|
|
446
497
|
if (rname === 'link') {
|
|
447
|
-
|
|
498
|
+
// Most links hold no button, and querying every link for every control was the walk's slowest step.
|
|
499
|
+
const nested = e.querySelector('button') ? e.querySelectorAll(SELECTOR) : [];
|
|
448
500
|
const button = nested.length === 1 ? nested[0] : null;
|
|
449
501
|
if (button?.tagName === 'BUTTON' && visible(button) && !button.matches(':disabled') &&
|
|
450
502
|
!button.closest('[aria-disabled="true"],[inert]') && labelOf(button) === labelOf(e)) continue;
|
|
@@ -531,11 +583,11 @@
|
|
|
531
583
|
// A label tied to no control titles what follows it, as a heading would: a blog post's "Date Picker 3" is one,
|
|
532
584
|
// and the post's first heading ("Data Entry Form") named that picker's Submit when only headings counted.
|
|
533
585
|
// A hidden title names nothing on screen: innerText still returns a `hidden` label's text.
|
|
534
|
-
const titles = scope =>
|
|
586
|
+
const titles = memo(scope =>
|
|
535
587
|
[...scope.querySelectorAll(`${HEADINGS},label`)].filter(
|
|
536
588
|
e =>
|
|
537
589
|
(e.localName !== 'label' || !e.control) && e.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }),
|
|
538
|
-
);
|
|
590
|
+
));
|
|
539
591
|
// A table's header cell names its own row, and a caption its own table, never what sits outside them: a jQuery UI
|
|
540
592
|
// datepicker's Next button was named "Su", its weekday header, and every month's Next read the same.
|
|
541
593
|
const reaches = (title, element) =>
|
|
@@ -546,7 +598,7 @@
|
|
|
546
598
|
let first = '',
|
|
547
599
|
before = '';
|
|
548
600
|
for (const title of titles(scope)) {
|
|
549
|
-
const text = firstLine(title
|
|
601
|
+
const text = firstLine(innerTextOf(title));
|
|
550
602
|
if (!text || text === label || title.contains(element) || !reaches(title, element)) continue;
|
|
551
603
|
first ||= text;
|
|
552
604
|
if (title.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING) before = text;
|
|
@@ -563,18 +615,18 @@
|
|
|
563
615
|
// A line that is itself a repeated control's label ("Prev") reads the same in every repeat, so it names none:
|
|
564
616
|
// past it, a datepicker's first line is the month it shows. The label is cut out as whole words, since a day
|
|
565
617
|
// labelled "2" cut out of "October 2026" left "October 0 6".
|
|
566
|
-
const lines = (scope
|
|
618
|
+
const lines = (innerTextOf(scope) || '')
|
|
567
619
|
.split('\n')
|
|
568
620
|
.map(line => line.replace(/\s+/g, ' ').trim())
|
|
569
621
|
.filter(line => line && !repeated.has(line));
|
|
570
622
|
return firstLine(lines.map(line => (label ? ` ${line} `.split(` ${label} `).join(' ') : line)).join('\n'));
|
|
571
623
|
};
|
|
572
|
-
const contextOf = (element,
|
|
624
|
+
const contextOf = (element, holding, label, nearest = true) => {
|
|
573
625
|
// The widest twin-free ancestor is the card, row or section the twins repeat over. A narrower one names
|
|
574
626
|
// the button's own wrapper, which on a shop is its price rather than the product it belongs to.
|
|
575
627
|
let scope = null;
|
|
576
628
|
for (let e = element.parentElement; e && e !== e.ownerDocument.body; e = e.parentElement) {
|
|
577
|
-
if (
|
|
629
|
+
if (holding.get(e) > 1) break;
|
|
578
630
|
scope = e;
|
|
579
631
|
// A lone open reply has no form twin nearby; crossing its record picks a different commenter's name.
|
|
580
632
|
if (e.matches('article,li,[role="listitem"]') ||
|
|
@@ -709,12 +761,18 @@
|
|
|
709
761
|
for (const group of byLabel.values()) {
|
|
710
762
|
if (group.length < 2) continue;
|
|
711
763
|
const twins = group.map(c => registry.nodes.get(c.id));
|
|
712
|
-
|
|
764
|
+
// How many twins each ancestor holds. One that holds two is past the card, and counting once beats asking
|
|
765
|
+
// every ancestor of every twin whether it contains each of the others.
|
|
766
|
+
const holding = new Map();
|
|
767
|
+
for (const twin of twins) {
|
|
768
|
+
for (let e = twin.parentElement; e; e = e.parentElement) holding.set(e, (holding.get(e) || 0) + 1);
|
|
769
|
+
}
|
|
770
|
+
const nearest = group.map(c => contextOf(registry.nodes.get(c.id), holding, c.label));
|
|
713
771
|
group.forEach((c, i) => {
|
|
714
772
|
// Cards that each end in an "Options" heading share their nearest title, which would name every twin
|
|
715
773
|
// alike; a twin whose nearest title another twin shares is named by its card's first title instead.
|
|
716
774
|
const shared = nearest.indexOf(nearest[i]) !== nearest.lastIndexOf(nearest[i]);
|
|
717
|
-
const context = shared ? contextOf(registry.nodes.get(c.id),
|
|
775
|
+
const context = shared ? contextOf(registry.nodes.get(c.id), holding, c.label, false) : nearest[i];
|
|
718
776
|
if (context) c.context = context;
|
|
719
777
|
});
|
|
720
778
|
// Options that are direct children of one list share every ancestor, and their aria-label hides the text
|
|
@@ -770,7 +828,7 @@
|
|
|
770
828
|
const aria = (e.getAttribute('aria-label') || '').trim();
|
|
771
829
|
const outside = [...e.children]
|
|
772
830
|
.filter(child => child !== path)
|
|
773
|
-
.map(child => firstLine(child
|
|
831
|
+
.map(child => firstLine(innerTextOf(child)))
|
|
774
832
|
.find(line => line && line !== label && !repeated.has(line));
|
|
775
833
|
return excerpt((aria !== label && aria) || outside || first, CONTROL_CONTEXT_CHARS);
|
|
776
834
|
}
|
|
@@ -883,6 +941,7 @@
|
|
|
883
941
|
// Text in a same-process child document is on screen as much as the parent's: a frameset page has no text of
|
|
884
942
|
// its own at all. Each child is read in its own coordinates, shifted by where its frame sits.
|
|
885
943
|
const words = [];
|
|
944
|
+
// Geometry, the cheaper test, goes first.
|
|
886
945
|
const readText = (doc, dx, dy) => {
|
|
887
946
|
if (!doc.body) return;
|
|
888
947
|
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
|
@@ -891,7 +950,7 @@
|
|
|
891
950
|
while ((node = walker.nextNode())) {
|
|
892
951
|
const value = node.textContent.trim(),
|
|
893
952
|
parent = node.parentElement;
|
|
894
|
-
if (!value || !parent
|
|
953
|
+
if (!value || !parent) continue;
|
|
895
954
|
range.selectNodeContents(node);
|
|
896
955
|
const r = range.getBoundingClientRect();
|
|
897
956
|
const top = r.top + dy,
|
|
@@ -902,7 +961,9 @@
|
|
|
902
961
|
top + r.height > 0 &&
|
|
903
962
|
top < innerHeight &&
|
|
904
963
|
left + r.width > 0 &&
|
|
905
|
-
left < innerWidth
|
|
964
|
+
left < innerWidth &&
|
|
965
|
+
!parent.closest('script,style,noscript,template') &&
|
|
966
|
+
visible(parent)
|
|
906
967
|
) {
|
|
907
968
|
words.push(value);
|
|
908
969
|
}
|
|
@@ -922,8 +983,50 @@
|
|
|
922
983
|
readText(document, 0, 0);
|
|
923
984
|
const viewport_text = words.join('\n');
|
|
924
985
|
|
|
925
|
-
|
|
926
|
-
|
|
986
|
+
// Page state carried by pixels: a chart, a picture, a drawing or a video is evidence the page text and the
|
|
987
|
+
// control list cannot hold, and the verifier needs the screenshot for it. Only what the viewport shows counts,
|
|
988
|
+
// because that is what a screenshot captures: an image below the fold cannot appear in it, and counting every
|
|
989
|
+
// image on a long page would ask for a screenshot on almost every page and undo the gate it exists to serve.
|
|
990
|
+
// Geometry is read in the parent's coordinates the way readText reads text, so a frame's image counts where the
|
|
991
|
+
// frame itself sits on screen. Elements small enough to be an icon or a rule are not page state.
|
|
992
|
+
const VISUAL = 'img,canvas,video,svg,picture,figure,[role="img"]';
|
|
993
|
+
const VISUAL_MIN_PX = 48;
|
|
994
|
+
let visual_content = 0;
|
|
995
|
+
const readVisual = (doc, dx, dy) => {
|
|
996
|
+
if (visual_content) return;
|
|
997
|
+
for (const e of doc.querySelectorAll(VISUAL)) {
|
|
998
|
+
if (e.closest('[aria-hidden="true"],[inert]') || !visible(e)) continue;
|
|
999
|
+
const r = e.getBoundingClientRect();
|
|
1000
|
+
const top = r.top + dy,
|
|
1001
|
+
left = r.left + dx;
|
|
1002
|
+
if (r.width < VISUAL_MIN_PX || r.height < VISUAL_MIN_PX) continue;
|
|
1003
|
+
if (top + r.height <= 0 || top >= innerHeight || left + r.width <= 0 || left >= innerWidth) continue;
|
|
1004
|
+
visual_content = 1;
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
for (const frame of doc.querySelectorAll('iframe,frame')) {
|
|
1008
|
+
let inner = null;
|
|
1009
|
+
try {
|
|
1010
|
+
inner = frame.contentDocument;
|
|
1011
|
+
} catch {
|
|
1012
|
+
inner = null;
|
|
1013
|
+
}
|
|
1014
|
+
if (!inner || !granted(inner)) continue;
|
|
1015
|
+
const f = frame.getBoundingClientRect();
|
|
1016
|
+
readVisual(inner, dx + f.left + frame.clientLeft, dy + f.top + frame.clientTop);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
readVisual(document, 0, 0);
|
|
1020
|
+
|
|
1021
|
+
const guards = {},
|
|
1022
|
+
retarget_keys = {};
|
|
1023
|
+
for (const c of controls) {
|
|
1024
|
+
const guard = registry.guard(registry.nodes.get(c.id));
|
|
1025
|
+
guards[c.id] = guard && digest(JSON.stringify(guard));
|
|
1026
|
+
// A redraw changes the node ids at either end of the guard. Everything between them, including the receiving
|
|
1027
|
+
// document's timeOrigin and form semantics, must survive before an action can follow it.
|
|
1028
|
+
retarget_keys[c.id] = guard && digest(JSON.stringify(guard.slice(1, -1)));
|
|
1029
|
+
}
|
|
927
1030
|
// A dependent field can appear without changing page text. Compare all controls,
|
|
928
1031
|
// excusing only the value and required marker of the field just filled.
|
|
929
1032
|
const formState = controls.map(({ distance, offscreen, ...c }) => {
|
|
@@ -945,13 +1048,13 @@
|
|
|
945
1048
|
}
|
|
946
1049
|
registry.formObserved = { page: formPage, controls: formState };
|
|
947
1050
|
// Kept so a later check can ask whether any of these controls changed without sending the guards back.
|
|
948
|
-
registry.observed = new Map(controls.map(c => [c.id,
|
|
1051
|
+
registry.observed = new Map(controls.map(c => [c.id, guards[c.id]]));
|
|
949
1052
|
const field_state = [...document.querySelectorAll('input,textarea,select')]
|
|
950
1053
|
.filter(safe)
|
|
951
1054
|
.map(e => [identity(e), reveal(e), e.checked, e.selectedIndex, e.disabled, e.readOnly]);
|
|
952
1055
|
// Compare meaning and identity for the page_key fingerprint; geometry is re-resolved just before input.
|
|
953
1056
|
const semantics = controls.map(({ distance, ...c }) => c);
|
|
954
|
-
const page_key = JSON.stringify([
|
|
1057
|
+
const page_key = digest(JSON.stringify([
|
|
955
1058
|
location.href,
|
|
956
1059
|
scrollX,
|
|
957
1060
|
scrollY,
|
|
@@ -961,16 +1064,35 @@
|
|
|
961
1064
|
viewport_text,
|
|
962
1065
|
semantics,
|
|
963
1066
|
field_state,
|
|
964
|
-
]);
|
|
1067
|
+
]));
|
|
965
1068
|
|
|
966
|
-
|
|
1069
|
+
const result = {
|
|
967
1070
|
url: location.href,
|
|
968
1071
|
title: document.title,
|
|
969
1072
|
viewport_text,
|
|
1073
|
+
visual_content,
|
|
970
1074
|
document_key: String(performance.timeOrigin),
|
|
971
1075
|
controls: semantics,
|
|
972
1076
|
page_key,
|
|
973
1077
|
guards,
|
|
1078
|
+
retarget_keys,
|
|
974
1079
|
inaccessible_frames: inaccessible,
|
|
975
1080
|
};
|
|
1081
|
+
if (!limits) return result;
|
|
1082
|
+
// Python keeps at most `limits.onscreen` of the controls on screen and `limits.offscreen` of those off it, each
|
|
1083
|
+
// the first in this order, and any pager past either cap. A control past the caps is sent with only what decides
|
|
1084
|
+
// whether it is a pager; Python asks for the rest of one it keeps, by this snapshot's token, from what is kept here.
|
|
1085
|
+
const token = (registry.snapshots = (registry.snapshots || 0) + 1);
|
|
1086
|
+
registry.sent = { token, rows: new Map(semantics.map(c => [c.id, [c, guards[c.id], retarget_keys[c.id]]])) };
|
|
1087
|
+
let onscreen = 0,
|
|
1088
|
+
offscreen = 0;
|
|
1089
|
+
result.controls = semantics.map(c => {
|
|
1090
|
+
if (c.offscreen ? offscreen++ < limits.offscreen : onscreen++ < limits.onscreen) return c;
|
|
1091
|
+
delete result.guards[c.id];
|
|
1092
|
+
delete result.retarget_keys[c.id];
|
|
1093
|
+
const { id, role, label, operations, href, next_page } = c;
|
|
1094
|
+
return { id, role, label, operations, href, next_page, offscreen: c.offscreen, partial: true };
|
|
1095
|
+
});
|
|
1096
|
+
result.token = token;
|
|
1097
|
+
return result;
|
|
976
1098
|
})
|
package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastbrowse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Browser automation with cited answers, structured output and code-controlled credentials and authorization.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agent-labs-dev/fastbrowse
|
|
6
6
|
Project-URL: Repository, https://github.com/agent-labs-dev/fastbrowse
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pydantic-settings<3,>=2.10
|
|
|
17
17
|
Requires-Dist: pydantic<3,>=2.11
|
|
18
18
|
Requires-Dist: websockets<18,>=14
|
|
19
19
|
Provides-Extra: browser-use
|
|
20
|
-
Requires-Dist: browser-use-sdk==3.
|
|
20
|
+
Requires-Dist: browser-use-sdk==3.12.1; extra == 'browser-use'
|
|
21
21
|
Provides-Extra: eval-data
|
|
22
22
|
Requires-Dist: pyyaml<7,>=6; extra == 'eval-data'
|
|
23
23
|
Provides-Extra: mcp
|
|
@@ -60,16 +60,19 @@ Benchmarking compares exactly three things:
|
|
|
60
60
|
|
|
61
61
|
| | Comparison | Arms | Recorded |
|
|
62
62
|
|:--|:--|:--|:--|
|
|
63
|
-
| C1 | agent against agent | fastbrowse against the hosted Browser Use agent |
|
|
64
|
-
| C2 | backend against backend | fastbrowse on its own browser pool against fastbrowse on a Browser Use Cloud browser |
|
|
63
|
+
| C1 | agent against agent | fastbrowse against the hosted Browser Use agent | 46 tasks, three repeats: 99.2% against 97.8%, median 16.3s against 16.1s, $0.0256 against $0.2840 a result |
|
|
64
|
+
| C2 | backend against backend | fastbrowse on its own browser pool against fastbrowse on a Browser Use Cloud browser | 53 tasks, three repeats: 96.2% against 98.1%, median 15.9s against 15.0s, $0.0192 against $0.0200 a result |
|
|
65
65
|
| C3 | fastbrowse and its backend against [Browser Use Ultrafast](https://github.com/browser-use/jev-ultrafast) | fastbrowse on its own browser pool against `jev-ultrafast`, 6 tasks | not yet measured |
|
|
66
66
|
|
|
67
|
-
C1's figures are the recorded head-to-head on
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
C1's figures are the recorded head-to-head on 46 tasks, three repeats each, on Browser Use cloud browsers under
|
|
68
|
+
identical limits, with the hosted agent at its defaults. C2 compares browsers, not agents: both arms run the same
|
|
69
|
+
fastbrowse build, tasks and repeats, and only the browser differs. On that backend fastbrowse passes fewer tasks
|
|
70
|
+
on its own pool than on the Browser Use Cloud browser, 96.2% against 98.1%. [Benchmark results](docs/benchmarks.md)
|
|
71
|
+
gives each comparison's sources, the campaigns behind C1, the browser cost basis and the limitations. The
|
|
72
|
+
[benchmark page](https://fastbrowse.ai/benchmarks) renders only figures the maintainer approved; the C1 and C2
|
|
73
|
+
figures come from the 0.5.25 campaign staged in `docs/results/benchmark-candidate.json`, which awaits approval.
|
|
74
|
+
Rows are comparable only at matching task versions; the upstream suites and the Codex with Cua Driver arm are
|
|
75
|
+
diagnostics, described in [eval results and workflow](docs/evals.md).
|
|
73
76
|
|
|
74
77
|
### Why fastbrowse, against each kind of agent
|
|
75
78
|
|
|
@@ -94,13 +97,14 @@ A Node program needs neither uv nor Python: see [Use it from JavaScript](#use-it
|
|
|
94
97
|
|
|
95
98
|
```sh
|
|
96
99
|
export OPENROUTER_API_KEY=... # for Jev and the LLM that plans and reads
|
|
97
|
-
# export AI_GATEWAY_API_KEY=... # Vercel AI Gateway: Jev backup, and the LLM when OPENROUTER_API_KEY is unset
|
|
100
|
+
# export AI_GATEWAY_API_KEY=... # Vercel AI Gateway: Jev backup, and Jev and the LLM when OPENROUTER_API_KEY is unset
|
|
98
101
|
export BROWSER_USE_API_KEY=... # the cloud browser; or pass --local to use Chrome
|
|
99
102
|
uvx fastbrowse "What is the title of the top story right now?" --start https://news.ycombinator.com/
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
Jev uses direct TypeSafe when `TYPESAFE_API_KEY` is supplied
|
|
103
|
-
Vercel AI Gateway is
|
|
105
|
+
Jev uses direct TypeSafe when `TYPESAFE_API_KEY` is supplied, otherwise OpenRouter when `OPENROUTER_API_KEY` is
|
|
106
|
+
set, and the Vercel AI Gateway when neither is. With `AI_GATEWAY_API_KEY` beside another key, the gateway is the
|
|
107
|
+
backup. `FASTBROWSE_JEV_SOURCE` overrides
|
|
104
108
|
automatic selection; see [provider routing](docs/jev.md#provider-failover). The LLM uses OpenRouter when
|
|
105
109
|
`OPENROUTER_API_KEY` is set, otherwise the Vercel AI Gateway's OpenAI-compatible chat completions.
|
|
106
110
|
|
|
@@ -121,8 +125,8 @@ cp .env.example .env # then fill in the keys, BROWSER_USE_API_KEY included, or
|
|
|
121
125
|
uv run fastbrowse "What is the title of the top story right now?" --start https://news.ycombinator.com/
|
|
122
126
|
```
|
|
123
127
|
|
|
124
|
-
Steps go to stderr; the status, cost, step count and answer go to stdout.
|
|
125
|
-
the quotes behind the answer, and cost by component.
|
|
128
|
+
Steps go to stderr; the status, cost, step count, the reason a run stopped short and the answer go to stdout.
|
|
129
|
+
`--json` prints every step, the quotes behind the answer, and cost by component.
|
|
126
130
|
|
|
127
131
|
| Flag | Effect |
|
|
128
132
|
|:--|:--|
|
|
@@ -259,7 +263,7 @@ Only `complete` exits 0, so a program can also check zero versus nonzero.
|
|
|
259
263
|
|
|
260
264
|
| Status | Exit | Meaning |
|
|
261
265
|
|:--|--:|:--|
|
|
262
|
-
| `complete` | 0 | every information requirement is backed by a quote, and every action is confirmed on the page |
|
|
266
|
+
| `complete` | 0 | every information requirement is backed by a quote, and every action that changes the site is confirmed on the page |
|
|
263
267
|
| `unverified` | 10 | it believes it finished but could not back every claim |
|
|
264
268
|
| `needs_confirmation` | 3 | stopped before an irreversible action; re-run with `--authorize` |
|
|
265
269
|
| `needs_login` | 4 | a sign-in wall that no `--secret` covers |
|
|
@@ -275,12 +279,18 @@ A `budget_exceeded` JSON result includes `budget.resource` (`steps`, `seconds`,
|
|
|
275
279
|
`llm_calls`) and `budget.limit`. Other results have `budget: null`. The embedding and MCP APIs carry the same
|
|
276
280
|
optional object, so callers can identify the affected limit without parsing an error message.
|
|
277
281
|
|
|
282
|
+
A run that stops `stuck` or `unverified` before writing its answer, with evidence read for a task that asks for
|
|
283
|
+
one, returns a cited answer that starts `Answer (unverified)`, written by one more composer call. If that call
|
|
284
|
+
fails, the answer is the list of quotes the run collected, as it is for the other statuses that stop with evidence.
|
|
285
|
+
|
|
278
286
|
A bot check can block a cloud browser before any task steps execute. Retry with an attached local browser
|
|
279
287
|
or choose another source for the research. Credentials do not resolve a CAPTCHA.
|
|
280
288
|
The `blocked` result's `evidence` quotes the opening text of the challenge page, with its address.
|
|
281
289
|
|
|
282
290
|
If a model completion omits its cost, fastbrowse looks up the matching OpenRouter or Vercel generation
|
|
283
|
-
receipt. If that receipt is still unavailable,
|
|
291
|
+
receipt. If that receipt is still unavailable, an OpenRouter call is charged at an upper bound from the model's
|
|
292
|
+
listed prices: its whole context window at the highest price. A call that cannot be bounded, through the Vercel AI
|
|
293
|
+
Gateway or a custom endpoint, leaves the cost unknown, and a run with a dollar cap stops with `budget_exceeded`;
|
|
284
294
|
this does not mean the known charges reached the cap. Supplied `inputs` can provide exact field text
|
|
285
295
|
without a field-writing generation.
|
|
286
296
|
|
|
@@ -312,7 +322,7 @@ More in [docs/design.md](docs/design.md).
|
|
|
312
322
|
| Signing in | yes | password fields excluded | `--secret` or a Bitwarden vault item; models see names only |
|
|
313
323
|
| Irreversible actions | explicit task instructions in our fixture tests | no authorization parameter in Agent | classifier plus a code-enforced authorization gate |
|
|
314
324
|
| Browser | cloud | Chrome through Browser Harness; cloud in these evals | cloud by default, or local Chrome with `--local` |
|
|
315
|
-
| Benchmarked in | C1 | C3 | C1 on a cloud browser; C2 and C3 on its own
|
|
325
|
+
| Benchmarked in | C1 | C3 | C1 on a cloud browser; C2 on its own pool and on a Browser Use Cloud browser; C3 on its own pool |
|
|
316
326
|
|
|
317
327
|
Browser Use Ultrafast (`jev-ultrafast`) is Browser Use's navigation agent, separate from its hosted
|
|
318
328
|
LLM agent; fastbrowse shares its core techniques. Its column describes
|
|
@@ -410,7 +420,9 @@ it can be `None` for an ordinary successful action.
|
|
|
410
420
|
|
|
411
421
|
For continuous live images, pass an async `on_frame` handler accepting JPEG bytes. Frames follow the active
|
|
412
422
|
tab and are acknowledged after delivery, with no fixed frame rate. Only the latest pending frame is kept.
|
|
413
|
-
Handler failures are logged without stopping the run.
|
|
423
|
+
Handler failures are logged without stopping the run. A handler must honour cancellation: it runs on the
|
|
424
|
+
caller's event loop, so one that catches `CancelledError` without re-raising keeps that loop alive, and
|
|
425
|
+
`asyncio.run` then waits for it at shutdown. Live frames and recordings are held back while a
|
|
414
426
|
resolved secret may show on the page, as PNG step frames are. No handler means no live capture.
|
|
415
427
|
|
|
416
428
|
Jev uses direct TypeSafe when keyed, otherwise OpenRouter, with Vercel AI Gateway also supported.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
../../../bin/fastbrowse,sha256=kaVbuMx-AlSCiesFjTFd09Km4R8Lunnqm06vuR0Q4ow,336
|
|
2
|
+
../../../bin/fastbrowse-mcp,sha256=ZXqxb9qRtFFkC1H9e4YqAdOBsFqTUwa9EZeg_4K3Ybo,343
|
|
3
|
+
_editable_impl_fastbrowse.pth,sha256=i4F7SPHtRO2rWCDD5u0Gnwv0cyF2GMJMQGgsS1QKCTI,43
|
|
4
|
+
fastbrowse-0.7.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
|
5
|
+
fastbrowse-0.7.1.dist-info/METADATA,sha256=mKePR_0mnmqEJpiPFAYyCl0v5kF_qBF8IUFFBKaENFg,38738
|
|
6
|
+
fastbrowse-0.7.1.dist-info/RECORD,,
|
|
7
|
+
fastbrowse-0.7.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fastbrowse-0.7.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
fastbrowse-0.7.1.dist-info/direct_url.json,sha256=p4VhBh51_mJBO89EvLp28QXawaajq5zi5eItI6PDXTg,85
|
|
10
|
+
fastbrowse-0.7.1.dist-info/entry_points.txt,sha256=SlQAK1_4_oywTAMckx8BYuCQJva0F-QuShQPcxtUm-g,95
|
|
11
|
+
fastbrowse-0.7.1.dist-info/licenses/LICENSE,sha256=gcUazG_j5a2blKPmfKqRKE98KOqX2kPbWY-EOtQZiNU,1067
|
|
12
|
+
fastbrowse-0.7.1.dist-info/licenses/NOTICE,sha256=qEO0Az_Tk98vRVL0xjnGYgMvexlMCA92lWl4JBwYRzg,1854
|
|
13
|
+
fastbrowse-0.7.1.dist-info/uv_build.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
|
|
14
|
+
fastbrowse-0.7.1.dist-info/uv_cache.json,sha256=DerUJgub597QeSdF8qxY4E-JmhxfSbHJSrORY_KaUxY,194
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"timestamp":{"secs_since_epoch":1792907015,"nanos_since_epoch":434232915},"commit":null,"tags":null,"env":{},"directories":{"src":{"secs_since_epoch":1792907015,"nanos_since_epoch":434232915}}}
|
|
Binary file
|
package/fastbrowse/fastbrowse
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
../../../bin/fastbrowse,sha256=kaVbuMx-AlSCiesFjTFd09Km4R8Lunnqm06vuR0Q4ow,336
|
|
2
|
-
../../../bin/fastbrowse-mcp,sha256=ZXqxb9qRtFFkC1H9e4YqAdOBsFqTUwa9EZeg_4K3Ybo,343
|
|
3
|
-
_editable_impl_fastbrowse.pth,sha256=i4F7SPHtRO2rWCDD5u0Gnwv0cyF2GMJMQGgsS1QKCTI,43
|
|
4
|
-
fastbrowse-0.5.25.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
|
5
|
-
fastbrowse-0.5.25.dist-info/METADATA,sha256=PviO3CI9ERI53eHfNlnFhSL0Sn9CNuF-Oe-vEJqhBHg,37292
|
|
6
|
-
fastbrowse-0.5.25.dist-info/RECORD,,
|
|
7
|
-
fastbrowse-0.5.25.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
fastbrowse-0.5.25.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
-
fastbrowse-0.5.25.dist-info/direct_url.json,sha256=p4VhBh51_mJBO89EvLp28QXawaajq5zi5eItI6PDXTg,85
|
|
10
|
-
fastbrowse-0.5.25.dist-info/entry_points.txt,sha256=SlQAK1_4_oywTAMckx8BYuCQJva0F-QuShQPcxtUm-g,95
|
|
11
|
-
fastbrowse-0.5.25.dist-info/licenses/LICENSE,sha256=gcUazG_j5a2blKPmfKqRKE98KOqX2kPbWY-EOtQZiNU,1067
|
|
12
|
-
fastbrowse-0.5.25.dist-info/licenses/NOTICE,sha256=qEO0Az_Tk98vRVL0xjnGYgMvexlMCA92lWl4JBwYRzg,1854
|
|
13
|
-
fastbrowse-0.5.25.dist-info/uv_build.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
|
|
14
|
-
fastbrowse-0.5.25.dist-info/uv_cache.json,sha256=nuMm0LpaxGmXjICGUHug7x3u7cj1OiLULzJUGM_sVa4,194
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"timestamp":{"secs_since_epoch":1792479825,"nanos_since_epoch":590769738},"commit":null,"tags":null,"env":{},"directories":{"src":{"secs_since_epoch":1792479825,"nanos_since_epoch":591769768}}}
|
/package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/INSTALLER
RENAMED
|
File without changes
|
/package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/REQUESTED
RENAMED
|
File without changes
|
/package/fastbrowse/_internal/{fastbrowse-0.5.25.dist-info → fastbrowse-0.7.1.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|