explorbot 0.1.32-beta.1 → 0.2.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/README.md +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RawElementData } from './html.js';
|
|
1
|
+
import { EXPLORBOT_ATTRS, type RawElementData } from './html.js';
|
|
2
2
|
import { type XPathMatch } from './xpath.js';
|
|
3
3
|
export { extractElementData } from './html.js';
|
|
4
4
|
export declare class WebElement {
|
|
@@ -26,6 +26,7 @@ export declare class WebElement {
|
|
|
26
26
|
get keyAttrs(): string;
|
|
27
27
|
get coordinates(): string;
|
|
28
28
|
get eidx(): string | null;
|
|
29
|
+
ourAttr(name: keyof typeof EXPLORBOT_ATTRS): string | undefined;
|
|
29
30
|
get isNavigationLink(): boolean;
|
|
30
31
|
get filteredClasses(): string[];
|
|
31
32
|
get areaHints(): string[];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sanitizeCodeBlock(code: string): string;
|
|
2
|
+
export declare function hasPlaywrightCommands(code: string): boolean;
|
|
3
|
+
export declare function playwrightSandbox(page: any, code: string): Promise<any>;
|
|
4
|
+
export declare function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
3
|
+
import step from 'codeceptjs/steps';
|
|
4
|
+
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
5
|
+
const ALLOWED_COMMAND_HEADS = ['I.', 'page.', 'await page.', 'await I.'];
|
|
6
|
+
const PLAYWRIGHT_COMMAND_HEADS = ['page.', 'await page.'];
|
|
7
|
+
const PLAYWRIGHT_ARG_NAMES = ['page'];
|
|
8
|
+
const CODECEPT_ARG_NAMES = ['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'];
|
|
9
|
+
export function sanitizeCodeBlock(code) {
|
|
10
|
+
return code
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((line) => line.trim())
|
|
13
|
+
.filter((line) => ALLOWED_COMMAND_HEADS.some((head) => line.startsWith(head)))
|
|
14
|
+
.join('\n');
|
|
15
|
+
}
|
|
16
|
+
export function hasPlaywrightCommands(code) {
|
|
17
|
+
return code.split('\n').some((line) => {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
return PLAYWRIGHT_COMMAND_HEADS.some((head) => trimmed.startsWith(head));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function playwrightSandbox(page, code) {
|
|
23
|
+
const run = createSandbox(PLAYWRIGHT_ARG_NAMES, `return (async () => { ${code} })()`);
|
|
24
|
+
return run(page);
|
|
25
|
+
}
|
|
26
|
+
export function codeceptJSSandbox(actor, codeOrFn) {
|
|
27
|
+
if (typeof codeOrFn === 'function') {
|
|
28
|
+
codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const run = createSandbox(CODECEPT_ARG_NAMES, codeOrFn);
|
|
32
|
+
run(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
33
|
+
}
|
|
34
|
+
function createSandbox(argNames, body) {
|
|
35
|
+
const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
|
|
36
|
+
return (...args) => fn(...args);
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,18 @@
|
|
|
16
16
|
"bin": {
|
|
17
17
|
"explorbot": "./dist/bin/explorbot-cli.js"
|
|
18
18
|
},
|
|
19
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"src/**/*.tsx",
|
|
23
|
+
"bin/**/*.ts",
|
|
24
|
+
"boat/api-tester/src/**/*.ts",
|
|
25
|
+
"boat/doc-collector/src/**/*.ts",
|
|
26
|
+
"boat/doc-collector/bin/**/*.ts",
|
|
27
|
+
"boat/doc-collector/package.json",
|
|
28
|
+
"rules/",
|
|
29
|
+
"assets/sample-files/"
|
|
30
|
+
],
|
|
20
31
|
"scripts": {
|
|
21
32
|
"build": "bun run build:bin",
|
|
22
33
|
"build:bin": "bun build bin/explorbot-cli.ts --outdir bin --target node --external commander --format esm",
|
|
@@ -43,7 +54,13 @@
|
|
|
43
54
|
"check:fix": "biome check --write .",
|
|
44
55
|
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts"
|
|
45
56
|
},
|
|
46
|
-
"keywords": [
|
|
57
|
+
"keywords": [
|
|
58
|
+
"cli",
|
|
59
|
+
"react",
|
|
60
|
+
"ink",
|
|
61
|
+
"codeceptjs",
|
|
62
|
+
"playwright"
|
|
63
|
+
],
|
|
47
64
|
"repository": {
|
|
48
65
|
"type": "git",
|
|
49
66
|
"url": "https://github.com/testomatio/explorbot"
|
package/src/action-result.ts
CHANGED
|
@@ -2,10 +2,12 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
|
|
4
4
|
import type { Link, WebPageState } from './state-manager.ts';
|
|
5
|
-
import { compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
|
|
5
|
+
import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
|
|
6
|
+
import { TTLCache } from './utils/cache.ts';
|
|
6
7
|
import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff } from './utils/html-diff.ts';
|
|
7
8
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
|
|
8
9
|
import { createDebug } from './utils/logger.ts';
|
|
10
|
+
import { slugify } from './utils/strings.ts';
|
|
9
11
|
import { extractStatePath, matchesUrl } from './utils/url-matcher.ts';
|
|
10
12
|
|
|
11
13
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -37,6 +39,7 @@ export interface PageDiff {
|
|
|
37
39
|
previousUrl?: string;
|
|
38
40
|
currentUrl: string;
|
|
39
41
|
ariaChanges?: string | null;
|
|
42
|
+
ariaChangeCount?: number;
|
|
40
43
|
htmlParts?: HtmlDiffPart[];
|
|
41
44
|
iframes?: string;
|
|
42
45
|
}
|
|
@@ -68,13 +71,12 @@ export class ActionResult implements ActionResultData {
|
|
|
68
71
|
private _screenshot: Buffer | undefined = undefined;
|
|
69
72
|
readonly htmlFile: string | undefined = undefined;
|
|
70
73
|
private _html: string | undefined = undefined;
|
|
71
|
-
private snapshotCache = new
|
|
74
|
+
private snapshotCache = new TTLCache<string>();
|
|
72
75
|
readonly logFile: string | undefined = undefined;
|
|
73
|
-
private _browserLogs: any[] | undefined = undefined;
|
|
74
76
|
readonly ariaSnapshotFile: string | undefined = undefined;
|
|
75
77
|
private _ariaSnapshot: string | null | undefined = undefined;
|
|
76
78
|
private _lastExtractedHtml: string | undefined = undefined;
|
|
77
|
-
notes:
|
|
79
|
+
notes: string[] = [];
|
|
78
80
|
public links: Link[] = [];
|
|
79
81
|
public verifications?: Record<string, boolean>;
|
|
80
82
|
|
|
@@ -106,14 +108,11 @@ export class ActionResult implements ActionResultData {
|
|
|
106
108
|
this.ariaSnapshotFile = data.ariaSnapshotFile;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
// Store HTML
|
|
111
|
+
// Store HTML in a private property if provided
|
|
110
112
|
if (data.html !== undefined) {
|
|
111
113
|
this._html = data.html;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
if (data.browserLogs !== undefined) {
|
|
115
|
-
this._browserLogs = data.browserLogs;
|
|
116
|
-
}
|
|
117
116
|
if (data.screenshot !== undefined) {
|
|
118
117
|
this._screenshot = data.screenshot;
|
|
119
118
|
}
|
|
@@ -121,7 +120,7 @@ export class ActionResult implements ActionResultData {
|
|
|
121
120
|
this._ariaSnapshot = data.ariaSnapshot;
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
if (!this.fullUrl && this.url
|
|
123
|
+
if (!this.fullUrl && this.url) {
|
|
125
124
|
this.fullUrl = this.url;
|
|
126
125
|
}
|
|
127
126
|
|
|
@@ -133,7 +132,7 @@ export class ActionResult implements ActionResultData {
|
|
|
133
132
|
this.links = extractLinks(this._html);
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
if (this.url
|
|
135
|
+
if (this.url) {
|
|
137
136
|
this.url = extractStatePath(this.url);
|
|
138
137
|
}
|
|
139
138
|
}
|
|
@@ -165,16 +164,6 @@ export class ActionResult implements ActionResultData {
|
|
|
165
164
|
return undefined;
|
|
166
165
|
}
|
|
167
166
|
|
|
168
|
-
get browserLogsContent(): any[] {
|
|
169
|
-
if (this._browserLogs !== undefined) {
|
|
170
|
-
return this._browserLogs;
|
|
171
|
-
}
|
|
172
|
-
if (this.logFile) {
|
|
173
|
-
return ActionResult.loadBrowserLogsFromFile(this.logFile);
|
|
174
|
-
}
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
167
|
get ariaSnapshot(): string | null {
|
|
179
168
|
if (this._ariaSnapshot !== undefined) {
|
|
180
169
|
return this._ariaSnapshot;
|
|
@@ -223,14 +212,14 @@ export class ActionResult implements ActionResultData {
|
|
|
223
212
|
}
|
|
224
213
|
|
|
225
214
|
isSameUrl(state: WebPageState): boolean {
|
|
226
|
-
if (!this.url
|
|
215
|
+
if (!this.url) {
|
|
227
216
|
return false;
|
|
228
217
|
}
|
|
229
218
|
return extractStatePath(state.url) === extractStatePath(this.url);
|
|
230
219
|
}
|
|
231
220
|
|
|
232
221
|
isMatchedBy(state: WebPageState): boolean {
|
|
233
|
-
if (!this.url
|
|
222
|
+
if (!this.url) {
|
|
234
223
|
return false;
|
|
235
224
|
}
|
|
236
225
|
|
|
@@ -266,13 +255,13 @@ export class ActionResult implements ActionResultData {
|
|
|
266
255
|
async simplifiedHtml(htmlConfig?: HtmlConfig): Promise<string> {
|
|
267
256
|
const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
|
|
268
257
|
const cacheKey = `simplified:${JSON.stringify(normalizedConfig?.minimal ?? null)}`;
|
|
269
|
-
return this.
|
|
258
|
+
return this.snapshotCache.fetch(cacheKey, () => minifyHtml(htmlMinimalUISnapshot(this.html ?? '', normalizedConfig?.minimal)));
|
|
270
259
|
}
|
|
271
260
|
|
|
272
261
|
async combinedHtml(htmlConfig?: HtmlConfig & { keepPositions?: boolean }): Promise<string> {
|
|
273
262
|
const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
|
|
274
263
|
const cacheKey = `combined:${!!htmlConfig?.keepPositions}:${JSON.stringify(normalizedConfig?.combined ?? null)}`;
|
|
275
|
-
return this.
|
|
264
|
+
return this.snapshotCache.fetch(cacheKey, async () => {
|
|
276
265
|
const combinedHtml = await minifyHtml(htmlCombinedSnapshot(this.html ?? '', normalizedConfig?.combined, { keepPositions: htmlConfig?.keepPositions }));
|
|
277
266
|
debugLog(`----${this.url}----`);
|
|
278
267
|
debugLog(`Combined HTML: \n${combinedHtml}`);
|
|
@@ -284,7 +273,7 @@ export class ActionResult implements ActionResultData {
|
|
|
284
273
|
async textHtml(htmlConfig?: HtmlConfig): Promise<string> {
|
|
285
274
|
const normalizedConfig = this.normalizeHtmlConfig(htmlConfig);
|
|
286
275
|
const cacheKey = `text:${JSON.stringify(normalizedConfig?.text ?? null)}`;
|
|
287
|
-
return this.
|
|
276
|
+
return this.snapshotCache.fetch(cacheKey, () => minifyHtml(htmlTextSnapshot(this.html ?? '', normalizedConfig?.text)));
|
|
288
277
|
}
|
|
289
278
|
|
|
290
279
|
getInteractiveARIA(): string {
|
|
@@ -303,14 +292,6 @@ export class ActionResult implements ActionResultData {
|
|
|
303
292
|
return parser.getConfig().html;
|
|
304
293
|
}
|
|
305
294
|
|
|
306
|
-
private async cachedSnapshot(key: string, compute: () => Promise<string>): Promise<string> {
|
|
307
|
-
const cached = this.snapshotCache.get(key);
|
|
308
|
-
if (cached !== undefined) return cached;
|
|
309
|
-
const value = await compute();
|
|
310
|
-
this.snapshotCache.set(key, value);
|
|
311
|
-
return value;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
295
|
static fromState(state: WebPageState): ActionResult {
|
|
315
296
|
let html: string | undefined = undefined;
|
|
316
297
|
let screenshot: Buffer | undefined = undefined;
|
|
@@ -356,26 +337,26 @@ export class ActionResult implements ActionResultData {
|
|
|
356
337
|
return new ActionResult(actionResultData);
|
|
357
338
|
}
|
|
358
339
|
|
|
359
|
-
private static
|
|
340
|
+
private static loadStateFile(file: string): string | null {
|
|
360
341
|
try {
|
|
361
|
-
const filePath = outputPath('states',
|
|
362
|
-
if (fs.existsSync(filePath))
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
342
|
+
const filePath = outputPath('states', file);
|
|
343
|
+
if (!fs.existsSync(filePath)) return null;
|
|
344
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
366
345
|
} catch (error) {
|
|
367
|
-
console.error('Failed to load
|
|
346
|
+
console.error('Failed to load state file:', error);
|
|
368
347
|
return null;
|
|
369
348
|
}
|
|
370
349
|
}
|
|
371
350
|
|
|
351
|
+
private static loadHtmlFromFile(htmlFile: string): string | null {
|
|
352
|
+
return ActionResult.loadStateFile(htmlFile);
|
|
353
|
+
}
|
|
354
|
+
|
|
372
355
|
private static loadScreenshotFromFile(screenshotFile: string): Buffer | undefined {
|
|
373
356
|
try {
|
|
374
357
|
const filePath = outputPath('states', screenshotFile);
|
|
375
|
-
if (fs.existsSync(filePath))
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
return undefined;
|
|
358
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
359
|
+
return fs.readFileSync(filePath);
|
|
379
360
|
} catch (error) {
|
|
380
361
|
console.error('Failed to load screenshot from file:', error);
|
|
381
362
|
return undefined;
|
|
@@ -383,58 +364,38 @@ export class ActionResult implements ActionResultData {
|
|
|
383
364
|
}
|
|
384
365
|
|
|
385
366
|
private static loadBrowserLogsFromFile(logFile: string): any[] {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return { text: line, type: 'log', level: 'log', message: line };
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
return [];
|
|
408
|
-
} catch (error) {
|
|
409
|
-
console.error('Failed to load browser logs from file:', error);
|
|
410
|
-
return [];
|
|
411
|
-
}
|
|
367
|
+
const logContent = ActionResult.loadStateFile(logFile);
|
|
368
|
+
if (!logContent) return [];
|
|
369
|
+
return logContent
|
|
370
|
+
.split('\n')
|
|
371
|
+
.filter((line) => line.trim())
|
|
372
|
+
.map((line) => {
|
|
373
|
+
const match = line.match(/\[([^\]]+)\] (\w+): (.+)/);
|
|
374
|
+
if (match) {
|
|
375
|
+
return {
|
|
376
|
+
timestamp: match[1],
|
|
377
|
+
type: match[2].toLowerCase(),
|
|
378
|
+
text: match[3],
|
|
379
|
+
level: match[2].toLowerCase(),
|
|
380
|
+
message: match[3],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return { text: line, type: 'log', level: 'log', message: line };
|
|
384
|
+
});
|
|
412
385
|
}
|
|
413
386
|
|
|
414
387
|
private static loadAriaSnapshotFromFile(ariaSnapshotFile: string): string | null {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
422
|
-
const trimmed = content.trim();
|
|
423
|
-
if (!trimmed) {
|
|
424
|
-
return null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return trimmed.endsWith('\n') ? trimmed : `${trimmed}\n`;
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error('Failed to load aria snapshot from file:', error);
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
388
|
+
const content = ActionResult.loadStateFile(ariaSnapshotFile);
|
|
389
|
+
if (!content) return null;
|
|
390
|
+
const trimmed = content.trim();
|
|
391
|
+
if (!trimmed) return null;
|
|
392
|
+
return trimmed.endsWith('\n') ? trimmed : `${trimmed}\n`;
|
|
432
393
|
}
|
|
433
394
|
|
|
434
395
|
toAiContext(): string {
|
|
435
396
|
const parts: string[] = [];
|
|
436
397
|
|
|
437
|
-
if (this.url
|
|
398
|
+
if (this.url) {
|
|
438
399
|
parts.push(`<url>${this.url}</url>`);
|
|
439
400
|
}
|
|
440
401
|
|
|
@@ -480,7 +441,7 @@ export class ActionResult implements ActionResultData {
|
|
|
480
441
|
}
|
|
481
442
|
|
|
482
443
|
get relativeUrl(): string | null {
|
|
483
|
-
if (!this.url
|
|
444
|
+
if (!this.url) return null;
|
|
484
445
|
|
|
485
446
|
try {
|
|
486
447
|
const urlObj = new URL(this.url);
|
|
@@ -505,22 +466,10 @@ export class ActionResult implements ActionResultData {
|
|
|
505
466
|
|
|
506
467
|
this.extractHeadings(this.html);
|
|
507
468
|
|
|
508
|
-
|
|
469
|
+
if (this.h1) parts.push(`h1_${this.h1}`);
|
|
470
|
+
if (this.h2) parts.push(`h2_${this.h2}`);
|
|
509
471
|
|
|
510
|
-
|
|
511
|
-
const value = this[heading as keyof this] as string;
|
|
512
|
-
if (value) {
|
|
513
|
-
parts.push(`${heading}_${value}`);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
let stateString = parts
|
|
518
|
-
.map((part) => part.substring(0, 100))
|
|
519
|
-
.join('_')
|
|
520
|
-
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
521
|
-
.replace(/_+/g, '_')
|
|
522
|
-
.replace(/^_|_$/g, '')
|
|
523
|
-
.toLowerCase();
|
|
472
|
+
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
524
473
|
|
|
525
474
|
if (stateString.length > 200) {
|
|
526
475
|
stateString = stateString.substring(0, 200);
|
|
@@ -532,60 +481,8 @@ export class ActionResult implements ActionResultData {
|
|
|
532
481
|
return stateString;
|
|
533
482
|
}
|
|
534
483
|
|
|
535
|
-
private saveBrowserLogs(): void {
|
|
536
|
-
const logs = this.browserLogsContent;
|
|
537
|
-
if (!logs || logs.length === 0) return;
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
const outputDir = 'output';
|
|
541
|
-
const stateHash = this.getStateHash();
|
|
542
|
-
const filename = `${stateHash}.log`;
|
|
543
|
-
const filePath = join(outputDir, filename);
|
|
544
|
-
|
|
545
|
-
// Ensure output directory exists
|
|
546
|
-
if (!fs.existsSync(outputDir)) {
|
|
547
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Format logs for saving
|
|
551
|
-
const formattedLogs = logs.map((log: any) => {
|
|
552
|
-
const timestamp = new Date().toISOString();
|
|
553
|
-
const level = (log.type || log.level || 'LOG').toUpperCase();
|
|
554
|
-
const message = log.text || log.message || String(log);
|
|
555
|
-
return `[${timestamp}] ${level}: ${message}`;
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
// Save log content to file
|
|
559
|
-
const logContent = `${formattedLogs.join('\n')}\n`;
|
|
560
|
-
fs.writeFileSync(filePath, logContent, 'utf8');
|
|
561
|
-
} catch (error) {
|
|
562
|
-
// Silently fail to avoid breaking the main flow
|
|
563
|
-
console.error('Failed to save browser logs:', error);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
private saveHtmlOutput(): void {
|
|
568
|
-
try {
|
|
569
|
-
const outputDir = 'output';
|
|
570
|
-
const stateHash = this.getStateHash();
|
|
571
|
-
const filename = `${stateHash}.html`;
|
|
572
|
-
const filePath = join(outputDir, filename);
|
|
573
|
-
|
|
574
|
-
// Ensure output directory exists
|
|
575
|
-
if (!fs.existsSync(outputDir)) {
|
|
576
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Save HTML content to file
|
|
580
|
-
fs.writeFileSync(filePath, this.html, 'utf8');
|
|
581
|
-
} catch (error) {
|
|
582
|
-
// Silently fail to avoid breaking the main flow
|
|
583
|
-
console.error('Failed to save HTML output:', error);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
484
|
async diff(previousState: ActionResult | null): Promise<Diff> {
|
|
588
|
-
return
|
|
485
|
+
return Diff.create(this, previousState);
|
|
589
486
|
}
|
|
590
487
|
|
|
591
488
|
async toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata> {
|
|
@@ -617,7 +514,6 @@ export class ActionResult implements ActionResultData {
|
|
|
617
514
|
}
|
|
618
515
|
|
|
619
516
|
const diff = await this.diff(previousState);
|
|
620
|
-
await diff.calculate();
|
|
621
517
|
|
|
622
518
|
const pageDiff: PageDiff = {
|
|
623
519
|
urlChanged,
|
|
@@ -627,6 +523,7 @@ export class ActionResult implements ActionResultData {
|
|
|
627
523
|
|
|
628
524
|
if (diff.ariaChanged) {
|
|
629
525
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
526
|
+
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
630
527
|
}
|
|
631
528
|
|
|
632
529
|
if (diff.htmlParts.length > 0) {
|
|
@@ -645,10 +542,7 @@ export class ActionResult implements ActionResultData {
|
|
|
645
542
|
}
|
|
646
543
|
|
|
647
544
|
if (pageDiff.ariaChanges && this.iframeSnapshots.length > 0) {
|
|
648
|
-
|
|
649
|
-
const removedMatch = pageDiff.ariaChanges.match(/removed: (\d+) interactive/);
|
|
650
|
-
const removedCount = removedMatch ? Number.parseInt(removedMatch[1]) : 0;
|
|
651
|
-
if (addedCount + removedCount >= 50) {
|
|
545
|
+
if (diff.ariaChangeCount >= LARGE_ARIA_CHANGE_THRESHOLD) {
|
|
652
546
|
pageDiff.iframes = this.iframeSnapshots.map((snap) => `iframe src="${snap.src}":\n${snap.html}`).join('\n\n');
|
|
653
547
|
}
|
|
654
548
|
}
|
|
@@ -686,20 +580,25 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
|
|
|
686
580
|
export class Diff {
|
|
687
581
|
private _htmlDiffResult: HtmlDiffResult | null = null;
|
|
688
582
|
private _ariaDiffResult: string | null = null;
|
|
583
|
+
private _ariaChangeCount = 0;
|
|
689
584
|
private _isSameUrl: boolean;
|
|
690
|
-
private _urlChanged: boolean;
|
|
691
585
|
|
|
692
586
|
constructor(
|
|
693
587
|
private current: ActionResult,
|
|
694
588
|
private previous: ActionResult | null
|
|
695
589
|
) {
|
|
696
590
|
this._isSameUrl = previous ? current.isSameUrl({ url: previous.url }) : false;
|
|
697
|
-
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
static async create(current: ActionResult, previous: ActionResult | null): Promise<Diff> {
|
|
594
|
+
const diff = new Diff(current, previous);
|
|
595
|
+
await diff.calculate();
|
|
596
|
+
return diff;
|
|
698
597
|
}
|
|
699
598
|
|
|
700
599
|
hasChanges(): boolean {
|
|
701
600
|
if (!this.previous) return false;
|
|
702
|
-
if (this.
|
|
601
|
+
if (!this._isSameUrl) return true;
|
|
703
602
|
|
|
704
603
|
const hasHtmlChanges = this._htmlDiffResult && (this._htmlDiffResult.parts.length > 0 || this._htmlDiffResult.added.length > 0 || this._htmlDiffResult.removed.length > 0);
|
|
705
604
|
const hasAriaChanges = this._ariaDiffResult !== null;
|
|
@@ -712,7 +611,7 @@ export class Diff {
|
|
|
712
611
|
}
|
|
713
612
|
|
|
714
613
|
urlHasChanged(): boolean {
|
|
715
|
-
return this.
|
|
614
|
+
return !this._isSameUrl;
|
|
716
615
|
}
|
|
717
616
|
|
|
718
617
|
get htmlParts(): HtmlDiffPart[] {
|
|
@@ -724,18 +623,14 @@ export class Diff {
|
|
|
724
623
|
return this._ariaDiffResult;
|
|
725
624
|
}
|
|
726
625
|
|
|
727
|
-
get
|
|
728
|
-
return this.
|
|
626
|
+
get ariaChangeCount(): number {
|
|
627
|
+
return this._ariaChangeCount;
|
|
729
628
|
}
|
|
730
629
|
|
|
731
630
|
get htmlDiff(): HtmlDiffResult | null {
|
|
732
631
|
return this._htmlDiffResult;
|
|
733
632
|
}
|
|
734
633
|
|
|
735
|
-
get ariaDiff(): string | null {
|
|
736
|
-
return this._ariaDiffResult;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
634
|
async calculate(): Promise<void> {
|
|
740
635
|
if (!this.previous) return;
|
|
741
636
|
|
|
@@ -743,6 +638,8 @@ export class Diff {
|
|
|
743
638
|
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
744
639
|
}
|
|
745
640
|
|
|
746
|
-
|
|
641
|
+
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
642
|
+
this._ariaDiffResult = ariaDiff.text;
|
|
643
|
+
this._ariaChangeCount = ariaDiff.count;
|
|
747
644
|
}
|
|
748
645
|
}
|