explorbot 0.0.5 → 0.1.0
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 +4 -3
- package/dist/bin/explorbot-cli.js +4 -3
- package/dist/src/action.js +14 -11
- package/dist/src/ai/planner/subpages.js +42 -6
- package/dist/src/ai/planner.js +15 -3
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/coordinates.js +4 -2
- package/dist/src/ai/researcher/deep-analysis.js +16 -19
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/parser.js +4 -3
- package/dist/src/ai/researcher/research-result.js +2 -0
- package/dist/src/ai/researcher.js +6 -5
- package/dist/src/ai/tools.js +4 -0
- package/dist/src/commands/context-command.js +2 -2
- package/dist/src/commands/explore-command.js +1 -1
- package/dist/src/commands/init-command.js +4 -2
- package/dist/src/commands/plan-command.js +6 -1
- package/dist/src/explorbot.js +1 -1
- package/dist/src/explorer.js +58 -16
- package/dist/src/utils/web-element.js +6 -4
- package/package.json +2 -2
- package/src/action.ts +14 -10
- package/src/ai/planner/subpages.ts +37 -7
- package/src/ai/planner.ts +16 -3
- package/src/ai/researcher/cache.ts +14 -8
- package/src/ai/researcher/coordinates.ts +8 -7
- package/src/ai/researcher/deep-analysis.ts +18 -21
- package/src/ai/researcher/locators.ts +3 -3
- package/src/ai/researcher/parser.ts +4 -4
- package/src/ai/researcher/research-result.ts +1 -0
- package/src/ai/researcher.ts +6 -5
- package/src/ai/tools.ts +5 -0
- package/src/commands/context-command.ts +2 -2
- package/src/commands/explore-command.ts +1 -1
- package/src/commands/init-command.ts +5 -2
- package/src/commands/plan-command.ts +6 -1
- package/src/config.ts +1 -0
- package/src/explorbot.ts +1 -1
- package/src/explorer.ts +67 -20
- package/src/utils/web-element.ts +12 -10
package/src/explorer.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { Test } from './test-plan.ts';
|
|
|
19
19
|
import { RequestStore } from './api/request-store.ts';
|
|
20
20
|
import { XhrCapture } from './api/xhr-capture.ts';
|
|
21
21
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
22
|
+
import { WebElement, extractElementData } from './utils/web-element.ts';
|
|
22
23
|
|
|
23
24
|
declare global {
|
|
24
25
|
namespace NodeJS {
|
|
@@ -308,35 +309,23 @@ class Explorer {
|
|
|
308
309
|
return action;
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
async annotateElements(): Promise<
|
|
312
|
-
|
|
313
|
-
const roles = ['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem'];
|
|
314
|
-
let idx = 1;
|
|
315
|
-
for (const role of roles) {
|
|
316
|
-
const elements = await page.getByRole(role).all();
|
|
317
|
-
for (const el of elements) {
|
|
318
|
-
await el.evaluate((node: Element, i: number) => {
|
|
319
|
-
node.setAttribute('data-explorbot-eidx', String(i));
|
|
320
|
-
}, idx);
|
|
321
|
-
idx++;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return idx - 1;
|
|
312
|
+
async annotateElements(): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
313
|
+
return annotatePageElements(this.playwrightHelper.page);
|
|
325
314
|
}
|
|
326
315
|
|
|
327
316
|
async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
|
|
328
317
|
return visuallyAnnotateContainers(this.playwrightHelper.page, opts?.containers || []);
|
|
329
318
|
}
|
|
330
319
|
|
|
331
|
-
async getEidxInContainer(containerCss: string | null): Promise<
|
|
320
|
+
async getEidxInContainer(containerCss: string | null): Promise<string[]> {
|
|
332
321
|
const page = this.playwrightHelper.page;
|
|
333
322
|
try {
|
|
334
323
|
const selector = containerCss ? `${containerCss} [data-explorbot-eidx]` : '[data-explorbot-eidx]';
|
|
335
324
|
const elements = await page.locator(selector).all();
|
|
336
|
-
const result:
|
|
325
|
+
const result: string[] = [];
|
|
337
326
|
for (const el of elements) {
|
|
338
327
|
const attr = await el.getAttribute('data-explorbot-eidx');
|
|
339
|
-
if (attr) result.push(
|
|
328
|
+
if (attr) result.push(attr);
|
|
340
329
|
}
|
|
341
330
|
return result;
|
|
342
331
|
} catch (error) {
|
|
@@ -348,13 +337,12 @@ class Explorer {
|
|
|
348
337
|
}
|
|
349
338
|
}
|
|
350
339
|
|
|
351
|
-
async getEidxByLocator(locator: string, container?: string | null): Promise<
|
|
340
|
+
async getEidxByLocator(locator: string, container?: string | null): Promise<string | null> {
|
|
352
341
|
try {
|
|
353
342
|
const page = this.playwrightHelper.page;
|
|
354
343
|
const base = container ? page.locator(container) : page;
|
|
355
344
|
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
356
|
-
|
|
357
|
-
return eidx ? Number.parseInt(eidx, 10) : null;
|
|
345
|
+
return await el.first().getAttribute('data-explorbot-eidx');
|
|
358
346
|
} catch (error) {
|
|
359
347
|
if (this.isFatalBrowserError(error)) {
|
|
360
348
|
tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
|
|
@@ -710,4 +698,63 @@ function toCodeceptjsTest(test: Test): any {
|
|
|
710
698
|
return codeceptjsTest;
|
|
711
699
|
}
|
|
712
700
|
|
|
701
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
702
|
+
|
|
703
|
+
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
704
|
+
|
|
705
|
+
function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string; ref: string }> {
|
|
706
|
+
const entries: Array<{ role: string; name: string; ref: string }> = [];
|
|
707
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
708
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
709
|
+
if (!match) continue;
|
|
710
|
+
if (!ANNOTATABLE_ROLES.has(match[2])) continue;
|
|
711
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
712
|
+
}
|
|
713
|
+
return entries;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
717
|
+
const ariaSnapshot: string = await page.locator('body').ariaSnapshot({ forAI: true });
|
|
718
|
+
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
719
|
+
|
|
720
|
+
const byRole = new Map<string, Array<{ name: string; ref: string }>>();
|
|
721
|
+
for (const { role, name, ref } of refEntries) {
|
|
722
|
+
let list = byRole.get(role);
|
|
723
|
+
if (!list) {
|
|
724
|
+
list = [];
|
|
725
|
+
byRole.set(role, list);
|
|
726
|
+
}
|
|
727
|
+
list.push({ name, ref });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const elements: WebElement[] = [];
|
|
731
|
+
for (const [role, entries] of byRole) {
|
|
732
|
+
try {
|
|
733
|
+
const rawList = await page.getByRole(role).evaluateAll(
|
|
734
|
+
(domElements: Element[], [data, extractFnStr]: [Array<{ name: string; ref: string }>, string]) => {
|
|
735
|
+
const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
|
|
736
|
+
const results: any[] = [];
|
|
737
|
+
let ariaIdx = 0;
|
|
738
|
+
for (const el of domElements) {
|
|
739
|
+
if (ariaIdx >= data.length) break;
|
|
740
|
+
el.setAttribute('data-explorbot-eidx', data[ariaIdx].ref);
|
|
741
|
+
const elData = extract(el);
|
|
742
|
+
if (elData) results.push(elData);
|
|
743
|
+
ariaIdx++;
|
|
744
|
+
}
|
|
745
|
+
return results;
|
|
746
|
+
},
|
|
747
|
+
[entries, extractElementData.toString()]
|
|
748
|
+
);
|
|
749
|
+
for (const raw of rawList) {
|
|
750
|
+
elements.push(WebElement.fromRawData(raw, role));
|
|
751
|
+
}
|
|
752
|
+
} catch {
|
|
753
|
+
debugLog(`Failed to annotate role=${role}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return { ariaSnapshot, elements };
|
|
758
|
+
}
|
|
759
|
+
|
|
713
760
|
export default Explorer;
|
package/src/utils/web-element.ts
CHANGED
|
@@ -7,6 +7,7 @@ type RawElementData = NonNullable<ReturnType<typeof extractElementData>>;
|
|
|
7
7
|
|
|
8
8
|
export class WebElement {
|
|
9
9
|
tag: string;
|
|
10
|
+
role: string;
|
|
10
11
|
xpath: string;
|
|
11
12
|
clickXPath: string;
|
|
12
13
|
attrs: Record<string, string>;
|
|
@@ -14,8 +15,9 @@ export class WebElement {
|
|
|
14
15
|
outerHTML: string;
|
|
15
16
|
x: number;
|
|
16
17
|
y: number;
|
|
17
|
-
constructor(data: { tag: string; xpath: string; clickXPath: string; attrs: Record<string, string>; text: string; outerHTML?: string; x: number; y: number }) {
|
|
18
|
+
constructor(data: { tag: string; role?: string; xpath: string; clickXPath: string; attrs: Record<string, string>; text: string; outerHTML?: string; x: number; y: number }) {
|
|
18
19
|
this.tag = data.tag;
|
|
20
|
+
this.role = data.role || data.attrs.role || '';
|
|
19
21
|
this.xpath = data.xpath;
|
|
20
22
|
this.clickXPath = data.clickXPath;
|
|
21
23
|
this.attrs = data.attrs;
|
|
@@ -40,9 +42,8 @@ export class WebElement {
|
|
|
40
42
|
return `(${this.x}, ${this.y})`;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
get eidx():
|
|
44
|
-
|
|
45
|
-
return val ? Number.parseInt(val, 10) : null;
|
|
45
|
+
get eidx(): string | null {
|
|
46
|
+
return this.attrs['data-explorbot-eidx'] || this.attrs.eidx || null;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
get isNavigationLink(): boolean {
|
|
@@ -56,9 +57,10 @@ export class WebElement {
|
|
|
56
57
|
return cls.split(/\s+/).filter((c) => c.length > 2 && !isDynamicId(c) && !isGenericClass(c));
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
static fromRawData(d: RawElementData, role?: string): WebElement {
|
|
60
61
|
return new WebElement({
|
|
61
62
|
tag: d.tag,
|
|
63
|
+
role,
|
|
62
64
|
xpath: '',
|
|
63
65
|
clickXPath: buildClickableXPath({ tag: d.tag, allAttrs: d.allAttrs, text: d.text } as XPathMatch),
|
|
64
66
|
attrs: d.allAttrs,
|
|
@@ -93,15 +95,15 @@ export class WebElement {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
static async fromEidx(page: any, eidx:
|
|
98
|
+
static async fromEidx(page: any, eidx: string): Promise<WebElement | null> {
|
|
97
99
|
return WebElement.fromPlaywrightLocator(page.locator(`[data-explorbot-eidx="${eidx}"]`));
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
static async fromEidxList(page: any, eidxList:
|
|
102
|
+
static async fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]> {
|
|
101
103
|
if (eidxList.length === 0) return [];
|
|
102
104
|
|
|
103
105
|
const rawList: RawElementData[] = await page.evaluate(
|
|
104
|
-
([list, extractFnStr]: [
|
|
106
|
+
([list, extractFnStr]: [string[], string]) => {
|
|
105
107
|
const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
|
|
106
108
|
const results: any[] = [];
|
|
107
109
|
for (const eidx of list) {
|
|
@@ -112,7 +114,7 @@ export class WebElement {
|
|
|
112
114
|
}
|
|
113
115
|
return results;
|
|
114
116
|
},
|
|
115
|
-
[eidxList, extractElementData.toString()] as [
|
|
117
|
+
[eidxList, extractElementData.toString()] as [string[], string]
|
|
116
118
|
);
|
|
117
119
|
|
|
118
120
|
return rawList.map((d) => WebElement.fromRawData(d));
|
|
@@ -125,7 +127,7 @@ export class WebElement {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
function extractElementData(el: Element) {
|
|
130
|
+
export function extractElementData(el: Element) {
|
|
129
131
|
const rect = el.getBoundingClientRect();
|
|
130
132
|
if (rect.width === 0 && rect.height === 0) return null;
|
|
131
133
|
|