ai-browser 0.2.3 → 0.2.5
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 +62 -5
- package/dist/agent/agent-loop.d.ts +8 -2
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +138 -86
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/config.d.ts +5 -0
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/config.js +5 -0
- package/dist/agent/config.js.map +1 -1
- package/dist/agent/content-budget.d.ts +11 -0
- package/dist/agent/content-budget.d.ts.map +1 -0
- package/dist/agent/content-budget.js +129 -0
- package/dist/agent/content-budget.js.map +1 -0
- package/dist/agent/conversation-manager.d.ts +48 -0
- package/dist/agent/conversation-manager.d.ts.map +1 -0
- package/dist/agent/conversation-manager.js +157 -0
- package/dist/agent/conversation-manager.js.map +1 -0
- package/dist/agent/error-recovery.d.ts +29 -0
- package/dist/agent/error-recovery.d.ts.map +1 -0
- package/dist/agent/error-recovery.js +72 -0
- package/dist/agent/error-recovery.js.map +1 -0
- package/dist/agent/page-state-cache.d.ts +22 -0
- package/dist/agent/page-state-cache.d.ts.map +1 -0
- package/dist/agent/page-state-cache.js +71 -0
- package/dist/agent/page-state-cache.js.map +1 -0
- package/dist/agent/progress-estimator.d.ts +17 -0
- package/dist/agent/progress-estimator.d.ts.map +1 -0
- package/dist/agent/progress-estimator.js +67 -0
- package/dist/agent/progress-estimator.js.map +1 -0
- package/dist/agent/prompt.d.ts +1 -1
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +83 -48
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/task-agent.d.ts +89 -0
- package/dist/agent/task-agent.d.ts.map +1 -0
- package/dist/agent/task-agent.js +448 -0
- package/dist/agent/task-agent.js.map +1 -0
- package/dist/agent/token-tracker.d.ts +22 -0
- package/dist/agent/token-tracker.d.ts.map +1 -0
- package/dist/agent/token-tracker.js +29 -0
- package/dist/agent/token-tracker.js.map +1 -0
- package/dist/agent/tool-usage-tracker.d.ts +45 -0
- package/dist/agent/tool-usage-tracker.d.ts.map +1 -0
- package/dist/agent/tool-usage-tracker.js +145 -0
- package/dist/agent/tool-usage-tracker.js.map +1 -0
- package/dist/agent/types.d.ts +24 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +305 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/ai-markdown.d.ts +2 -0
- package/dist/mcp/ai-markdown.d.ts.map +1 -0
- package/dist/mcp/ai-markdown.js +1739 -0
- package/dist/mcp/ai-markdown.js.map +1 -0
- package/dist/mcp/browser-mcp-server.d.ts.map +1 -1
- package/dist/mcp/browser-mcp-server.js +279 -49
- package/dist/mcp/browser-mcp-server.js.map +1 -1
- package/dist/mcp/task-tools.d.ts.map +1 -1
- package/dist/mcp/task-tools.js +107 -13
- package/dist/mcp/task-tools.js.map +1 -1
- package/dist/task/tool-actions.d.ts +4 -0
- package/dist/task/tool-actions.d.ts.map +1 -1
- package/dist/task/tool-actions.js +72 -0
- package/dist/task/tool-actions.js.map +1 -1
- package/package.json +5 -2
- package/public/index.html +593 -41
- package/public/task-result.html +135 -0
- package/public/tasks.html +126 -0
|
@@ -8,6 +8,7 @@ import * as toolActions from '../task/tool-actions.js';
|
|
|
8
8
|
import { RunManager } from '../task/run-manager.js';
|
|
9
9
|
import { ArtifactStore } from '../task/artifact-store.js';
|
|
10
10
|
import { registerTaskTools } from './task-tools.js';
|
|
11
|
+
import { enrichWithAiMarkdown } from './ai-markdown.js';
|
|
11
12
|
export { ErrorCode } from '../task/error-codes.js';
|
|
12
13
|
import { ErrorCode } from '../task/error-codes.js';
|
|
13
14
|
export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
@@ -106,8 +107,9 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
106
107
|
return tab;
|
|
107
108
|
}
|
|
108
109
|
// Helper: wrap result as MCP text content
|
|
109
|
-
function textResult(data) {
|
|
110
|
-
|
|
110
|
+
function textResult(data, toolName) {
|
|
111
|
+
const payload = toolName ? enrichWithAiMarkdown(toolName, data) : data;
|
|
112
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
111
113
|
}
|
|
112
114
|
// Helper: wrap error as MCP error content
|
|
113
115
|
function errorResult(message, errorCode) {
|
|
@@ -139,6 +141,33 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
139
141
|
err.errorCode = ErrorCode.INVALID_PARAMETER;
|
|
140
142
|
return err;
|
|
141
143
|
}
|
|
144
|
+
function summarizeNetworkIssues(logs) {
|
|
145
|
+
const timeoutLogs = logs.filter((l) => String(l?.error || '').toLowerCase().includes('timeout'));
|
|
146
|
+
const failedLogs = logs.filter((l) => Boolean(l?.error));
|
|
147
|
+
const httpErrorLogs = logs.filter((l) => typeof l?.status === 'number' && l.status >= 400);
|
|
148
|
+
const slowLogs = logs.filter((l) => (l?.timing?.duration ?? 0) > 1000);
|
|
149
|
+
const issues = [];
|
|
150
|
+
if (timeoutLogs.length > 0)
|
|
151
|
+
issues.push({ kind: 'timeout', count: timeoutLogs.length, sample: timeoutLogs[0]?.url });
|
|
152
|
+
if (failedLogs.length > 0)
|
|
153
|
+
issues.push({ kind: 'request_failed', count: failedLogs.length, sample: failedLogs[0]?.url });
|
|
154
|
+
if (httpErrorLogs.length > 0)
|
|
155
|
+
issues.push({ kind: 'http_error', count: httpErrorLogs.length, sample: httpErrorLogs[0]?.url });
|
|
156
|
+
if (slowLogs.length > 0)
|
|
157
|
+
issues.push({ kind: 'slow_request', count: slowLogs.length, sample: slowLogs[0]?.url });
|
|
158
|
+
return issues.slice(0, 5);
|
|
159
|
+
}
|
|
160
|
+
function summarizeConsoleIssues(logs) {
|
|
161
|
+
const levels = ['error', 'warn', 'info', 'log', 'debug'];
|
|
162
|
+
const issues = [];
|
|
163
|
+
for (const level of levels) {
|
|
164
|
+
const levelLogs = logs.filter((l) => l?.level === level);
|
|
165
|
+
if (levelLogs.length > 0) {
|
|
166
|
+
issues.push({ kind: level, count: levelLogs.length, sample: levelLogs[0]?.text });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return issues.slice(0, 5);
|
|
170
|
+
}
|
|
142
171
|
// Helper: wrap async handler with try/catch to prevent unhandled exceptions
|
|
143
172
|
function safe(fn) {
|
|
144
173
|
return (async (...args) => {
|
|
@@ -175,7 +204,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
175
204
|
defaultSessionId = session.id;
|
|
176
205
|
}
|
|
177
206
|
options?.onSessionCreated?.(session.id);
|
|
178
|
-
return textResult({ sessionId: session.id });
|
|
207
|
+
return textResult({ sessionId: session.id }, 'create_session');
|
|
179
208
|
}));
|
|
180
209
|
server.tool('close_session', '关闭浏览器会话(headful 会话会保留,不会被自动关闭)', {
|
|
181
210
|
sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
|
|
@@ -183,21 +212,21 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
183
212
|
}, safe(async ({ sessionId: rawSessionId, force }) => {
|
|
184
213
|
// No-op if no sessionId provided and no default session exists
|
|
185
214
|
if (!rawSessionId && !defaultSessionId && !defaultSessionPromise) {
|
|
186
|
-
return textResult({ success: true, reason: 'No active session to close' });
|
|
215
|
+
return textResult({ success: true, reason: 'No active session to close' }, 'close_session');
|
|
187
216
|
}
|
|
188
217
|
const sessionId = await resolveSession(rawSessionId);
|
|
189
218
|
await sessionManager.saveAllCookies(sessionId);
|
|
190
219
|
// 默认保留 headful 会话,force=true 时允许主动关闭。
|
|
191
220
|
const session = sessionManager.get(sessionId);
|
|
192
221
|
if (session && !session.headless && !force) {
|
|
193
|
-
return textResult({ success: true, kept: true, reason: 'headful session preserved (set force=true to close)' });
|
|
222
|
+
return textResult({ success: true, kept: true, reason: 'headful session preserved (set force=true to close)' }, 'close_session');
|
|
194
223
|
}
|
|
195
224
|
const closed = await sessionManager.close(sessionId);
|
|
196
225
|
// Clear default session if it was closed
|
|
197
226
|
if (sessionId === defaultSessionId) {
|
|
198
227
|
defaultSessionId = null;
|
|
199
228
|
}
|
|
200
|
-
return textResult({ success: closed });
|
|
229
|
+
return textResult({ success: closed }, 'close_session');
|
|
201
230
|
}));
|
|
202
231
|
// ===== navigate =====
|
|
203
232
|
server.tool('navigate', '导航到指定URL', {
|
|
@@ -206,7 +235,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
206
235
|
}, safe(async ({ sessionId: rawSessionId, url }) => {
|
|
207
236
|
const sessionId = await resolveSession(rawSessionId);
|
|
208
237
|
const result = await toolActions.navigate(toolCtx, sessionId, url);
|
|
209
|
-
return textResult(result);
|
|
238
|
+
return textResult(result, 'navigate');
|
|
210
239
|
}));
|
|
211
240
|
// ===== get_page_info =====
|
|
212
241
|
server.tool('get_page_info', '获取当前页面的语义信息,包括可交互元素列表', {
|
|
@@ -216,8 +245,56 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
216
245
|
}, safe(async ({ sessionId: rawSessionId, maxElements, visibleOnly }) => {
|
|
217
246
|
const sessionId = await resolveSession(rawSessionId);
|
|
218
247
|
const tab = getActiveTab(sessionId);
|
|
219
|
-
|
|
220
|
-
|
|
248
|
+
// Fetch with a generous limit first to measure page complexity
|
|
249
|
+
const fetchLimit = maxElements ?? 200;
|
|
250
|
+
const result = await toolActions.getPageInfo(toolCtx, sessionId, tab.id, { maxElements: fetchLimit, visibleOnly });
|
|
251
|
+
const totalElements = Array.isArray(result.elements) ? result.elements.length : 0;
|
|
252
|
+
// Adaptive limit: if caller specified maxElements, respect it; otherwise auto-adjust
|
|
253
|
+
let effectiveLimit;
|
|
254
|
+
if (maxElements !== undefined) {
|
|
255
|
+
effectiveLimit = maxElements;
|
|
256
|
+
}
|
|
257
|
+
else if (totalElements <= 30) {
|
|
258
|
+
// Small page — return all
|
|
259
|
+
effectiveLimit = totalElements;
|
|
260
|
+
}
|
|
261
|
+
else if (totalElements <= 100) {
|
|
262
|
+
// Medium page — default 50, prioritize inputs > buttons > links
|
|
263
|
+
effectiveLimit = 50;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Complex page — default 30, but always include intent-recommended elements
|
|
267
|
+
effectiveLimit = 30;
|
|
268
|
+
}
|
|
269
|
+
// Prioritize elements: inputs first, then buttons, then links, then rest
|
|
270
|
+
let elements = Array.isArray(result.elements) ? result.elements : [];
|
|
271
|
+
if (elements.length > effectiveLimit) {
|
|
272
|
+
const intentIds = new Set();
|
|
273
|
+
if (Array.isArray(result.recommendedByIntent)) {
|
|
274
|
+
for (const rec of result.recommendedByIntent) {
|
|
275
|
+
if (Array.isArray(rec?.suggestedElementIds)) {
|
|
276
|
+
for (const id of rec.suggestedElementIds)
|
|
277
|
+
intentIds.add(id);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const intentElements = elements.filter((e) => intentIds.has(e.id));
|
|
282
|
+
const rest = elements.filter((e) => !intentIds.has(e.id));
|
|
283
|
+
const inputs = rest.filter((e) => e.type === 'input' || e.type === 'textarea' || e.type === 'select');
|
|
284
|
+
const buttons = rest.filter((e) => e.type === 'button' || e.type === 'submit');
|
|
285
|
+
const links = rest.filter((e) => e.type === 'link');
|
|
286
|
+
const others = rest.filter((e) => !['input', 'textarea', 'select', 'button', 'submit', 'link'].includes(e.type));
|
|
287
|
+
const prioritized = [...intentElements, ...inputs, ...buttons, ...links, ...others];
|
|
288
|
+
elements = prioritized.slice(0, effectiveLimit);
|
|
289
|
+
}
|
|
290
|
+
const hasMore = totalElements > effectiveLimit;
|
|
291
|
+
const nextCursor = hasMore
|
|
292
|
+
? {
|
|
293
|
+
strategy: 'increase_max_elements',
|
|
294
|
+
suggestedMaxElements: Math.min(Math.max(effectiveLimit * 2, 100), 1000),
|
|
295
|
+
}
|
|
296
|
+
: null;
|
|
297
|
+
return textResult({ ...result, elements, totalElements, hasMore, nextCursor }, 'get_page_info');
|
|
221
298
|
}));
|
|
222
299
|
// ===== get_page_content =====
|
|
223
300
|
server.tool('get_page_content', '提取当前页面的文本内容', {
|
|
@@ -227,7 +304,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
227
304
|
const sessionId = await resolveSession(rawSessionId);
|
|
228
305
|
const tab = getActiveTab(sessionId);
|
|
229
306
|
const result = await toolActions.getPageContent(toolCtx, sessionId, tab.id, { maxLength });
|
|
230
|
-
return textResult(result);
|
|
307
|
+
return textResult(result, 'get_page_content');
|
|
231
308
|
}));
|
|
232
309
|
// ===== click =====
|
|
233
310
|
server.tool('click', '点击页面上的元素', {
|
|
@@ -264,7 +341,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
264
341
|
if (pending)
|
|
265
342
|
result.dialog = pending;
|
|
266
343
|
}
|
|
267
|
-
return textResult(result);
|
|
344
|
+
return textResult(result, 'click');
|
|
268
345
|
}));
|
|
269
346
|
// ===== type_text =====
|
|
270
347
|
server.tool('type_text', '在输入框中输入文本。设置 submit=true 可在输入后自动按回车提交(适用于搜索框)', {
|
|
@@ -287,7 +364,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
287
364
|
return textResult({
|
|
288
365
|
success: true,
|
|
289
366
|
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
290
|
-
});
|
|
367
|
+
}, 'type_text');
|
|
291
368
|
}));
|
|
292
369
|
// ===== scroll =====
|
|
293
370
|
server.tool('scroll', '滚动页面', {
|
|
@@ -301,7 +378,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
301
378
|
const tab = getActiveTab(sessionId);
|
|
302
379
|
await executeAction(tab.page, 'scroll', undefined, direction);
|
|
303
380
|
sessionManager.updateActivity(sessionId);
|
|
304
|
-
return textResult({ success: true });
|
|
381
|
+
return textResult({ success: true }, 'scroll');
|
|
305
382
|
}));
|
|
306
383
|
// ===== press_key =====
|
|
307
384
|
const ALLOWED_KEYS = new Set([
|
|
@@ -373,7 +450,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
373
450
|
return textResult({
|
|
374
451
|
success: true,
|
|
375
452
|
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
376
|
-
});
|
|
453
|
+
}, 'press_key');
|
|
377
454
|
}
|
|
378
455
|
// Single key mode (original logic)
|
|
379
456
|
if (!ALLOWED_KEYS.has(key)) {
|
|
@@ -387,7 +464,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
387
464
|
return textResult({
|
|
388
465
|
success: true,
|
|
389
466
|
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
390
|
-
});
|
|
467
|
+
}, 'press_key');
|
|
391
468
|
}));
|
|
392
469
|
// ===== go_back =====
|
|
393
470
|
server.tool('go_back', '返回上一页', {
|
|
@@ -401,7 +478,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
401
478
|
return textResult({
|
|
402
479
|
success: true,
|
|
403
480
|
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
404
|
-
});
|
|
481
|
+
}, 'go_back');
|
|
405
482
|
}));
|
|
406
483
|
// ===== find_element =====
|
|
407
484
|
server.tool('find_element', '通过自然语言描述模糊匹配页面元素', {
|
|
@@ -422,7 +499,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
422
499
|
score: c.score,
|
|
423
500
|
matchReason: c.matchReason,
|
|
424
501
|
})),
|
|
425
|
-
});
|
|
502
|
+
}, 'find_element');
|
|
426
503
|
}));
|
|
427
504
|
// ===== wait =====
|
|
428
505
|
server.tool('wait', '按条件等待:time / selector / networkidle / element_hidden', {
|
|
@@ -455,7 +532,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
455
532
|
await new Promise(r => setTimeout(r, Math.min(milliseconds || 1000, 30000)));
|
|
456
533
|
}
|
|
457
534
|
sessionManager.updateActivity(sessionId);
|
|
458
|
-
return textResult({ success: true });
|
|
535
|
+
return textResult({ success: true }, 'wait');
|
|
459
536
|
}));
|
|
460
537
|
// ===== execute_javascript =====
|
|
461
538
|
server.tool('execute_javascript', '在当前页面执行 JavaScript(仅 local 模式可用)。脚本需通过表达式或 return 返回数据,console.log 不会作为返回值', {
|
|
@@ -491,14 +568,14 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
491
568
|
}
|
|
492
569
|
sessionManager.updateActivity(sessionId);
|
|
493
570
|
if (result === undefined || result === null) {
|
|
494
|
-
return textResult({ result: null, hint: '脚本无返回值。如需获取数据,请在脚本末尾使用表达式(如 document.title)或 return 语句。console.log 的输出不会返回。' });
|
|
571
|
+
return textResult({ result: null, hint: '脚本无返回值。如需获取数据,请在脚本末尾使用表达式(如 document.title)或 return 语句。console.log 的输出不会返回。' }, 'execute_javascript');
|
|
495
572
|
}
|
|
496
573
|
let serialized = JSON.stringify(result);
|
|
497
574
|
const truncated = serialized && serialized.length > 4000;
|
|
498
575
|
if (truncated) {
|
|
499
576
|
serialized = serialized.slice(0, 4000) + '...(truncated)';
|
|
500
577
|
}
|
|
501
|
-
return textResult({ result: truncated ? serialized : result, truncated });
|
|
578
|
+
return textResult({ result: truncated ? serialized : result, truncated }, 'execute_javascript');
|
|
502
579
|
}));
|
|
503
580
|
// ===== select_option =====
|
|
504
581
|
server.tool('select_option', '选择下拉框中的选项', {
|
|
@@ -510,7 +587,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
510
587
|
const tab = getActiveTab(sessionId);
|
|
511
588
|
await executeAction(tab.page, 'select', element_id, value);
|
|
512
589
|
sessionManager.updateActivity(sessionId);
|
|
513
|
-
return textResult({ success: true });
|
|
590
|
+
return textResult({ success: true }, 'select_option');
|
|
514
591
|
}));
|
|
515
592
|
// ===== hover =====
|
|
516
593
|
server.tool('hover', '将鼠标悬停在页面元素上(触发 tooltip/dropdown 等)', {
|
|
@@ -521,7 +598,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
521
598
|
const tab = getActiveTab(sessionId);
|
|
522
599
|
await executeAction(tab.page, 'hover', element_id);
|
|
523
600
|
sessionManager.updateActivity(sessionId);
|
|
524
|
-
return textResult({ success: true });
|
|
601
|
+
return textResult({ success: true }, 'hover');
|
|
525
602
|
}));
|
|
526
603
|
// ===== close_tab =====
|
|
527
604
|
server.tool('close_tab', '关闭指定标签页', {
|
|
@@ -530,7 +607,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
530
607
|
}, safe(async ({ sessionId: rawSessionId, tabId }) => {
|
|
531
608
|
const sessionId = await resolveSession(rawSessionId);
|
|
532
609
|
const result = await toolActions.closeTab(toolCtx, sessionId, tabId);
|
|
533
|
-
return textResult(result);
|
|
610
|
+
return textResult(result, 'close_tab');
|
|
534
611
|
}));
|
|
535
612
|
// ===== switch_tab =====
|
|
536
613
|
server.tool('switch_tab', '切换到指定标签页', {
|
|
@@ -546,7 +623,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
546
623
|
return textResult({
|
|
547
624
|
success: true,
|
|
548
625
|
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
549
|
-
});
|
|
626
|
+
}, 'switch_tab');
|
|
550
627
|
}));
|
|
551
628
|
// ===== create_tab =====
|
|
552
629
|
server.tool('create_tab', '在当前会话中创建新标签页', {
|
|
@@ -555,7 +632,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
555
632
|
}, safe(async ({ sessionId: rawSessionId, url }) => {
|
|
556
633
|
const sessionId = await resolveSession(rawSessionId);
|
|
557
634
|
const result = await toolActions.createTab(toolCtx, sessionId, url);
|
|
558
|
-
return textResult(result);
|
|
635
|
+
return textResult(result, 'create_tab');
|
|
559
636
|
}));
|
|
560
637
|
// ===== list_tabs =====
|
|
561
638
|
server.tool('list_tabs', '列出当前会话的所有标签页', {
|
|
@@ -567,17 +644,20 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
567
644
|
throw new Error(`Session not found: ${sessionId}`);
|
|
568
645
|
const tabs = sessionManager.listTabs(sessionId);
|
|
569
646
|
sessionManager.updateActivity(sessionId);
|
|
647
|
+
const tabItems = await Promise.all(tabs.map(async (t) => {
|
|
648
|
+
let title = '';
|
|
649
|
+
try {
|
|
650
|
+
title = await t.page.title();
|
|
651
|
+
}
|
|
652
|
+
catch { }
|
|
653
|
+
return { id: t.id, url: t.page.url(), title };
|
|
654
|
+
}));
|
|
570
655
|
return textResult({
|
|
571
656
|
activeTabId: session.activeTabId,
|
|
572
|
-
tabs:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
catch { }
|
|
578
|
-
return { id: t.id, url: t.page.url(), title };
|
|
579
|
-
})),
|
|
580
|
-
});
|
|
657
|
+
tabs: tabItems,
|
|
658
|
+
hasMore: false,
|
|
659
|
+
nextCursor: null,
|
|
660
|
+
}, 'list_tabs');
|
|
581
661
|
}));
|
|
582
662
|
// ===== screenshot =====
|
|
583
663
|
server.tool('screenshot', '截取当前页面的屏幕截图,支持全页截图、元素截图、格式和质量选项', {
|
|
@@ -624,11 +704,18 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
624
704
|
sessionManager.updateActivity(sessionId);
|
|
625
705
|
const pageUrl = tab.page.url();
|
|
626
706
|
const pageTitle = await tab.page.title().catch(() => '');
|
|
707
|
+
const screenshotMeta = enrichWithAiMarkdown('screenshot', {
|
|
708
|
+
captured: true,
|
|
709
|
+
url: pageUrl,
|
|
710
|
+
title: pageTitle,
|
|
711
|
+
fullPage: !!fullPage,
|
|
712
|
+
element: element_id || null,
|
|
713
|
+
});
|
|
627
714
|
return {
|
|
628
715
|
content: [
|
|
629
716
|
// Some MCP clients only render the first content block; keep image first for compatibility.
|
|
630
717
|
{ type: 'image', data: base64, mimeType },
|
|
631
|
-
{ type: 'text', text: JSON.stringify(
|
|
718
|
+
{ type: 'text', text: JSON.stringify(screenshotMeta) },
|
|
632
719
|
],
|
|
633
720
|
};
|
|
634
721
|
}));
|
|
@@ -678,7 +765,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
678
765
|
await setValueByAccessibility(tab.page, element_id, value, html);
|
|
679
766
|
}
|
|
680
767
|
sessionManager.updateActivity(sessionId);
|
|
681
|
-
return textResult({ success: true });
|
|
768
|
+
return textResult({ success: true }, 'set_value');
|
|
682
769
|
}));
|
|
683
770
|
// ===== handle_dialog =====
|
|
684
771
|
server.tool('handle_dialog', '处理页面弹窗(alert/confirm/prompt),接受或拒绝', {
|
|
@@ -692,15 +779,15 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
692
779
|
const sessionId = await resolveSession(rawSessionId);
|
|
693
780
|
const tab = getActiveTab(sessionId);
|
|
694
781
|
if (!tab.events) {
|
|
695
|
-
return textResult({ success: false, reason: 'Event tracking not available' });
|
|
782
|
+
return textResult({ success: false, reason: 'Event tracking not available' }, 'handle_dialog');
|
|
696
783
|
}
|
|
697
784
|
const pending = tab.events.getPendingDialog();
|
|
698
785
|
if (!pending) {
|
|
699
|
-
return textResult({ success: false, reason: 'No pending dialog' });
|
|
786
|
+
return textResult({ success: false, reason: 'No pending dialog' }, 'handle_dialog');
|
|
700
787
|
}
|
|
701
788
|
await tab.events.handleDialog(action, text);
|
|
702
789
|
sessionManager.updateActivity(sessionId);
|
|
703
|
-
return textResult({ success: true, dialog: pending });
|
|
790
|
+
return textResult({ success: true, dialog: pending }, 'handle_dialog');
|
|
704
791
|
}));
|
|
705
792
|
// ===== get_dialog_info =====
|
|
706
793
|
server.tool('get_dialog_info', '获取页面弹窗信息和历史记录', {
|
|
@@ -709,13 +796,15 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
709
796
|
const sessionId = await resolveSession(rawSessionId);
|
|
710
797
|
const tab = getActiveTab(sessionId);
|
|
711
798
|
if (!tab.events) {
|
|
712
|
-
return textResult({ pendingDialog: null, dialogHistory: [] });
|
|
799
|
+
return textResult({ pendingDialog: null, dialogHistory: [], hasMore: false, nextCursor: null }, 'get_dialog_info');
|
|
713
800
|
}
|
|
714
801
|
sessionManager.updateActivity(sessionId);
|
|
715
802
|
return textResult({
|
|
716
803
|
pendingDialog: tab.events.getPendingDialog(),
|
|
717
804
|
dialogHistory: tab.events.getDialogs(),
|
|
718
|
-
|
|
805
|
+
hasMore: false,
|
|
806
|
+
nextCursor: null,
|
|
807
|
+
}, 'get_dialog_info');
|
|
719
808
|
}));
|
|
720
809
|
// ===== wait_for_stable =====
|
|
721
810
|
server.tool('wait_for_stable', '等待页面 DOM 稳定(无新增/删除节点且无待处理网络请求)', {
|
|
@@ -726,7 +815,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
726
815
|
const sessionId = await resolveSession(rawSessionId);
|
|
727
816
|
const tab = getActiveTab(sessionId);
|
|
728
817
|
const result = await toolActions.waitForStable(toolCtx, sessionId, tab.id, { timeout, quietMs });
|
|
729
|
-
return textResult(result);
|
|
818
|
+
return textResult(result, 'wait_for_stable');
|
|
730
819
|
}));
|
|
731
820
|
// ===== get_network_logs =====
|
|
732
821
|
server.tool('get_network_logs', '获取页面网络请求日志', {
|
|
@@ -742,7 +831,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
742
831
|
const sessionId = await resolveSession(rawSessionId);
|
|
743
832
|
const tab = getActiveTab(sessionId);
|
|
744
833
|
if (!tab.events) {
|
|
745
|
-
return textResult({ logs: [], totalCount: 0, truncated: false });
|
|
834
|
+
return textResult({ logs: [], totalCount: 0, truncated: false, topIssues: [], hasMore: false, nextCursor: null }, 'get_network_logs');
|
|
746
835
|
}
|
|
747
836
|
let logs = tab.events.getNetworkLogs();
|
|
748
837
|
const totalCount = logs.length;
|
|
@@ -771,7 +860,12 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
771
860
|
logs = logs.slice(-limit);
|
|
772
861
|
}
|
|
773
862
|
sessionManager.updateActivity(sessionId);
|
|
774
|
-
|
|
863
|
+
const hasMore = truncated;
|
|
864
|
+
const nextCursor = hasMore
|
|
865
|
+
? { strategy: 'increase_max_entries', suggestedMaxEntries: Math.min((limit || 50) * 2, 1000) }
|
|
866
|
+
: null;
|
|
867
|
+
const topIssues = summarizeNetworkIssues(logs);
|
|
868
|
+
return textResult({ logs, totalCount, truncated, topIssues, hasMore, nextCursor }, 'get_network_logs');
|
|
775
869
|
}));
|
|
776
870
|
// ===== get_console_logs =====
|
|
777
871
|
server.tool('get_console_logs', '获取页面控制台日志', {
|
|
@@ -785,7 +879,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
785
879
|
const sessionId = await resolveSession(rawSessionId);
|
|
786
880
|
const tab = getActiveTab(sessionId);
|
|
787
881
|
if (!tab.events) {
|
|
788
|
-
return textResult({ logs: [], truncated: false });
|
|
882
|
+
return textResult({ logs: [], truncated: false, topIssues: [], hasMore: false, nextCursor: null }, 'get_console_logs');
|
|
789
883
|
}
|
|
790
884
|
let logs = tab.events.getConsoleLogs();
|
|
791
885
|
// Filter by level (default: error + warn)
|
|
@@ -802,7 +896,12 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
802
896
|
logs = logs.slice(-limit);
|
|
803
897
|
}
|
|
804
898
|
sessionManager.updateActivity(sessionId);
|
|
805
|
-
|
|
899
|
+
const hasMore = truncated;
|
|
900
|
+
const nextCursor = hasMore
|
|
901
|
+
? { strategy: 'increase_max_entries', suggestedMaxEntries: Math.min((limit || 50) * 2, 1000) }
|
|
902
|
+
: null;
|
|
903
|
+
const topIssues = summarizeConsoleIssues(logs);
|
|
904
|
+
return textResult({ logs, truncated, topIssues, hasMore, nextCursor }, 'get_console_logs');
|
|
806
905
|
}));
|
|
807
906
|
// ===== upload_file =====
|
|
808
907
|
server.tool('upload_file', '上传文件到 file input 元素(仅 local 模式可用)', {
|
|
@@ -841,7 +940,7 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
841
940
|
}
|
|
842
941
|
await elementHandle.uploadFile(resolvedPath);
|
|
843
942
|
sessionManager.updateActivity(sessionId);
|
|
844
|
-
return textResult({ success: true, filePath: resolvedPath });
|
|
943
|
+
return textResult({ success: true, filePath: resolvedPath }, 'upload_file');
|
|
845
944
|
}));
|
|
846
945
|
// ===== get_downloads =====
|
|
847
946
|
server.tool('get_downloads', '获取当前页面的下载文件列表', {
|
|
@@ -850,10 +949,141 @@ export function createBrowserMcpServer(sessionManager, cookieStore, options) {
|
|
|
850
949
|
const sessionId = await resolveSession(rawSessionId);
|
|
851
950
|
const tab = getActiveTab(sessionId);
|
|
852
951
|
if (!tab.events) {
|
|
853
|
-
return textResult({ downloads: [] });
|
|
952
|
+
return textResult({ downloads: [], hasMore: false, nextCursor: null }, 'get_downloads');
|
|
953
|
+
}
|
|
954
|
+
sessionManager.updateActivity(sessionId);
|
|
955
|
+
return textResult({ downloads: tab.events.getDownloads(), hasMore: false, nextCursor: null }, 'get_downloads');
|
|
956
|
+
}));
|
|
957
|
+
// ===== Composite Tools =====
|
|
958
|
+
// fill_form: fill multiple form fields and optionally submit
|
|
959
|
+
server.tool('fill_form', '一次填写多个表单字段并可选提交。减少多次 type_text 调用', {
|
|
960
|
+
sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
|
|
961
|
+
fields: z.array(z.object({
|
|
962
|
+
element_id: z.string().describe('输入框的语义ID'),
|
|
963
|
+
value: z.string().describe('要输入的值'),
|
|
964
|
+
})).describe('要填写的字段列表'),
|
|
965
|
+
submit: z.object({
|
|
966
|
+
element_id: z.string().optional().describe('提交按钮的语义ID'),
|
|
967
|
+
pressEnter: z.boolean().optional().describe('是否按回车提交'),
|
|
968
|
+
}).optional().describe('提交方式'),
|
|
969
|
+
}, safe(async ({ sessionId: rawSessionId, fields, submit }) => {
|
|
970
|
+
const sessionId = await resolveSession(rawSessionId);
|
|
971
|
+
const tab = getActiveTab(sessionId);
|
|
972
|
+
const results = [];
|
|
973
|
+
for (const field of fields) {
|
|
974
|
+
try {
|
|
975
|
+
await executeAction(tab.page, 'type', field.element_id, field.value);
|
|
976
|
+
results.push({ element_id: field.element_id, success: true });
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
results.push({ element_id: field.element_id, success: false, error: err.message });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
let submitResult;
|
|
983
|
+
if (submit) {
|
|
984
|
+
try {
|
|
985
|
+
if (submit.element_id) {
|
|
986
|
+
await executeAction(tab.page, 'click', submit.element_id);
|
|
987
|
+
await new Promise(r => setTimeout(r, 200));
|
|
988
|
+
}
|
|
989
|
+
else if (submit.pressEnter) {
|
|
990
|
+
await Promise.all([
|
|
991
|
+
tab.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 }).catch(() => { }),
|
|
992
|
+
tab.page.keyboard.press('Enter'),
|
|
993
|
+
]);
|
|
994
|
+
}
|
|
995
|
+
submitResult = { success: true };
|
|
996
|
+
}
|
|
997
|
+
catch (err) {
|
|
998
|
+
submitResult = { success: false, error: err.message };
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
await saveCookiesFromPage(tab.page);
|
|
1002
|
+
sessionManager.updateActivity(sessionId);
|
|
1003
|
+
return textResult({
|
|
1004
|
+
fieldResults: results,
|
|
1005
|
+
submitResult,
|
|
1006
|
+
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
1007
|
+
}, 'fill_form');
|
|
1008
|
+
}));
|
|
1009
|
+
// click_and_wait: click then auto-wait for stability/navigation
|
|
1010
|
+
server.tool('click_and_wait', '点击元素后自动等待页面稳定或导航完成', {
|
|
1011
|
+
sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
|
|
1012
|
+
element_id: z.string().describe('要点击的元素的语义ID'),
|
|
1013
|
+
waitFor: z.string().optional().describe('等待条件: stable(默认) / navigation / selector'),
|
|
1014
|
+
selector: z.string().optional().describe('当 waitFor=selector 时,等待此CSS选择器出现'),
|
|
1015
|
+
}, safe(async ({ sessionId: rawSessionId, element_id, waitFor, selector }) => {
|
|
1016
|
+
const sessionId = await resolveSession(rawSessionId);
|
|
1017
|
+
const tab = getActiveTab(sessionId);
|
|
1018
|
+
const waitType = waitFor || 'stable';
|
|
1019
|
+
// Click
|
|
1020
|
+
await executeAction(tab.page, 'click', element_id);
|
|
1021
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1022
|
+
// Wait
|
|
1023
|
+
let waitResult = { stable: true, method: waitType };
|
|
1024
|
+
try {
|
|
1025
|
+
if (waitType === 'navigation') {
|
|
1026
|
+
await tab.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
1027
|
+
}
|
|
1028
|
+
else if (waitType === 'selector' && selector) {
|
|
1029
|
+
await tab.page.waitForSelector(selector, { timeout: 10000 });
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
// stable: wait for network idle + DOM quiet
|
|
1033
|
+
await tab.page.waitForNetworkIdle({ timeout: 5000 }).catch(() => { });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
waitResult.stable = false;
|
|
1038
|
+
}
|
|
1039
|
+
tab.url = tab.page.url();
|
|
1040
|
+
await saveCookiesFromPage(tab.page);
|
|
1041
|
+
// Check for popups
|
|
1042
|
+
let newTabCreated;
|
|
1043
|
+
if (tab.events) {
|
|
1044
|
+
const popups = tab.events.getPopupPages();
|
|
1045
|
+
for (const popupPage of popups) {
|
|
1046
|
+
const newTab = await sessionManager.registerPopupAsTab(sessionId, popupPage);
|
|
1047
|
+
if (newTab)
|
|
1048
|
+
newTabCreated = newTab.id;
|
|
1049
|
+
}
|
|
1050
|
+
tab.events.clearPopupPages();
|
|
854
1051
|
}
|
|
855
1052
|
sessionManager.updateActivity(sessionId);
|
|
856
|
-
|
|
1053
|
+
const result = {
|
|
1054
|
+
success: true,
|
|
1055
|
+
waitResult,
|
|
1056
|
+
page: { url: tab.page.url(), title: await tab.page.title() },
|
|
1057
|
+
};
|
|
1058
|
+
if (newTabCreated)
|
|
1059
|
+
result.newTabCreated = newTabCreated;
|
|
1060
|
+
if (tab.events) {
|
|
1061
|
+
const pending = tab.events.getPendingDialog();
|
|
1062
|
+
if (pending)
|
|
1063
|
+
result.dialog = pending;
|
|
1064
|
+
}
|
|
1065
|
+
return textResult(result, 'click_and_wait');
|
|
1066
|
+
}));
|
|
1067
|
+
// navigate_and_extract: navigate then immediately extract content
|
|
1068
|
+
server.tool('navigate_and_extract', '导航到URL后立即提取内容,减少两次独立调用', {
|
|
1069
|
+
sessionId: z.string().optional().describe('会话ID,不传则使用默认会话'),
|
|
1070
|
+
url: z.string().describe('要导航到的URL'),
|
|
1071
|
+
extract: z.string().optional().describe('提取类型: content(默认) / elements / both'),
|
|
1072
|
+
}, safe(async ({ sessionId: rawSessionId, url, extract }) => {
|
|
1073
|
+
const sessionId = await resolveSession(rawSessionId);
|
|
1074
|
+
const extractType = extract || 'content';
|
|
1075
|
+
// Navigate
|
|
1076
|
+
const navResult = await toolActions.navigate(toolCtx, sessionId, url);
|
|
1077
|
+
const tab = getActiveTab(sessionId);
|
|
1078
|
+
const result = { navigation: navResult };
|
|
1079
|
+
// Extract based on type
|
|
1080
|
+
if (extractType === 'content' || extractType === 'both') {
|
|
1081
|
+
result.content = await toolActions.getPageContent(toolCtx, sessionId, tab.id, {});
|
|
1082
|
+
}
|
|
1083
|
+
if (extractType === 'elements' || extractType === 'both') {
|
|
1084
|
+
result.elements = await toolActions.getPageInfo(toolCtx, sessionId, tab.id, {});
|
|
1085
|
+
}
|
|
1086
|
+
return textResult(result, 'navigate_and_extract');
|
|
857
1087
|
}));
|
|
858
1088
|
// ===== Task Template Tools (delegated to task-tools.ts) =====
|
|
859
1089
|
registerTaskTools(server, toolCtx, runManager, artifactStore, isRemote, safe, resolveSession, createIsolatedSession);
|