explorbot 0.3.5 → 0.4.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/bin/explorbot-cli.ts +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
package/src/utils/html.ts
CHANGED
|
@@ -101,17 +101,12 @@ export const HTML_SELECTORS = {
|
|
|
101
101
|
} as const;
|
|
102
102
|
|
|
103
103
|
export const HTML_VISIBILITY_LIMITS = {
|
|
104
|
-
maxViewportOverlayRatio: 0.95,
|
|
105
104
|
minOpacity: 0.1,
|
|
106
|
-
minOverlayHeight: 40,
|
|
107
|
-
minOverlayWidth: 80,
|
|
108
105
|
} as const;
|
|
109
106
|
|
|
110
107
|
export const HTML_EXTRACTION_LIMITS = {
|
|
111
108
|
componentScopeHtmlLength: 8000,
|
|
112
|
-
maxOverlayCount: 3,
|
|
113
109
|
maxScopeInteractiveCount: 16,
|
|
114
|
-
overlayHtmlLength: 6000,
|
|
115
110
|
} as const;
|
|
116
111
|
|
|
117
112
|
export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'] as const;
|
|
@@ -158,14 +153,6 @@ export const ELEMENT_EXTRACTION_CONFIG = {
|
|
|
158
153
|
|
|
159
154
|
export type ElementExtractionConfig = typeof ELEMENT_EXTRACTION_CONFIG;
|
|
160
155
|
export type RawElementData = NonNullable<ReturnType<typeof extractElementData>>;
|
|
161
|
-
export type VisibleOverlayExtractionConfig = {
|
|
162
|
-
interactiveContentSelector: string;
|
|
163
|
-
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
164
|
-
overlaySelectors: readonly string[];
|
|
165
|
-
overlaySemanticSelector: string;
|
|
166
|
-
visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
|
|
167
|
-
geometryFallback?: boolean;
|
|
168
|
-
};
|
|
169
156
|
export type ComponentScopeExtractionConfig = {
|
|
170
157
|
eidxAttr: string;
|
|
171
158
|
interactiveControlSelector: string;
|
|
@@ -449,80 +436,6 @@ export function getElementDataExtractorSource(): string {
|
|
|
449
436
|
return extractElementData.toString();
|
|
450
437
|
}
|
|
451
438
|
|
|
452
|
-
export function extractVisibleOverlayHtml(config: VisibleOverlayExtractionConfig): string {
|
|
453
|
-
function isVisible(element: Element): boolean {
|
|
454
|
-
const html = element as HTMLElement;
|
|
455
|
-
const style = window.getComputedStyle(html);
|
|
456
|
-
const rect = html.getBoundingClientRect();
|
|
457
|
-
if (rect.width === 0 && rect.height === 0) return false;
|
|
458
|
-
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
459
|
-
if (Number.parseFloat(style.opacity || '1') < config.visibilityLimits.minOpacity) return false;
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function getUsefulContent(element: Element): { interactiveCount: number; text: string } {
|
|
464
|
-
const text = (element.textContent || '').replace(/\s+/g, ' ').trim();
|
|
465
|
-
const interactiveCount = element.querySelectorAll(config.interactiveContentSelector).length;
|
|
466
|
-
return { interactiveCount, text };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function isLikelyFloatingOverlay(element: Element): boolean {
|
|
470
|
-
const html = element as HTMLElement;
|
|
471
|
-
const style = window.getComputedStyle(html);
|
|
472
|
-
const rect = html.getBoundingClientRect();
|
|
473
|
-
const zIndex = Number.parseInt(style.zIndex || '0', 10);
|
|
474
|
-
const isFloating = style.position === 'fixed' || style.position === 'absolute' || style.position === 'sticky' || zIndex > 0;
|
|
475
|
-
if (!isFloating) return false;
|
|
476
|
-
if (rect.width < config.visibilityLimits.minOverlayWidth || rect.height < config.visibilityLimits.minOverlayHeight) return false;
|
|
477
|
-
if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth) return false;
|
|
478
|
-
if (rect.width >= window.innerWidth * config.visibilityLimits.maxViewportOverlayRatio && rect.height >= window.innerHeight * config.visibilityLimits.maxViewportOverlayRatio) return false;
|
|
479
|
-
const { interactiveCount, text } = getUsefulContent(element);
|
|
480
|
-
return interactiveCount > 0 || text.length > 0;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function isFloatingOverlay(element: Element): boolean {
|
|
484
|
-
const style = window.getComputedStyle(element as HTMLElement);
|
|
485
|
-
return style.position === 'fixed' || style.position === 'absolute' || Number.parseInt(style.zIndex || '0', 10) > 0;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const seen = new Set<Element>();
|
|
489
|
-
const collected: Element[] = [];
|
|
490
|
-
for (const selector of config.overlaySelectors) {
|
|
491
|
-
for (const element of Array.from(document.querySelectorAll(selector))) {
|
|
492
|
-
if (seen.has(element)) continue;
|
|
493
|
-
seen.add(element);
|
|
494
|
-
if (!isVisible(element)) continue;
|
|
495
|
-
if (!element.matches(config.overlaySemanticSelector) && !isFloatingOverlay(element)) continue;
|
|
496
|
-
const { interactiveCount, text } = getUsefulContent(element);
|
|
497
|
-
if (interactiveCount === 0 && text.length === 0) continue;
|
|
498
|
-
collected.push(element);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const overlays = collected.filter((element) => !collected.some((other) => other !== element && element.contains(other))).map((element) => (element as HTMLElement).outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
503
|
-
|
|
504
|
-
if (overlays.length === 0 && config.geometryFallback !== false) {
|
|
505
|
-
const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
|
|
506
|
-
.filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
|
|
507
|
-
.sort((left, right) => {
|
|
508
|
-
const leftStyle = window.getComputedStyle(left as HTMLElement);
|
|
509
|
-
const rightStyle = window.getComputedStyle(right as HTMLElement);
|
|
510
|
-
const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10) || 0;
|
|
511
|
-
const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10) || 0;
|
|
512
|
-
if (leftZ !== rightZ) return rightZ - leftZ;
|
|
513
|
-
const leftRect = (left as HTMLElement).getBoundingClientRect();
|
|
514
|
-
const rightRect = (right as HTMLElement).getBoundingClientRect();
|
|
515
|
-
return leftRect.width * leftRect.height - rightRect.width * rightRect.height;
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
for (const element of floatingCandidates.slice(0, config.limits.maxOverlayCount)) {
|
|
519
|
-
overlays.push((element as HTMLElement).outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return overlays.slice(0, config.limits.maxOverlayCount).join('\n\n--- overlay ---\n\n');
|
|
524
|
-
}
|
|
525
|
-
|
|
526
439
|
export function extractComponentScopeHtml(eidx: string, config: ComponentScopeExtractionConfig): string {
|
|
527
440
|
const element = document.querySelector(`[${config.eidxAttr}="${eidx}"]`);
|
|
528
441
|
if (!element) return '';
|
|
@@ -544,10 +457,6 @@ export function extractComponentScopeHtml(eidx: string, config: ComponentScopeEx
|
|
|
544
457
|
return '';
|
|
545
458
|
}
|
|
546
459
|
|
|
547
|
-
export function getVisibleOverlayHtmlExtractorSource(): string {
|
|
548
|
-
return extractVisibleOverlayHtml.toString();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
460
|
export function getComponentScopeHtmlExtractorSource(): string {
|
|
552
461
|
return extractComponentScopeHtml.toString();
|
|
553
462
|
}
|
|
@@ -1020,6 +929,19 @@ export function htmlCombinedSnapshot(html: string, htmlConfig?: HtmlConfig['comb
|
|
|
1020
929
|
return serialize(document);
|
|
1021
930
|
}
|
|
1022
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Cleans a small HTML snippet for AI consumption: keeps structure and text,
|
|
934
|
+
* drops noisy attributes and generated class names
|
|
935
|
+
*/
|
|
936
|
+
export function cleanHtmlSnippet(html: string): string {
|
|
937
|
+
const fragment = parseFragment(html);
|
|
938
|
+
for (const node of fragment.childNodes) {
|
|
939
|
+
if (!('tagName' in node)) continue;
|
|
940
|
+
cleanAllElements(node as parse5TreeAdapter.Element);
|
|
941
|
+
}
|
|
942
|
+
return serialize(fragment);
|
|
943
|
+
}
|
|
944
|
+
|
|
1023
945
|
/**
|
|
1024
946
|
* Creates text-only snapshot with markdown formatting
|
|
1025
947
|
*/
|
|
@@ -1526,6 +1448,7 @@ function cleanElement(element: parse5TreeAdapter.Element): void {
|
|
|
1526
1448
|
if (attr.name === 'class') {
|
|
1527
1449
|
attr.value = attr.value
|
|
1528
1450
|
.split(/\s+/)
|
|
1451
|
+
.filter(Boolean)
|
|
1529
1452
|
.filter((className) => !/\d/.test(className))
|
|
1530
1453
|
.filter((className) => !className.includes(':'))
|
|
1531
1454
|
.filter((className) => !TAILWIND_CLASS_PATTERNS.some((pattern) => pattern.test(className)))
|
package/src/utils/overlay.ts
CHANGED
|
@@ -1,51 +1,254 @@
|
|
|
1
1
|
import { detectFocusArea } from './aria.js';
|
|
2
|
-
import
|
|
2
|
+
import type { HtmlDiffPart } from './html-diff.js';
|
|
3
|
+
import { pathToXPath } from './html-diff.js';
|
|
4
|
+
import { extractHeadings } from './html.js';
|
|
5
|
+
import { createDebug } from './logger.js';
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
6
|
-
modalOverlays: ['[role="dialog"]', '[role="alertdialog"]', '[aria-modal="true"]', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
7
|
-
overlaySemanticSelector: '[role="dialog"], [role="alertdialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="tooltip"]',
|
|
8
|
-
} as const;
|
|
7
|
+
const debugLog = createDebug('explorbot:overlay');
|
|
9
8
|
|
|
10
|
-
export type
|
|
9
|
+
export type OverlayType = 'modal' | 'region';
|
|
10
|
+
export type OverlayData = {
|
|
11
|
+
type?: OverlayType | null;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
root?: string | null;
|
|
14
|
+
html?: string | null;
|
|
15
|
+
xpath?: string | null;
|
|
16
|
+
parent?: OverlayData | null;
|
|
17
|
+
};
|
|
11
18
|
|
|
12
19
|
export class Overlay {
|
|
13
|
-
readonly type:
|
|
20
|
+
readonly type: OverlayType | null;
|
|
14
21
|
readonly name: string | null;
|
|
22
|
+
readonly root: string | null;
|
|
23
|
+
readonly html: string | null;
|
|
24
|
+
readonly xpath: string | null;
|
|
25
|
+
readonly parent: OverlayData | null;
|
|
15
26
|
|
|
16
27
|
constructor(data: OverlayData = {}) {
|
|
17
28
|
this.type = data.type ?? null;
|
|
18
29
|
this.name = data.name ?? null;
|
|
30
|
+
this.root = data.root ?? null;
|
|
31
|
+
this.html = data.html ?? null;
|
|
32
|
+
this.xpath = data.xpath ?? null;
|
|
33
|
+
this.parent = data.parent ?? null;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
get detected(): boolean {
|
|
37
|
+
return this.type !== null && this.type !== 'region';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get present(): boolean {
|
|
22
41
|
return this.type !== null;
|
|
23
42
|
}
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
describe(): string {
|
|
45
|
+
if (!this.present) return '';
|
|
46
|
+
let text = `${this.type} "${this.name || 'unnamed'}" opened`;
|
|
47
|
+
if (this.root) text += `, scope: ${this.root}`;
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
withGeometry(geometry: Overlay): Overlay {
|
|
52
|
+
return new Overlay({
|
|
53
|
+
type: this.type,
|
|
54
|
+
name: this.name || geometry.name,
|
|
55
|
+
root: geometry.root,
|
|
56
|
+
html: geometry.html,
|
|
57
|
+
xpath: geometry.xpath,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
withParent(parent: Overlay): Overlay {
|
|
62
|
+
return new Overlay({
|
|
63
|
+
type: this.type,
|
|
64
|
+
name: this.name,
|
|
65
|
+
root: this.root,
|
|
66
|
+
html: this.html,
|
|
67
|
+
xpath: this.xpath,
|
|
68
|
+
parent: { type: parent.type, name: parent.name, root: parent.root, xpath: parent.xpath },
|
|
69
|
+
});
|
|
29
70
|
}
|
|
30
71
|
|
|
31
72
|
static fromAria(snapshot: string | null): Overlay {
|
|
32
73
|
return new Overlay(detectFocusArea(snapshot));
|
|
33
74
|
}
|
|
34
75
|
|
|
35
|
-
static resolve(data: {
|
|
36
|
-
if (data.overlayHtml) return Overlay.fromHtml(data.overlayHtml);
|
|
76
|
+
static resolve(data: { overlay?: OverlayData | null; ariaSnapshot?: string | null }): Overlay {
|
|
37
77
|
if (data.overlay) return new Overlay(data.overlay);
|
|
38
78
|
return Overlay.fromAria(data.ariaSnapshot ?? null);
|
|
39
79
|
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class OverlayPage {
|
|
83
|
+
constructor(private page: { evaluate(fn: any, arg: any): Promise<any> } | null) {}
|
|
84
|
+
|
|
85
|
+
async detectRegion(diff: RegionDiff): Promise<Overlay | null> {
|
|
86
|
+
if (!diff.sameUrl && diff.similarity < SOFT_NAVIGATION_SIMILARITY) return null;
|
|
87
|
+
for (const subRoot of this.appearedSubRoots(diff)) {
|
|
88
|
+
const probe = await this.probe(subRoot.elementXPath);
|
|
89
|
+
if (probe?.found && probe.onScreen && !probe.centerBelongs) {
|
|
90
|
+
debugLog('Appeared region is hidden or covered, trying the next candidate');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const overlay = this.toOverlay(subRoot, probe, diff.previousHtml);
|
|
94
|
+
if (!overlay.name && !overlay.root) {
|
|
95
|
+
debugLog('Appeared region has no name and no semantic root, ignoring it');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
debugLog(`Region detected: ${overlay.describe()}`);
|
|
99
|
+
return overlay;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async isStillOpen(overlay: Overlay): Promise<boolean> {
|
|
105
|
+
if (!overlay.xpath) return true;
|
|
106
|
+
const probe = await this.probe(overlay.xpath);
|
|
107
|
+
if (!probe) return true;
|
|
108
|
+
if (!probe.found) return false;
|
|
109
|
+
if (probe.onScreen && !probe.centerBelongs && !probe.coveredByFloating) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private appearedSubRoots(diff: RegionDiff): AppearedSubRoot[] {
|
|
114
|
+
const candidates: AppearedSubRoot[] = [];
|
|
115
|
+
for (const part of diff.parts) {
|
|
116
|
+
const appeared = part.added.find((line) => line.startsWith('ELEMENT:'));
|
|
117
|
+
if (!appeared) continue;
|
|
118
|
+
if (part.subtree.length < SUBROOT_MIN_HTML) continue;
|
|
119
|
+
if (diff.pageSize > 0 && part.rawSize > REGION_MAX_RATIO * diff.pageSize) {
|
|
120
|
+
debugLog(`Appeared subtree spans ${Math.round((part.rawSize / diff.pageSize) * 100)}% of the page — a new state, not a region`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
candidates.push({
|
|
124
|
+
container: part.container,
|
|
125
|
+
elementXPath: pathToXPath(appeared.slice('ELEMENT:'.length)),
|
|
126
|
+
subtree: part.subtree,
|
|
127
|
+
size: part.subtree.length,
|
|
128
|
+
rawSize: part.rawSize,
|
|
129
|
+
appearedSelector: part.appearedSelector,
|
|
130
|
+
fresh: !!this.freshHeading(part.subtree, diff.previousHtml),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (candidates.length === 0) return [];
|
|
134
|
+
const totalRawSize = candidates.reduce((sum, c) => sum + c.rawSize, 0);
|
|
135
|
+
const largest = Math.max(...candidates.map((c) => c.rawSize));
|
|
136
|
+
if (largest < REGION_DOMINANCE * totalRawSize) {
|
|
137
|
+
debugLog('Changes are scattered across the page, no dominant region');
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
candidates.sort((a, b) => Number(b.fresh) - Number(a.fresh) || b.size - a.size);
|
|
141
|
+
return candidates;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async probe(xpath: string): Promise<RegionProbe | null> {
|
|
145
|
+
if (!this.page) return null;
|
|
146
|
+
return this.page
|
|
147
|
+
.evaluate(
|
|
148
|
+
({ probeSource, config }: { probeSource: string; config: any }) => {
|
|
149
|
+
const probe = new Function(`return ${probeSource}`)() as (config: any) => any;
|
|
150
|
+
return probe(config);
|
|
151
|
+
},
|
|
152
|
+
{ probeSource: inspectRegion.toString(), config: { xpath } }
|
|
153
|
+
)
|
|
154
|
+
.catch((err: Error) => {
|
|
155
|
+
debugLog('Region probe failed:', err.message);
|
|
156
|
+
return null;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private toOverlay(subRoot: AppearedSubRoot, probe: RegionProbe | null, previousHtml: string): Overlay {
|
|
161
|
+
let type: OverlayType = 'region';
|
|
162
|
+
if (probe?.centerBelongs && probe.floating) type = 'modal';
|
|
163
|
+
let root: string | null = null;
|
|
164
|
+
if (subRoot.container !== 'body' && !subRoot.container.startsWith('//')) root = subRoot.container;
|
|
165
|
+
if (!root && subRoot.appearedSelector) root = subRoot.appearedSelector;
|
|
166
|
+
return new Overlay({ type, name: this.nameFrom(subRoot.subtree, previousHtml), root, html: subRoot.subtree, xpath: subRoot.elementXPath });
|
|
167
|
+
}
|
|
40
168
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
48
|
-
geometryFallback: false,
|
|
49
|
-
};
|
|
169
|
+
private nameFrom(html: string, previousHtml: string): string | null {
|
|
170
|
+
const fresh = this.freshHeading(html, previousHtml);
|
|
171
|
+
if (fresh) return fresh;
|
|
172
|
+
const candidates = this.headingsOf(html);
|
|
173
|
+
if (candidates.length === 0) return null;
|
|
174
|
+
return candidates.join(' ');
|
|
50
175
|
}
|
|
176
|
+
|
|
177
|
+
private freshHeading(html: string, previousHtml: string): string | null {
|
|
178
|
+
const fresh = this.headingsOf(html).filter((heading) => !previousHtml.includes(heading));
|
|
179
|
+
return fresh[0] ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private headingsOf(html: string): string[] {
|
|
183
|
+
const headings = extractHeadings(html);
|
|
184
|
+
return [headings.h1, headings.h2, headings.h3, headings.h4].filter(Boolean) as string[];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const SUBROOT_MIN_HTML = 5_000;
|
|
189
|
+
const SOFT_NAVIGATION_SIMILARITY = 50;
|
|
190
|
+
const REGION_MAX_RATIO = 0.6;
|
|
191
|
+
const REGION_DOMINANCE = 0.7;
|
|
192
|
+
|
|
193
|
+
// Serialized via toString() into page.evaluate — must stay a plain function with no outer-scope references.
|
|
194
|
+
function inspectRegion(config: { xpath: string }): RegionProbe {
|
|
195
|
+
const probe: RegionProbe = { found: false, onScreen: false, floating: false, centerBelongs: false, coveredByFloating: false };
|
|
196
|
+
|
|
197
|
+
const isFloating = (start: HTMLElement | null): boolean => {
|
|
198
|
+
let node = start;
|
|
199
|
+
while (node && node !== document.body) {
|
|
200
|
+
const style = window.getComputedStyle(node);
|
|
201
|
+
if (style.position === 'fixed' || style.position === 'absolute' || (Number.parseInt(style.zIndex || '0', 10) || 0) > 0) return true;
|
|
202
|
+
node = node.parentElement;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = document.evaluate(config.xpath, document, null, 9, null);
|
|
208
|
+
const node = result.singleNodeValue;
|
|
209
|
+
if (!node || node.nodeType !== 1) return probe;
|
|
210
|
+
const element = node as HTMLElement;
|
|
211
|
+
const rect = element.getBoundingClientRect();
|
|
212
|
+
if (rect.width === 0 && rect.height === 0) return probe;
|
|
213
|
+
probe.found = true;
|
|
214
|
+
|
|
215
|
+
probe.floating = isFloating(element);
|
|
216
|
+
|
|
217
|
+
const left = Math.max(rect.left, 0);
|
|
218
|
+
const top = Math.max(rect.top, 0);
|
|
219
|
+
const right = Math.min(rect.right, window.innerWidth);
|
|
220
|
+
const bottom = Math.min(rect.bottom, window.innerHeight);
|
|
221
|
+
if (right <= left || bottom <= top) return probe;
|
|
222
|
+
probe.onScreen = true;
|
|
223
|
+
|
|
224
|
+
const hit = document.elementFromPoint((left + right) / 2, (top + bottom) / 2);
|
|
225
|
+
probe.centerBelongs = !!hit && (hit === element || element.contains(hit));
|
|
226
|
+
if (hit && !probe.centerBelongs) probe.coveredByFloating = isFloating(hit as HTMLElement);
|
|
227
|
+
return probe;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface RegionDiff {
|
|
231
|
+
parts: HtmlDiffPart[];
|
|
232
|
+
pageSize: number;
|
|
233
|
+
similarity: number;
|
|
234
|
+
sameUrl: boolean;
|
|
235
|
+
previousHtml: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface AppearedSubRoot {
|
|
239
|
+
container: string;
|
|
240
|
+
elementXPath: string;
|
|
241
|
+
subtree: string;
|
|
242
|
+
size: number;
|
|
243
|
+
rawSize: number;
|
|
244
|
+
appearedSelector?: string;
|
|
245
|
+
fresh: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface RegionProbe {
|
|
249
|
+
found: boolean;
|
|
250
|
+
onScreen: boolean;
|
|
251
|
+
floating: boolean;
|
|
252
|
+
centerBelongs: boolean;
|
|
253
|
+
coveredByFloating: boolean;
|
|
51
254
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RequestResult } from '../api/request-result.ts';
|
|
2
|
+
|
|
3
|
+
export class RequestMap {
|
|
4
|
+
private requests = new Map<string, RequestResult>();
|
|
5
|
+
|
|
6
|
+
constructor(requests: RequestResult[] = []) {
|
|
7
|
+
for (const request of requests) this.add(request);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(request: RequestResult): void {
|
|
11
|
+
const { id } = request.extractIdAndTitle();
|
|
12
|
+
if (id === undefined) return;
|
|
13
|
+
this.requests.set(String(id), request);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(id: string | number): RequestResult | undefined {
|
|
17
|
+
return this.requests.get(String(id));
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/utils/url-matcher.ts
CHANGED
|
@@ -9,14 +9,15 @@ export function isDynamicSegment(segment: string): boolean {
|
|
|
9
9
|
/* config not loaded yet */
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
if (/^v\d+$/i.test(segment)) return false;
|
|
12
13
|
// numeric: /users/123
|
|
13
14
|
if (/^\d+$/.test(segment)) return true;
|
|
14
15
|
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
15
16
|
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return true;
|
|
16
17
|
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
17
18
|
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment)) return true;
|
|
18
|
-
// hex ID (4+ chars): /suite/70dae98a
|
|
19
|
-
if (/^[a-f0-9]{4,}$/i.test(segment)) return true;
|
|
19
|
+
// hex ID (4+ chars): /suite/70dae98a — short letters-only hex reads as a word
|
|
20
|
+
if (/^[a-f0-9]{4,}$/i.test(segment) && (segment.length >= 8 || /\d/.test(segment))) return true;
|
|
20
21
|
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
21
22
|
if (/^[a-f0-9]{8,}-/i.test(segment)) return true;
|
|
22
23
|
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|