@tontoko/fast-playwright-mcp 0.0.5 → 0.0.7
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/lib/batch/batch-executor.js +33 -7
- package/lib/browser-server-backend.js +2 -0
- package/lib/context.js +18 -3
- package/lib/diagnostics/element-discovery.js +124 -14
- package/lib/diagnostics/resource-manager.js +3 -1
- package/lib/response.js +3 -1
- package/lib/tab.js +34 -2
- package/lib/tools/batch-execute.js +1 -1
- package/lib/tools/find-elements.js +20 -1
- package/package.json +1 -1
|
@@ -18,22 +18,32 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
20
|
// src/batch/batch-executor.ts
|
|
21
|
+
import { randomBytes } from "node:crypto";
|
|
22
|
+
import debug from "debug";
|
|
21
23
|
import { Response } from "../response.js";
|
|
22
24
|
import { mergeExpectations } from "../schemas/expectation.js";
|
|
23
25
|
import { getErrorMessage } from "../utils/common-formatters.js";
|
|
26
|
+
var batchDebug = debug("pw:mcp:batch");
|
|
24
27
|
|
|
25
28
|
class BatchExecutor {
|
|
26
29
|
toolRegistry;
|
|
27
30
|
context;
|
|
31
|
+
currentBatchContext;
|
|
28
32
|
constructor(context, toolRegistry) {
|
|
29
33
|
this.context = context;
|
|
30
34
|
this.toolRegistry = toolRegistry;
|
|
31
35
|
}
|
|
36
|
+
generateBatchId() {
|
|
37
|
+
const timestamp = Date.now();
|
|
38
|
+
const random = randomBytes(4).toString("hex");
|
|
39
|
+
return `batch_${timestamp}_${random}`;
|
|
40
|
+
}
|
|
32
41
|
validateAllSteps(steps) {
|
|
33
42
|
for (const [index, step] of steps.entries()) {
|
|
34
43
|
const tool = this.toolRegistry.get(step.tool);
|
|
35
44
|
if (!tool) {
|
|
36
|
-
|
|
45
|
+
const availableTools = Array.from(this.toolRegistry.keys()).filter((name) => name.startsWith("browser_") && name !== "browser_batch_execute").sort((a, b) => a.localeCompare(b)).join(",");
|
|
46
|
+
throw new Error(`Unknown tool: "${step.tool}" at step ${index}. Available tools: ${availableTools}`);
|
|
37
47
|
}
|
|
38
48
|
try {
|
|
39
49
|
const parseResult = tool.schema.inputSchema.safeParse({
|
|
@@ -52,6 +62,11 @@ class BatchExecutor {
|
|
|
52
62
|
const results = [];
|
|
53
63
|
const startTime = Date.now();
|
|
54
64
|
let stopReason = "completed";
|
|
65
|
+
this.currentBatchContext = {
|
|
66
|
+
batchId: this.generateBatchId(),
|
|
67
|
+
startTime
|
|
68
|
+
};
|
|
69
|
+
batchDebug(`Starting batch execution ${this.currentBatchContext.batchId} with ${options.steps.length} steps`);
|
|
55
70
|
this.validateAllSteps(options.steps);
|
|
56
71
|
const executeSequentially = async (index) => {
|
|
57
72
|
if (index >= options.steps.length) {
|
|
@@ -60,7 +75,10 @@ class BatchExecutor {
|
|
|
60
75
|
const step = options.steps[index];
|
|
61
76
|
const stepStartTime = Date.now();
|
|
62
77
|
try {
|
|
63
|
-
|
|
78
|
+
if (this.currentBatchContext) {
|
|
79
|
+
this.currentBatchContext.currentStepIndex = index;
|
|
80
|
+
}
|
|
81
|
+
const result = await this.executeStep(step, options.globalExpectation, this.currentBatchContext);
|
|
64
82
|
const stepEndTime = Date.now();
|
|
65
83
|
results.push({
|
|
66
84
|
stepIndex: index,
|
|
@@ -100,7 +118,7 @@ class BatchExecutor {
|
|
|
100
118
|
stopReason
|
|
101
119
|
};
|
|
102
120
|
}
|
|
103
|
-
async executeStep(step, globalExpectation) {
|
|
121
|
+
async executeStep(step, globalExpectation, batchContext) {
|
|
104
122
|
const tool = this.toolRegistry.get(step.tool);
|
|
105
123
|
if (!tool) {
|
|
106
124
|
throw new Error(`Unknown tool: ${step.tool}`);
|
|
@@ -110,10 +128,18 @@ class BatchExecutor {
|
|
|
110
128
|
...step.arguments,
|
|
111
129
|
expectation: mergedExpectation
|
|
112
130
|
};
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
const previousBatchContext = this.context.batchContext;
|
|
132
|
+
this.context.batchContext = batchContext;
|
|
133
|
+
try {
|
|
134
|
+
const response = new Response(this.context, step.tool, argsWithExpectation, mergedExpectation);
|
|
135
|
+
batchDebug(`Executing batch step: ${step.tool}`);
|
|
136
|
+
await tool.handle(this.context, argsWithExpectation, response);
|
|
137
|
+
await response.finish();
|
|
138
|
+
batchDebug(`Batch step ${step.tool} completed`);
|
|
139
|
+
return response.serialize();
|
|
140
|
+
} finally {
|
|
141
|
+
this.context.batchContext = previousBatchContext;
|
|
142
|
+
}
|
|
117
143
|
}
|
|
118
144
|
mergeStepExpectations(toolName, globalExpectation, stepExpectation) {
|
|
119
145
|
let merged = mergeExpectations(toolName);
|
|
@@ -79,10 +79,12 @@ class BrowserServerBackend {
|
|
|
79
79
|
throw new Error(`Tool not found: ${schema.name}`);
|
|
80
80
|
}
|
|
81
81
|
context.setRunningTool(true);
|
|
82
|
+
backendDebug(`Executing tool: ${schema.name}`);
|
|
82
83
|
try {
|
|
83
84
|
await matchedTool.handle(context, parsedArguments, response);
|
|
84
85
|
await response.finish();
|
|
85
86
|
this._sessionLog?.logResponse(response);
|
|
87
|
+
backendDebug(`Tool ${schema.name} completed successfully`);
|
|
86
88
|
} catch (error) {
|
|
87
89
|
backendDebug(`Error executing tool ${schema.name}:`, error);
|
|
88
90
|
response.addError(String(error));
|
package/lib/context.js
CHANGED
|
@@ -24,6 +24,7 @@ import { outputFile } from "./config.js";
|
|
|
24
24
|
import { logUnhandledError } from "./log.js";
|
|
25
25
|
import { Tab } from "./tab.js";
|
|
26
26
|
var testDebug = debug("pw:mcp:test");
|
|
27
|
+
var contextDebug = debug("pw:mcp:context");
|
|
27
28
|
|
|
28
29
|
class Context {
|
|
29
30
|
tools;
|
|
@@ -40,6 +41,7 @@ class Context {
|
|
|
40
41
|
_closeBrowserContextPromise;
|
|
41
42
|
_isRunningTool = false;
|
|
42
43
|
_abortController = new AbortController;
|
|
44
|
+
batchContext;
|
|
43
45
|
constructor(options) {
|
|
44
46
|
this.tools = options.tools;
|
|
45
47
|
this.config = options.config;
|
|
@@ -66,6 +68,7 @@ class Context {
|
|
|
66
68
|
return this._currentTab;
|
|
67
69
|
}
|
|
68
70
|
async newTab() {
|
|
71
|
+
contextDebug("Creating new tab");
|
|
69
72
|
const { browserContext } = await this._ensureBrowserContext();
|
|
70
73
|
const page = await browserContext.newPage();
|
|
71
74
|
const tab = this._tabs.find((t) => t.page === page);
|
|
@@ -73,6 +76,7 @@ class Context {
|
|
|
73
76
|
throw new Error("Failed to create tab: tab not found after creation");
|
|
74
77
|
}
|
|
75
78
|
this._currentTab = tab;
|
|
79
|
+
contextDebug("New tab created successfully");
|
|
76
80
|
return this._currentTab;
|
|
77
81
|
}
|
|
78
82
|
async selectTab(index) {
|
|
@@ -121,13 +125,21 @@ class Context {
|
|
|
121
125
|
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
122
126
|
}
|
|
123
127
|
if (!this._tabs.length) {
|
|
124
|
-
|
|
128
|
+
contextDebug("No tabs remaining, closing browser context");
|
|
129
|
+
this.closeBrowserContext().catch((error) => {
|
|
130
|
+
contextDebug("Error closing browser context:", error);
|
|
131
|
+
});
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
async closeBrowserContext() {
|
|
128
|
-
|
|
135
|
+
contextDebug("Closing browser context");
|
|
136
|
+
this._closeBrowserContextPromise ??= this._closeBrowserContextImpl().catch((error) => {
|
|
137
|
+
contextDebug("Failed to close browser context:", error);
|
|
138
|
+
logUnhandledError(error);
|
|
139
|
+
});
|
|
129
140
|
await this._closeBrowserContextPromise;
|
|
130
141
|
this._closeBrowserContextPromise = undefined;
|
|
142
|
+
contextDebug("Browser context closed");
|
|
131
143
|
}
|
|
132
144
|
isRunningTool() {
|
|
133
145
|
return this._isRunningTool;
|
|
@@ -175,8 +187,10 @@ class Context {
|
|
|
175
187
|
}
|
|
176
188
|
_ensureBrowserContext() {
|
|
177
189
|
this._browserContextPromise ??= (() => {
|
|
190
|
+
contextDebug("Ensuring browser context exists");
|
|
178
191
|
const promise = this._setupBrowserContext();
|
|
179
|
-
promise.catch(() => {
|
|
192
|
+
promise.catch((error) => {
|
|
193
|
+
contextDebug("Failed to setup browser context:", error);
|
|
180
194
|
this._browserContextPromise = undefined;
|
|
181
195
|
});
|
|
182
196
|
return promise;
|
|
@@ -187,6 +201,7 @@ class Context {
|
|
|
187
201
|
if (this._closeBrowserContextPromise) {
|
|
188
202
|
throw new Error("Another browser context is being closed.");
|
|
189
203
|
}
|
|
204
|
+
contextDebug("Setting up new browser context");
|
|
190
205
|
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal);
|
|
191
206
|
const { browserContext } = result;
|
|
192
207
|
await this._setupRequestInterception(browserContext);
|
|
@@ -23,7 +23,6 @@ import { DiagnosticBase } from "./common/diagnostic-base.js";
|
|
|
23
23
|
import { safeDispose } from "./common/error-enrichment-utils.js";
|
|
24
24
|
import { SmartHandleBatch } from "./smart-handle.js";
|
|
25
25
|
var elementDiscoveryDebug = debug("pw:mcp:element-discovery");
|
|
26
|
-
|
|
27
26
|
class ElementDiscovery extends DiagnosticBase {
|
|
28
27
|
smartHandleBatch;
|
|
29
28
|
maxBatchSize = 100;
|
|
@@ -358,23 +357,134 @@ class ElementDiscovery extends DiagnosticBase {
|
|
|
358
357
|
}
|
|
359
358
|
async generateSelector(element) {
|
|
360
359
|
return await element.evaluate((el) => {
|
|
360
|
+
const CLASS_SPLIT_PATTERN = "\\s+";
|
|
361
|
+
const classSplitRegex = new RegExp(CLASS_SPLIT_PATTERN);
|
|
362
|
+
const isUnique = (selector) => {
|
|
363
|
+
return document.querySelectorAll(selector).length === 1;
|
|
364
|
+
};
|
|
365
|
+
const getClasses = (elem) => {
|
|
366
|
+
return elem.className ? `.${elem.className.trim().split(classSplitRegex).join(".")}` : "";
|
|
367
|
+
};
|
|
368
|
+
const tryIdSelector = (elem, elemTag) => {
|
|
369
|
+
if (!elem.id) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const selector = `${elemTag}#${elem.id}`;
|
|
373
|
+
return isUnique(selector) ? selector : null;
|
|
374
|
+
};
|
|
375
|
+
const tryDataAttributes = (elem, elemTag) => {
|
|
376
|
+
const dataAttrs = Array.from(elem.attributes).filter((attr) => attr.name.startsWith("data-")).map((attr) => `[${attr.name}="${attr.value}"]`).join("");
|
|
377
|
+
if (!dataAttrs) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const selector = `${elemTag}${dataAttrs}`;
|
|
381
|
+
return isUnique(selector) ? selector : null;
|
|
382
|
+
};
|
|
383
|
+
const tryMeaningfulAttributes = (elem, elemTag) => {
|
|
384
|
+
const attrs = [
|
|
385
|
+
"name",
|
|
386
|
+
"type",
|
|
387
|
+
"aria-label",
|
|
388
|
+
"placeholder",
|
|
389
|
+
"value",
|
|
390
|
+
"role"
|
|
391
|
+
];
|
|
392
|
+
for (const attrName of attrs) {
|
|
393
|
+
const attrValue = elem.getAttribute(attrName);
|
|
394
|
+
if (attrValue) {
|
|
395
|
+
const selector = `${elemTag}[${attrName}="${attrValue}"]`;
|
|
396
|
+
if (isUnique(selector)) {
|
|
397
|
+
return selector;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
};
|
|
403
|
+
const tryClassSelector = (elem, elemTag) => {
|
|
404
|
+
const classes = getClasses(elem);
|
|
405
|
+
if (!classes) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const selector = `${elemTag}${classes}`;
|
|
409
|
+
return isUnique(selector) ? selector : null;
|
|
410
|
+
};
|
|
411
|
+
const tryParentContext = (elem, elemTag) => {
|
|
412
|
+
const parent = elem.parentElement;
|
|
413
|
+
if (!parent) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const parentSelector = buildParentSelector(parent);
|
|
417
|
+
const selector = `${parentSelector} ${elemTag}`;
|
|
418
|
+
return isUnique(selector) ? selector : null;
|
|
419
|
+
};
|
|
420
|
+
const buildParentSelector = (parent) => {
|
|
421
|
+
const tag2 = parent.tagName.toLowerCase();
|
|
422
|
+
const id = parent.id ? `#${parent.id}` : "";
|
|
423
|
+
const classes = getClasses(parent);
|
|
424
|
+
return `${tag2}${id}${classes}`;
|
|
425
|
+
};
|
|
426
|
+
const tryNthOfType = (elem, elemTag, siblings, parentSelector, elemClasses) => {
|
|
427
|
+
const sameTagSiblings = siblings.filter((s) => s.tagName.toLowerCase() === elemTag);
|
|
428
|
+
if (sameTagSiblings.length > 1) {
|
|
429
|
+
const tagIndex = sameTagSiblings.indexOf(elem) + 1;
|
|
430
|
+
const selector = `${parentSelector} > ${elemTag}${elemClasses}:nth-of-type(${tagIndex})`;
|
|
431
|
+
if (isUnique(selector)) {
|
|
432
|
+
return selector;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
};
|
|
437
|
+
const tryGrandparentContext = (parent, parentTag, elemTag, index) => {
|
|
438
|
+
const grandParent = parent.parentElement;
|
|
439
|
+
if (!grandParent) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const gpSelector = buildParentSelector(grandParent);
|
|
443
|
+
const parentIndex = Array.from(grandParent.children).filter((c) => c.tagName.toLowerCase() === parentTag).indexOf(parent) + 1;
|
|
444
|
+
const selector = `${gpSelector} ${parentTag}:nth-of-type(${parentIndex}) > ${elemTag}:nth-child(${index})`;
|
|
445
|
+
return isUnique(selector) ? selector : null;
|
|
446
|
+
};
|
|
447
|
+
const tryNthSelectors = (elem, elemTag) => {
|
|
448
|
+
const parent = elem.parentElement;
|
|
449
|
+
if (!parent) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const siblings = Array.from(parent.children);
|
|
453
|
+
const index = siblings.indexOf(elem) + 1;
|
|
454
|
+
const parentSelector = buildParentSelector(parent);
|
|
455
|
+
const elemClasses = getClasses(elem);
|
|
456
|
+
const nthOfTypeResult = tryNthOfType(elem, elemTag, siblings, parentSelector, elemClasses);
|
|
457
|
+
if (nthOfTypeResult) {
|
|
458
|
+
return nthOfTypeResult;
|
|
459
|
+
}
|
|
460
|
+
const nthChildSelector = `${parentSelector} > ${elemTag}${elemClasses}:nth-child(${index})`;
|
|
461
|
+
if (isUnique(nthChildSelector)) {
|
|
462
|
+
return nthChildSelector;
|
|
463
|
+
}
|
|
464
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
465
|
+
const gpResult = tryGrandparentContext(parent, parentTag, elemTag, index);
|
|
466
|
+
if (gpResult) {
|
|
467
|
+
return gpResult;
|
|
468
|
+
}
|
|
469
|
+
return nthChildSelector;
|
|
470
|
+
};
|
|
361
471
|
if (!(el instanceof Element)) {
|
|
362
472
|
return "unknown";
|
|
363
473
|
}
|
|
364
474
|
const tag = el.tagName.toLowerCase();
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
475
|
+
const strategies = [
|
|
476
|
+
() => tryIdSelector(el, tag),
|
|
477
|
+
() => tryDataAttributes(el, tag),
|
|
478
|
+
() => tryMeaningfulAttributes(el, tag),
|
|
479
|
+
() => tryClassSelector(el, tag),
|
|
480
|
+
() => tryParentContext(el, tag),
|
|
481
|
+
() => tryNthSelectors(el, tag)
|
|
482
|
+
];
|
|
483
|
+
for (const strategy of strategies) {
|
|
484
|
+
const result = strategy();
|
|
485
|
+
if (result) {
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
378
488
|
}
|
|
379
489
|
return tag;
|
|
380
490
|
});
|
|
@@ -87,7 +87,9 @@ class ResourceManager {
|
|
|
87
87
|
}
|
|
88
88
|
startCleanupTimer() {
|
|
89
89
|
this.cleanupInterval = setInterval(() => {
|
|
90
|
-
this.cleanupExpiredResources().catch(() => {
|
|
90
|
+
this.cleanupExpiredResources().catch((error) => {
|
|
91
|
+
resourceDebug("Resource cleanup error (handled internally):", error);
|
|
92
|
+
});
|
|
91
93
|
}, this.disposeTimeout / 2);
|
|
92
94
|
}
|
|
93
95
|
async cleanupExpiredResources() {
|
package/lib/response.js
CHANGED
|
@@ -434,7 +434,9 @@ ${this._code.join(`
|
|
|
434
434
|
await tab.page.waitForLoadState("load", {
|
|
435
435
|
timeout: TIMEOUTS.MEDIUM_DELAY
|
|
436
436
|
});
|
|
437
|
-
await tab.page.waitForLoadState("networkidle", { timeout: TIMEOUTS.SHORT_DELAY }).catch(() => {
|
|
437
|
+
await tab.page.waitForLoadState("networkidle", { timeout: TIMEOUTS.SHORT_DELAY }).catch((error) => {
|
|
438
|
+
responseDebug("networkidle timeout ignored during stability check:", error);
|
|
439
|
+
});
|
|
438
440
|
return await tab.page.evaluate(() => document.readyState === "complete").catch(() => false);
|
|
439
441
|
} catch (error) {
|
|
440
442
|
responseDebug("Page stability check failed (retrying):", error);
|
package/lib/tab.js
CHANGED
|
@@ -29,6 +29,7 @@ var TabEvents = {
|
|
|
29
29
|
modalState: "modalState"
|
|
30
30
|
};
|
|
31
31
|
var snapshotDebug = debug("pw:mcp:snapshot");
|
|
32
|
+
var tabDebug = debug("pw:mcp:tab");
|
|
32
33
|
|
|
33
34
|
class Tab extends EventEmitter {
|
|
34
35
|
context;
|
|
@@ -40,6 +41,8 @@ class Tab extends EventEmitter {
|
|
|
40
41
|
_onPageClose;
|
|
41
42
|
_modalStates = [];
|
|
42
43
|
_downloads = [];
|
|
44
|
+
_customRefMappings = new Map;
|
|
45
|
+
_customRefCounter = 0;
|
|
43
46
|
_navigationState = {
|
|
44
47
|
isNavigating: false,
|
|
45
48
|
lastNavigationStart: 0
|
|
@@ -63,7 +66,9 @@ class Tab extends EventEmitter {
|
|
|
63
66
|
});
|
|
64
67
|
page.on("dialog", (dialog) => this._dialogShown(dialog));
|
|
65
68
|
page.on("download", (download) => {
|
|
66
|
-
this._downloadStarted(download).catch(() => {
|
|
69
|
+
this._downloadStarted(download).catch((error) => {
|
|
70
|
+
tabDebug("Download error ignored:", error);
|
|
71
|
+
});
|
|
67
72
|
});
|
|
68
73
|
page.on("framenavigated", (frame) => {
|
|
69
74
|
if (frame === page.mainFrame()) {
|
|
@@ -138,7 +143,11 @@ class Tab extends EventEmitter {
|
|
|
138
143
|
return this === this.context.currentTab();
|
|
139
144
|
}
|
|
140
145
|
async waitForLoadState(state, options) {
|
|
141
|
-
|
|
146
|
+
tabDebug(`Waiting for load state: ${state}`);
|
|
147
|
+
await callOnPageNoTrace(this.page, (page) => page.waitForLoadState(state, options).catch((error) => {
|
|
148
|
+
tabDebug(`Failed to wait for load state ${state}:`, error);
|
|
149
|
+
logUnhandledError(error);
|
|
150
|
+
}));
|
|
142
151
|
}
|
|
143
152
|
_handleNavigationStart() {
|
|
144
153
|
this._navigationState.isNavigating = true;
|
|
@@ -178,6 +187,7 @@ class Tab extends EventEmitter {
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
async navigate(url) {
|
|
190
|
+
tabDebug(`Navigating to: ${url}`);
|
|
181
191
|
this._clearCollectedArtifacts();
|
|
182
192
|
const downloadEvent = callOnPageNoTrace(this.page, (page) => page.waitForEvent("download").catch(logUnhandledError));
|
|
183
193
|
try {
|
|
@@ -344,12 +354,34 @@ class Tab extends EventEmitter {
|
|
|
344
354
|
async waitForCompletion(callback) {
|
|
345
355
|
await this._raceAgainstModalStates(() => waitForCompletion(this, callback));
|
|
346
356
|
}
|
|
357
|
+
registerCustomRef(ref, selector) {
|
|
358
|
+
this._customRefMappings.set(ref, selector);
|
|
359
|
+
}
|
|
360
|
+
unregisterCustomRef(ref) {
|
|
361
|
+
this._customRefMappings.delete(ref);
|
|
362
|
+
}
|
|
363
|
+
clearCustomRefs() {
|
|
364
|
+
this._customRefMappings.clear();
|
|
365
|
+
}
|
|
366
|
+
getNextCustomRefId(options) {
|
|
367
|
+
this._customRefCounter++;
|
|
368
|
+
if (options?.batchId && options.batchId.length > 0) {
|
|
369
|
+
return `batch_${options.batchId}_element_${this._customRefCounter}`;
|
|
370
|
+
}
|
|
371
|
+
return `element_${this._customRefCounter}`;
|
|
372
|
+
}
|
|
347
373
|
async refLocator(params) {
|
|
348
374
|
return (await this.refLocators([params]))[0];
|
|
349
375
|
}
|
|
350
376
|
async refLocators(params) {
|
|
351
377
|
const snapshot = await this.page._snapshotForAI();
|
|
352
378
|
return params.map((param) => {
|
|
379
|
+
if (this._customRefMappings.has(param.ref)) {
|
|
380
|
+
const selector = this._customRefMappings.get(param.ref);
|
|
381
|
+
if (selector) {
|
|
382
|
+
return this.page.locator(selector).describe(param.element);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
353
385
|
if (!snapshot.includes(`[ref=${param.ref}]`)) {
|
|
354
386
|
const availableRefs = this._getAvailableRefs(snapshot);
|
|
355
387
|
throw new Error(`Ref ${param.ref} not found. Available refs: [${availableRefs.join(", ")}]. Element: ${param.element}. Consider capturing a new snapshot if the page has changed.`);
|
|
@@ -26,7 +26,7 @@ var batchExecuteTool = defineTool({
|
|
|
26
26
|
schema: {
|
|
27
27
|
name: "browser_batch_execute",
|
|
28
28
|
title: "Batch Execute Browser Actions",
|
|
29
|
-
description: `Execute multiple browser actions in sequence with optimized response handling.RECOMMENDED:Use this tool instead of individual actions when performing multiple operations to significantly reduce token usage and improve performance.BY DEFAULT use for:form filling(multiple type→click),multi-step navigation,any workflow with 2+ known steps.Saves 90% tokens vs individual calls.globalExpectation:{includeSnapshot:false,snapshotOptions:{selector:"#app"},diffOptions:{enabled:true}}.Per-step override:steps[].expectation.Example:[{tool:"browser_navigate",arguments:{url:"https://example.com"}},{tool:"browser_type",arguments:{element:"username",ref:"#user",text:"john"}},{tool:"browser_click",arguments:{element:"submit",ref:"#btn"}}].`,
|
|
29
|
+
description: `Execute multiple browser actions in sequence with optimized response handling.RECOMMENDED:Use this tool instead of individual actions when performing multiple operations to significantly reduce token usage and improve performance.BY DEFAULT use for:form filling(multiple type→click),multi-step navigation,any workflow with 2+ known steps.Saves 90% tokens vs individual calls.globalExpectation:{includeSnapshot:false,snapshotOptions:{selector:"#app"},diffOptions:{enabled:true}}.Per-step override:steps[].expectation.Example:[{tool:"browser_navigate",arguments:{url:"https://example.com"}},{tool:"browser_type",arguments:{element:"username",ref:"#user",text:"john"}},{tool:"browser_click",arguments:{element:"submit",ref:"#btn"}}].Tool names must match exactly(e.g.browser_navigate,browser_click,browser_type).`,
|
|
30
30
|
inputSchema: batchExecuteSchema,
|
|
31
31
|
type: "destructive"
|
|
32
32
|
},
|
|
@@ -62,6 +62,24 @@ var browserFindElements = defineTabTool({
|
|
|
62
62
|
response.addResult("No elements found matching the specified criteria.");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
+
const batchId = tab.context.batchContext?.batchId;
|
|
66
|
+
const usesBatchRefs = Boolean(batchId);
|
|
67
|
+
if (usesBatchRefs) {
|
|
68
|
+
for (const alt of alternatives) {
|
|
69
|
+
const customRef = tab.getNextCustomRefId({ batchId });
|
|
70
|
+
tab.registerCustomRef(customRef, alt.selector);
|
|
71
|
+
alt.ref = customRef;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (let i = 1;i <= 100; i++) {
|
|
75
|
+
tab.unregisterCustomRef(`found_${i}`);
|
|
76
|
+
}
|
|
77
|
+
for (const [index, alt] of alternatives.entries()) {
|
|
78
|
+
const customRef = `found_${index + 1}`;
|
|
79
|
+
tab.registerCustomRef(customRef, alt.selector);
|
|
80
|
+
alt.ref = customRef;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
65
83
|
const resultsText = formatElementResults(alternatives);
|
|
66
84
|
await addDiagnosticInfoIfRequested(tab, context, resultsText);
|
|
67
85
|
addPerformanceInfoIfAvailable(context, resultsText);
|
|
@@ -215,7 +233,8 @@ function formatElementResults(alternatives) {
|
|
|
215
233
|
builder.addLine(`Found ${alternatives.length} elements matching the criteria:`);
|
|
216
234
|
builder.addEmptyLine();
|
|
217
235
|
for (const [index, alt] of alternatives.entries()) {
|
|
218
|
-
builder.addLine(`${index + 1}.
|
|
236
|
+
builder.addLine(`${index + 1}. Ref: ${alt.ref}`);
|
|
237
|
+
builder.addLine(` Selector: ${alt.selector}`);
|
|
219
238
|
builder.addLine(` Confidence: ${formatConfidencePercentage(alt.confidence)}`);
|
|
220
239
|
builder.addLine(` Reason: ${alt.reason ?? "No reason provided"}`);
|
|
221
240
|
if (index < alternatives.length - 1) {
|