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.
Files changed (72) hide show
  1. package/README.md +62 -5
  2. package/dist/agent/agent-loop.d.ts +8 -2
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +138 -86
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/config.d.ts +5 -0
  7. package/dist/agent/config.d.ts.map +1 -1
  8. package/dist/agent/config.js +5 -0
  9. package/dist/agent/config.js.map +1 -1
  10. package/dist/agent/content-budget.d.ts +11 -0
  11. package/dist/agent/content-budget.d.ts.map +1 -0
  12. package/dist/agent/content-budget.js +129 -0
  13. package/dist/agent/content-budget.js.map +1 -0
  14. package/dist/agent/conversation-manager.d.ts +48 -0
  15. package/dist/agent/conversation-manager.d.ts.map +1 -0
  16. package/dist/agent/conversation-manager.js +157 -0
  17. package/dist/agent/conversation-manager.js.map +1 -0
  18. package/dist/agent/error-recovery.d.ts +29 -0
  19. package/dist/agent/error-recovery.d.ts.map +1 -0
  20. package/dist/agent/error-recovery.js +72 -0
  21. package/dist/agent/error-recovery.js.map +1 -0
  22. package/dist/agent/page-state-cache.d.ts +22 -0
  23. package/dist/agent/page-state-cache.d.ts.map +1 -0
  24. package/dist/agent/page-state-cache.js +71 -0
  25. package/dist/agent/page-state-cache.js.map +1 -0
  26. package/dist/agent/progress-estimator.d.ts +17 -0
  27. package/dist/agent/progress-estimator.d.ts.map +1 -0
  28. package/dist/agent/progress-estimator.js +67 -0
  29. package/dist/agent/progress-estimator.js.map +1 -0
  30. package/dist/agent/prompt.d.ts +1 -1
  31. package/dist/agent/prompt.d.ts.map +1 -1
  32. package/dist/agent/prompt.js +83 -48
  33. package/dist/agent/prompt.js.map +1 -1
  34. package/dist/agent/task-agent.d.ts +89 -0
  35. package/dist/agent/task-agent.d.ts.map +1 -0
  36. package/dist/agent/task-agent.js +448 -0
  37. package/dist/agent/task-agent.js.map +1 -0
  38. package/dist/agent/token-tracker.d.ts +22 -0
  39. package/dist/agent/token-tracker.d.ts.map +1 -0
  40. package/dist/agent/token-tracker.js +29 -0
  41. package/dist/agent/token-tracker.js.map +1 -0
  42. package/dist/agent/tool-usage-tracker.d.ts +45 -0
  43. package/dist/agent/tool-usage-tracker.d.ts.map +1 -0
  44. package/dist/agent/tool-usage-tracker.js +145 -0
  45. package/dist/agent/tool-usage-tracker.js.map +1 -0
  46. package/dist/agent/types.d.ts +24 -0
  47. package/dist/agent/types.d.ts.map +1 -1
  48. package/dist/api/routes.d.ts.map +1 -1
  49. package/dist/api/routes.js +305 -1
  50. package/dist/api/routes.js.map +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp/ai-markdown.d.ts +2 -0
  56. package/dist/mcp/ai-markdown.d.ts.map +1 -0
  57. package/dist/mcp/ai-markdown.js +1739 -0
  58. package/dist/mcp/ai-markdown.js.map +1 -0
  59. package/dist/mcp/browser-mcp-server.d.ts.map +1 -1
  60. package/dist/mcp/browser-mcp-server.js +279 -49
  61. package/dist/mcp/browser-mcp-server.js.map +1 -1
  62. package/dist/mcp/task-tools.d.ts.map +1 -1
  63. package/dist/mcp/task-tools.js +107 -13
  64. package/dist/mcp/task-tools.js.map +1 -1
  65. package/dist/task/tool-actions.d.ts +4 -0
  66. package/dist/task/tool-actions.d.ts.map +1 -1
  67. package/dist/task/tool-actions.js +72 -0
  68. package/dist/task/tool-actions.js.map +1 -1
  69. package/package.json +5 -2
  70. package/public/index.html +593 -41
  71. package/public/task-result.html +135 -0
  72. 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
- return { content: [{ type: 'text', text: JSON.stringify(data) }] };
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
- const result = await toolActions.getPageInfo(toolCtx, sessionId, tab.id, { maxElements, visibleOnly });
220
- return textResult(result);
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: await Promise.all(tabs.map(async (t) => {
573
- let title = '';
574
- try {
575
- title = await t.page.title();
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({ captured: true, url: pageUrl, title: pageTitle, fullPage: !!fullPage, element: element_id || null }) },
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
- return textResult({ logs, totalCount, truncated });
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
- return textResult({ logs, truncated });
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
- return textResult({ downloads: tab.events.getDownloads() });
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);