web-fetcher-mcp 1.0.9 → 1.0.13
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 +312 -98
- package/dist/index.js +550 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/click-element.d.ts +1 -1
- package/dist/tools/click-element.d.ts.map +1 -1
- package/dist/tools/click-element.js +219 -4
- package/dist/tools/click-element.js.map +1 -1
- package/dist/tools/execute-js.d.ts +16 -0
- package/dist/tools/execute-js.d.ts.map +1 -0
- package/dist/tools/execute-js.js +79 -0
- package/dist/tools/execute-js.js.map +1 -0
- package/dist/tools/get-console-logs.d.ts +6 -0
- package/dist/tools/get-console-logs.d.ts.map +1 -0
- package/dist/tools/get-console-logs.js +85 -0
- package/dist/tools/get-console-logs.js.map +1 -0
- package/dist/tools/get-element-attributes.d.ts +10 -0
- package/dist/tools/get-element-attributes.d.ts.map +1 -0
- package/dist/tools/get-element-attributes.js +90 -0
- package/dist/tools/get-element-attributes.js.map +1 -0
- package/dist/tools/get-page-info.d.ts +14 -0
- package/dist/tools/get-page-info.d.ts.map +1 -0
- package/dist/tools/get-page-info.js +163 -0
- package/dist/tools/get-page-info.js.map +1 -0
- package/dist/tools/hover-element.d.ts +9 -0
- package/dist/tools/hover-element.d.ts.map +1 -0
- package/dist/tools/hover-element.js +90 -0
- package/dist/tools/hover-element.js.map +1 -0
- package/dist/tools/navigate-history.d.ts +8 -0
- package/dist/tools/navigate-history.d.ts.map +1 -0
- package/dist/tools/navigate-history.js +45 -0
- package/dist/tools/navigate-history.js.map +1 -0
- package/dist/tools/open-devtools.d.ts +18 -0
- package/dist/tools/open-devtools.d.ts.map +1 -0
- package/dist/tools/open-devtools.js +64 -0
- package/dist/tools/open-devtools.js.map +1 -0
- package/dist/tools/scroll-page.d.ts +11 -0
- package/dist/tools/scroll-page.d.ts.map +1 -0
- package/dist/tools/scroll-page.js +85 -0
- package/dist/tools/scroll-page.js.map +1 -0
- package/dist/tools/select-option.d.ts +12 -0
- package/dist/tools/select-option.d.ts.map +1 -0
- package/dist/tools/select-option.js +197 -0
- package/dist/tools/select-option.js.map +1 -0
- package/dist/tools/upload-file.d.ts +10 -0
- package/dist/tools/upload-file.d.ts.map +1 -0
- package/dist/tools/upload-file.js +99 -0
- package/dist/tools/upload-file.js.map +1 -0
- package/dist/tools/wait-for-element.d.ts +11 -0
- package/dist/tools/wait-for-element.d.ts.map +1 -0
- package/dist/tools/wait-for-element.js +68 -0
- package/dist/tools/wait-for-element.js.map +1 -0
- package/dist/tools/web-page-fetcher.js +1 -1
- package/dist/tools/web-page-fetcher.js.map +1 -1
- package/dist/types/index.d.ts +111 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/browser.d.ts.map +1 -1
- package/dist/utils/browser.js +12 -11
- package/dist/utils/browser.js.map +1 -1
- package/dist/utils/console-logger.d.ts +27 -0
- package/dist/utils/console-logger.d.ts.map +1 -0
- package/dist/utils/console-logger.js +274 -0
- package/dist/utils/console-logger.js.map +1 -0
- package/dist/utils/html-skeleton.d.ts +6 -0
- package/dist/utils/html-skeleton.d.ts.map +1 -0
- package/dist/utils/html-skeleton.js +63 -0
- package/dist/utils/html-skeleton.js.map +1 -0
- package/dist/utils/page-content.d.ts.map +1 -1
- package/dist/utils/page-content.js +9 -55
- package/dist/utils/page-content.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,17 @@ import { focusInputElement } from './tools/focus-input.js';
|
|
|
8
8
|
import { typeInputElement } from './tools/type-input.js';
|
|
9
9
|
import { clickElement } from './tools/click-element.js';
|
|
10
10
|
import { captureElement } from './tools/capture-element.js';
|
|
11
|
+
import { getPageInfo } from './tools/get-page-info.js';
|
|
12
|
+
import { openDevtools } from './tools/open-devtools.js';
|
|
13
|
+
import { getConsoleLogs } from './tools/get-console-logs.js';
|
|
14
|
+
import { scrollPage } from './tools/scroll-page.js';
|
|
15
|
+
import { waitForElement } from './tools/wait-for-element.js';
|
|
16
|
+
import { hoverElement } from './tools/hover-element.js';
|
|
17
|
+
import { getElementAttributes } from './tools/get-element-attributes.js';
|
|
18
|
+
import { selectOption } from './tools/select-option.js';
|
|
19
|
+
import { navigateHistory } from './tools/navigate-history.js';
|
|
20
|
+
import { uploadFile } from './tools/upload-file.js';
|
|
21
|
+
import { executeJs } from './tools/execute-js.js';
|
|
11
22
|
// 定义工具列表
|
|
12
23
|
const tools = [
|
|
13
24
|
{
|
|
@@ -46,11 +57,13 @@ const tools = [
|
|
|
46
57
|
},
|
|
47
58
|
{
|
|
48
59
|
name: 'click-element',
|
|
49
|
-
description: '
|
|
60
|
+
description: '点击页面上的指定元素。支持按钮、链接、复选框、树节点等任意可点击元素。支持多种选择器格式,兼容 React/Vue 等现代框架组件。',
|
|
50
61
|
inputSchema: z.object({
|
|
51
62
|
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
52
|
-
selector: z.string().describe('CSS
|
|
63
|
+
selector: z.string().describe('元素选择器,支持多种格式:\n1. CSS选择器:#login-btn、.submit-button、button[type="submit"]\n2. 文本匹配:text=登录(精确匹配文本)\n3. 包含文本::has-text("登录")(包含指定文本)\n4. jQuery风格::contains("登录")(包含指定文本)\n推荐使用 text=xxx 格式,兼容性最好'),
|
|
53
64
|
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
65
|
+
waitForNavigation: z.boolean().optional().default(false).describe('点击后是否等待页面导航完成,默认 false'),
|
|
66
|
+
timeout: z.number().optional().default(10000).describe('等待元素出现的超时时间(毫秒),默认 10000'),
|
|
54
67
|
}),
|
|
55
68
|
},
|
|
56
69
|
{
|
|
@@ -63,6 +76,117 @@ const tools = [
|
|
|
63
76
|
fullPage: z.boolean().optional().default(false).describe('是否截取整个页面(滚动截屏),仅在 selector 未提供时有效,默认 false'),
|
|
64
77
|
}),
|
|
65
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: 'get-page-info',
|
|
81
|
+
description: '获取当前页面的信息(HTML骨架、文本内容、表单元素等),不刷新页面。适合在点击导航后获取新页面内容。',
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面,默认 current'),
|
|
84
|
+
includeHtmlSkeleton: z.boolean().optional().default(false).describe('是否返回 HTML 骨架结构,默认 false'),
|
|
85
|
+
includeFormElements: z.boolean().optional().default(false).describe('是否返回页面表单元素信息,默认 false'),
|
|
86
|
+
includeText: z.boolean().optional().default(true).describe('是否返回页面文本内容,默认 true'),
|
|
87
|
+
contentSelector: z.string().optional().default('body').describe('内容选择器,用于指定要提取内容的 HTML 元素,默认 body'),
|
|
88
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面(不刷新),默认 true'),
|
|
89
|
+
downloadImages: z.boolean().optional().default(false).describe('是否下载图片并转换为 base64,默认 false'),
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'open-devtools',
|
|
94
|
+
description: '打开浏览器开发者工具(DevTools)。支持开启手机设备模拟模式,方便进行移动端页面调试。',
|
|
95
|
+
inputSchema: z.object({
|
|
96
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
97
|
+
isMobile: z.boolean().optional().default(false).describe('是否开启手机设备模式,默认 false'),
|
|
98
|
+
mobileDevice: z.string().optional().default('iPhone 13').describe('模拟的手机设备型号,如:iPhone 13、iPhone 14、Pixel 5、Galaxy S21 等,默认 iPhone 13。仅当 isMobile 为 true 时生效'),
|
|
99
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'get-console-logs',
|
|
104
|
+
description: '获取浏览器控制台日志,包括错误、警告、普通日志等。用于调试页面 JavaScript 错误和查看 console 输出。',
|
|
105
|
+
inputSchema: z.object({
|
|
106
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
107
|
+
logTypes: z.array(z.enum(['error', 'warn', 'log', 'info', 'debug', 'all'])).optional().default(['all']).describe('要获取的日志类型数组,默认 ["all"] 获取所有类型。可选值:error, warn, log, info, debug, all'),
|
|
108
|
+
clearAfterGet: z.boolean().optional().default(false).describe('获取后是否清空日志缓冲区,默认 false'),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'scroll-page',
|
|
113
|
+
description: '滚动页面到指定位置或元素。支持上下左右滚动、按像素量滚动、滚动到指定元素位置。',
|
|
114
|
+
inputSchema: z.object({
|
|
115
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
116
|
+
direction: z.enum(['up', 'down', 'left', 'right']).optional().default('down').describe('滚动方向:up(向上) / down(向下) / left(向左) / right(向右),默认 down'),
|
|
117
|
+
amount: z.number().optional().default(500).describe('滚动的像素量,默认 500。当提供 selector 时此参数无效'),
|
|
118
|
+
selector: z.string().optional().describe('CSS 选择器,如果提供则滚动到该元素位置(忽略 direction 和 amount)'),
|
|
119
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'wait-for-element',
|
|
124
|
+
description: '等待指定元素出现在页面上。用于需要等待动态加载内容的场景,确保元素存在后再进行操作。',
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
127
|
+
selector: z.string().describe('CSS 选择器,等待该选择器匹配的元素出现'),
|
|
128
|
+
timeout: z.number().optional().default(30000).describe('超时时间(毫秒),默认 30000'),
|
|
129
|
+
state: z.enum(['attached', 'visible', 'hidden']).optional().default('visible').describe('等待状态:attached - 元素存在于DOM中;visible - 元素可见(默认);hidden - 元素隐藏或不存在'),
|
|
130
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'hover-element',
|
|
135
|
+
description: '将鼠标悬停在指定元素上。用于触发悬停菜单、工具提示等需要鼠标悬停才能显示的交互效果。',
|
|
136
|
+
inputSchema: z.object({
|
|
137
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
138
|
+
selector: z.string().describe('CSS 选择器,悬停到该元素上'),
|
|
139
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'get-element-attributes',
|
|
144
|
+
description: '获取页面元素的属性值。可以获取指定属性,也可以获取元素的所有属性。用于检查元素状态、获取数据属性等。',
|
|
145
|
+
inputSchema: z.object({
|
|
146
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
147
|
+
selector: z.string().describe('CSS 选择器'),
|
|
148
|
+
attributes: z.array(z.string()).optional().default([]).describe('要获取的属性名列表,为空则获取所有属性。例如:["href", "data-id", "class"]'),
|
|
149
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'select-option',
|
|
154
|
+
description: '选择下拉菜单的选项。支持原生 select 元素和常见 UI 框架的自定义下拉菜单(Element UI、Ant Design 等)。可通过 value、显示文本或索引选择。',
|
|
155
|
+
inputSchema: z.object({
|
|
156
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
157
|
+
selector: z.string().describe('CSS 选择器,指向 select 元素或自定义下拉菜单触发元素'),
|
|
158
|
+
value: z.string().optional().describe('按 option 的 value 属性选择'),
|
|
159
|
+
label: z.string().optional().describe('按 option 的显示文本选择'),
|
|
160
|
+
index: z.number().optional().describe('按索引选择(从 0 开始)'),
|
|
161
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'navigate-history',
|
|
166
|
+
description: '浏览器前进或后退导航。用于在浏览历史中前进或后退,等效于浏览器的后退/前进按钮。',
|
|
167
|
+
inputSchema: z.object({
|
|
168
|
+
direction: z.enum(['back', 'forward']).optional().default('back').describe('导航方向:back(后退) / forward(前进),默认 back'),
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'upload-file',
|
|
173
|
+
description: '上传文件到页面上的文件输入框。支持 input[type="file"] 元素,自动处理隐藏的文件输入控件。',
|
|
174
|
+
inputSchema: z.object({
|
|
175
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
176
|
+
selector: z.string().describe('CSS 选择器,指向 input[type="file"] 元素'),
|
|
177
|
+
filePath: z.string().describe('要上传的文件的绝对路径'),
|
|
178
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'execute-js',
|
|
183
|
+
description: '在页面中执行任意 JavaScript 代码并返回结果。可以用于执行复杂的 DOM 操作、获取数据、调用页面上的函数等。例如:document.getElementById("btn").click()、document.querySelector(".title").textContent、window.scrollTo(0, document.body.scrollHeight)',
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
url: z.string().optional().default('current').describe('网页 URL,如果是 current 则使用当前已打开的页面'),
|
|
186
|
+
script: z.string().describe('要执行的 JavaScript 代码。支持完整的 JS 语法,可以访问 DOM 和页面上的全局变量。返回值会被序列化输出'),
|
|
187
|
+
useExistingPage: z.boolean().optional().default(true).describe('是否使用已存在的页面,默认 true'),
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
66
190
|
];
|
|
67
191
|
// 创建 MCP Server
|
|
68
192
|
const server = new Server({
|
|
@@ -229,7 +353,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
229
353
|
if (name === 'click-element') {
|
|
230
354
|
const validatedArgs = tools[3].inputSchema.parse(args);
|
|
231
355
|
console.error(`正在点击元素: ${validatedArgs.selector}`);
|
|
232
|
-
const result = await clickElement(validatedArgs.url, validatedArgs.selector, validatedArgs.useExistingPage !== false);
|
|
356
|
+
const result = await clickElement(validatedArgs.url, validatedArgs.selector, validatedArgs.useExistingPage !== false, validatedArgs.waitForNavigation || false, validatedArgs.timeout || 10000);
|
|
233
357
|
if (result.success) {
|
|
234
358
|
const info = result.elementInfo;
|
|
235
359
|
let responseText = `✅ 点击成功!\n\n`;
|
|
@@ -302,6 +426,429 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
302
426
|
};
|
|
303
427
|
}
|
|
304
428
|
}
|
|
429
|
+
if (name === 'get-page-info') {
|
|
430
|
+
const validatedArgs = tools[5].inputSchema.parse(args);
|
|
431
|
+
console.error(`正在获取页面信息(不刷新): ${validatedArgs.url}`);
|
|
432
|
+
const result = await getPageInfo(validatedArgs.url, validatedArgs.includeHtmlSkeleton || false, validatedArgs.includeFormElements || false, validatedArgs.includeText !== false, validatedArgs.contentSelector || 'body', validatedArgs.useExistingPage !== false, validatedArgs.downloadImages || false);
|
|
433
|
+
if (result.success) {
|
|
434
|
+
const contentParts = [];
|
|
435
|
+
// 添加页面基本信息
|
|
436
|
+
contentParts.push({
|
|
437
|
+
type: 'text',
|
|
438
|
+
text: `📄 页面信息\n- 标题: ${result.title}\n- URL: ${result.url}\n`,
|
|
439
|
+
});
|
|
440
|
+
// 添加 HTML 骨架
|
|
441
|
+
if (validatedArgs.includeHtmlSkeleton && result.htmlSkeleton) {
|
|
442
|
+
contentParts.push({
|
|
443
|
+
type: 'text',
|
|
444
|
+
text: `\n🦴 HTML 骨架:\n\`\`\`html\n${result.htmlSkeleton}\n\`\`\`\n`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// 添加表单元素
|
|
448
|
+
if (validatedArgs.includeFormElements && result.formElements && result.formElements.length > 0) {
|
|
449
|
+
contentParts.push({
|
|
450
|
+
type: 'text',
|
|
451
|
+
text: `\n📋 页面表单元素 (${result.formElements.length} 个):\n`,
|
|
452
|
+
});
|
|
453
|
+
result.formElements.forEach((el, i) => {
|
|
454
|
+
let elInfo = `${i + 1}. <${el.tagName}`;
|
|
455
|
+
if (el.type)
|
|
456
|
+
elInfo += ` type="${el.type}"`;
|
|
457
|
+
if (el.id)
|
|
458
|
+
elInfo += ` id="${el.id}"`;
|
|
459
|
+
if (el.name)
|
|
460
|
+
elInfo += ` name="${el.name}"`;
|
|
461
|
+
if (el.placeholder)
|
|
462
|
+
elInfo += ` placeholder="${el.placeholder.slice(0, 30)}"`;
|
|
463
|
+
if (el.value)
|
|
464
|
+
elInfo += ` value="${el.value.slice(0, 30)}"`;
|
|
465
|
+
elInfo += `>\n 选择器: ${el.selector}\n`;
|
|
466
|
+
contentParts.push({
|
|
467
|
+
type: 'text',
|
|
468
|
+
text: elInfo,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
// 添加文本内容
|
|
473
|
+
if (validatedArgs.includeText !== false && result.text) {
|
|
474
|
+
contentParts.push({
|
|
475
|
+
type: 'text',
|
|
476
|
+
text: `\n📝 页面文本内容:\n${result.text}`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// 添加下载的图片(如果启用)
|
|
480
|
+
if (validatedArgs.downloadImages && result.downloadedImages && result.downloadedImages.length > 0) {
|
|
481
|
+
contentParts.push({
|
|
482
|
+
type: 'text',
|
|
483
|
+
text: `\n🖼️ 已下载图片 (${result.downloadedImages.length} 张):`,
|
|
484
|
+
});
|
|
485
|
+
for (const img of result.downloadedImages) {
|
|
486
|
+
contentParts.push({
|
|
487
|
+
type: 'image',
|
|
488
|
+
data: img.base64,
|
|
489
|
+
mimeType: img.mimeType,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// 否则只添加图片 URL 列表
|
|
494
|
+
else if (result.images && result.images.length > 0) {
|
|
495
|
+
contentParts.push({
|
|
496
|
+
type: 'text',
|
|
497
|
+
text: `\n🖼️ 页面图片 URL (${result.images.length} 个):\n${result.images.map((url, i) => `${i + 1}. ${url}`).join('\n')}`,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return { content: contentParts };
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
return {
|
|
504
|
+
content: [{
|
|
505
|
+
type: 'text',
|
|
506
|
+
text: `获取页面信息失败:${result.error || '未知错误'}`,
|
|
507
|
+
}],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (name === 'open-devtools') {
|
|
512
|
+
const validatedArgs = tools[6].inputSchema.parse(args);
|
|
513
|
+
console.error(`正在打开开发者工具: ${validatedArgs.isMobile ? `手机模式 (${validatedArgs.mobileDevice})` : '桌面模式'}`);
|
|
514
|
+
const result = await openDevtools(validatedArgs.url, validatedArgs.isMobile || false, validatedArgs.mobileDevice || 'iPhone 13', validatedArgs.useExistingPage !== false);
|
|
515
|
+
if (result.success) {
|
|
516
|
+
let responseText = `✅ 开发者工具已打开!\n\n`;
|
|
517
|
+
responseText += `当前状态:\n`;
|
|
518
|
+
responseText += `- 页面 URL: ${result.url}\n`;
|
|
519
|
+
responseText += `- 设备模式: ${validatedArgs.isMobile ? `📱 手机模式 (${validatedArgs.mobileDevice})` : '🖥️ 桌面模式'}\n`;
|
|
520
|
+
if (validatedArgs.isMobile && result.viewport) {
|
|
521
|
+
responseText += `- 视口尺寸: ${result.viewport.width} × ${result.viewport.height}\n`;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
content: [{
|
|
525
|
+
type: 'text',
|
|
526
|
+
text: responseText,
|
|
527
|
+
}],
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
return {
|
|
532
|
+
content: [{
|
|
533
|
+
type: 'text',
|
|
534
|
+
text: `❌ 打开开发者工具失败:${result.error || '未知错误'}`,
|
|
535
|
+
}],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (name === 'get-console-logs') {
|
|
540
|
+
const validatedArgs = tools[7].inputSchema.parse(args);
|
|
541
|
+
console.error(`正在获取控制台日志: ${validatedArgs.logTypes?.join(', ') || 'all'}`);
|
|
542
|
+
const result = await getConsoleLogs(validatedArgs.url, validatedArgs.logTypes || ['all'], validatedArgs.clearAfterGet || false);
|
|
543
|
+
if (result.success) {
|
|
544
|
+
let responseText = `📋 控制台日志\n\n`;
|
|
545
|
+
responseText += `页面: ${result.url}\n`;
|
|
546
|
+
responseText += `总日志数: ${result.totalCount}\n`;
|
|
547
|
+
responseText += `- 🔴 错误: ${result.errorCount}\n`;
|
|
548
|
+
responseText += `- 🟡 警告: ${result.warnCount}\n`;
|
|
549
|
+
responseText += `- 🔵 其他: ${result.totalCount - result.errorCount - result.warnCount}\n\n`;
|
|
550
|
+
if (result.logs.length > 0) {
|
|
551
|
+
responseText += `---\n\n`;
|
|
552
|
+
result.logs.forEach((log, i) => {
|
|
553
|
+
const icon = log.type === 'error' ? '🔴' : log.type === 'warn' ? '🟡' : '🔵';
|
|
554
|
+
responseText += `${icon} [${log.type.toUpperCase()}] ${log.message}\n`;
|
|
555
|
+
if (log.location) {
|
|
556
|
+
responseText += ` 📍 ${log.location}\n`;
|
|
557
|
+
}
|
|
558
|
+
if (log.stack) {
|
|
559
|
+
responseText += ` 📚 堆栈:\n${log.stack.split('\n').map(line => ` ${line}`).join('\n')}\n`;
|
|
560
|
+
}
|
|
561
|
+
responseText += `\n`;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
responseText += `暂无日志\n`;
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
content: [{
|
|
569
|
+
type: 'text',
|
|
570
|
+
text: responseText,
|
|
571
|
+
}],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
return {
|
|
576
|
+
content: [{
|
|
577
|
+
type: 'text',
|
|
578
|
+
text: `❌ 获取控制台日志失败:${result.error || '未知错误'}`,
|
|
579
|
+
}],
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (name === 'scroll-page') {
|
|
584
|
+
const validatedArgs = tools[8].inputSchema.parse(args);
|
|
585
|
+
console.error(`正在滚动页面: ${validatedArgs.selector ? `到元素 ${validatedArgs.selector}` : `${validatedArgs.direction || 'down'} ${validatedArgs.amount || 500}px`}`);
|
|
586
|
+
const result = await scrollPage(validatedArgs.url, validatedArgs.direction || 'down', validatedArgs.amount || 500, validatedArgs.selector, validatedArgs.useExistingPage !== false);
|
|
587
|
+
if (result.success) {
|
|
588
|
+
let responseText = `✅ 滚动成功!\n\n`;
|
|
589
|
+
responseText += `滚动信息:\n`;
|
|
590
|
+
responseText += `- 当前位置: x=${result.scrollPosition.x}, y=${result.scrollPosition.y}\n`;
|
|
591
|
+
responseText += `- 页面高度: ${result.pageHeight}px\n`;
|
|
592
|
+
responseText += `- 视口高度: ${result.viewportHeight}px\n`;
|
|
593
|
+
const scrollPercentage = result.pageHeight > 0
|
|
594
|
+
? Math.round((result.scrollPosition.y / (result.pageHeight - result.viewportHeight)) * 100)
|
|
595
|
+
: 0;
|
|
596
|
+
responseText += `- 滚动进度: ~${scrollPercentage}%\n`;
|
|
597
|
+
return {
|
|
598
|
+
content: [{
|
|
599
|
+
type: 'text',
|
|
600
|
+
text: responseText,
|
|
601
|
+
}],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
return {
|
|
606
|
+
content: [{
|
|
607
|
+
type: 'text',
|
|
608
|
+
text: `❌ 滚动失败:${result.error || '未知错误'}`,
|
|
609
|
+
}],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (name === 'wait-for-element') {
|
|
614
|
+
const validatedArgs = tools[9].inputSchema.parse(args);
|
|
615
|
+
console.error(`等待元素: ${validatedArgs.selector} (状态: ${validatedArgs.state || 'visible'}, 超时: ${validatedArgs.timeout || 30000}ms)`);
|
|
616
|
+
const result = await waitForElement(validatedArgs.url, validatedArgs.selector, validatedArgs.timeout || 30000, validatedArgs.state || 'visible', validatedArgs.useExistingPage !== false);
|
|
617
|
+
if (result.success) {
|
|
618
|
+
const info = result.elementInfo;
|
|
619
|
+
let responseText = `✅ 元素已${validatedArgs.state === 'hidden' ? '隐藏' : '出现'}!\n\n`;
|
|
620
|
+
if (info) {
|
|
621
|
+
responseText += `元素信息:\n`;
|
|
622
|
+
responseText += `- 标签: ${info.tagName}\n`;
|
|
623
|
+
if (info.type)
|
|
624
|
+
responseText += `- 类型: ${info.type}\n`;
|
|
625
|
+
if (info.id)
|
|
626
|
+
responseText += `- ID: ${info.id}\n`;
|
|
627
|
+
if (info.name)
|
|
628
|
+
responseText += `- Name: ${info.name}\n`;
|
|
629
|
+
if (info.text)
|
|
630
|
+
responseText += `- 文本: ${info.text.slice(0, 100)}\n`;
|
|
631
|
+
responseText += `- 可见: ${info.visible ? '是' : '否'}\n`;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [{
|
|
635
|
+
type: 'text',
|
|
636
|
+
text: responseText,
|
|
637
|
+
}],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
return {
|
|
642
|
+
content: [{
|
|
643
|
+
type: 'text',
|
|
644
|
+
text: `❌ 等待元素失败:${result.error || '未知错误'}`,
|
|
645
|
+
}],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (name === 'hover-element') {
|
|
650
|
+
const validatedArgs = tools[10].inputSchema.parse(args);
|
|
651
|
+
console.error(`正在悬停到元素: ${validatedArgs.selector}`);
|
|
652
|
+
const result = await hoverElement(validatedArgs.url, validatedArgs.selector, validatedArgs.useExistingPage !== false);
|
|
653
|
+
if (result.success) {
|
|
654
|
+
const info = result.elementInfo;
|
|
655
|
+
let responseText = `✅ 悬停成功!\n\n`;
|
|
656
|
+
responseText += `元素信息:\n`;
|
|
657
|
+
responseText += `- 标签: ${info?.tagName}\n`;
|
|
658
|
+
if (info?.id)
|
|
659
|
+
responseText += `- ID: ${info.id}\n`;
|
|
660
|
+
if (info?.className)
|
|
661
|
+
responseText += `- Class: ${info.className}\n`;
|
|
662
|
+
if (info?.text)
|
|
663
|
+
responseText += `- 文本: ${info.text.slice(0, 100)}\n`;
|
|
664
|
+
return {
|
|
665
|
+
content: [{
|
|
666
|
+
type: 'text',
|
|
667
|
+
text: responseText,
|
|
668
|
+
}],
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
return {
|
|
673
|
+
content: [{
|
|
674
|
+
type: 'text',
|
|
675
|
+
text: `❌ 悬停失败:${result.error || '未知错误'}`,
|
|
676
|
+
}],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (name === 'get-element-attributes') {
|
|
681
|
+
const validatedArgs = tools[11].inputSchema.parse(args);
|
|
682
|
+
console.error(`获取元素属性: ${validatedArgs.selector}`);
|
|
683
|
+
const result = await getElementAttributes(validatedArgs.url, validatedArgs.selector, validatedArgs.attributes || [], validatedArgs.useExistingPage !== false);
|
|
684
|
+
if (result.success) {
|
|
685
|
+
let responseText = `✅ 获取属性成功!\n\n`;
|
|
686
|
+
if (result.elementInfo) {
|
|
687
|
+
responseText += `元素信息:\n`;
|
|
688
|
+
responseText += `- 标签: ${result.elementInfo.tagName}\n`;
|
|
689
|
+
if (result.elementInfo.id)
|
|
690
|
+
responseText += `- ID: ${result.elementInfo.id}\n`;
|
|
691
|
+
if (result.elementInfo.className)
|
|
692
|
+
responseText += `- Class: ${result.elementInfo.className}\n`;
|
|
693
|
+
responseText += `\n`;
|
|
694
|
+
}
|
|
695
|
+
responseText += `属性列表 (${Object.keys(result.attributes).length} 个):\n`;
|
|
696
|
+
for (const [key, value] of Object.entries(result.attributes)) {
|
|
697
|
+
const displayValue = value.length > 100 ? value.slice(0, 100) + '...' : value;
|
|
698
|
+
responseText += `- ${key}: ${displayValue}\n`;
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
content: [{
|
|
702
|
+
type: 'text',
|
|
703
|
+
text: responseText,
|
|
704
|
+
}],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
return {
|
|
709
|
+
content: [{
|
|
710
|
+
type: 'text',
|
|
711
|
+
text: `❌ 获取属性失败:${result.error || '未知错误'}`,
|
|
712
|
+
}],
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (name === 'select-option') {
|
|
717
|
+
const validatedArgs = tools[12].inputSchema.parse(args);
|
|
718
|
+
console.error(`选择下拉选项: ${validatedArgs.selector}`);
|
|
719
|
+
const result = await selectOption(validatedArgs.url, validatedArgs.selector, validatedArgs.value, validatedArgs.label, validatedArgs.index, validatedArgs.useExistingPage !== false);
|
|
720
|
+
if (result.success) {
|
|
721
|
+
let responseText = `✅ 选择成功!\n\n`;
|
|
722
|
+
responseText += `选中项:\n`;
|
|
723
|
+
if (result.selectedValue)
|
|
724
|
+
responseText += `- Value: ${result.selectedValue}\n`;
|
|
725
|
+
if (result.selectedText)
|
|
726
|
+
responseText += `- 文本: ${result.selectedText}\n`;
|
|
727
|
+
if (result.allOptions && result.allOptions.length > 0) {
|
|
728
|
+
responseText += `\n所有选项 (${result.allOptions.length} 个):\n`;
|
|
729
|
+
result.allOptions.forEach((opt, i) => {
|
|
730
|
+
const marker = opt.selected ? ' ← 已选中' : '';
|
|
731
|
+
responseText += `${i}. ${opt.text} (value="${opt.value}")${marker}\n`;
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
content: [{
|
|
736
|
+
type: 'text',
|
|
737
|
+
text: responseText,
|
|
738
|
+
}],
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
return {
|
|
743
|
+
content: [{
|
|
744
|
+
type: 'text',
|
|
745
|
+
text: `❌ 选择失败:${result.error || '未知错误'}`,
|
|
746
|
+
}],
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (name === 'navigate-history') {
|
|
751
|
+
const validatedArgs = tools[13].inputSchema.parse(args);
|
|
752
|
+
console.error(`导航历史: ${validatedArgs.direction === 'back' ? '后退' : '前进'}`);
|
|
753
|
+
const result = await navigateHistory(validatedArgs.direction);
|
|
754
|
+
if (result.success) {
|
|
755
|
+
let responseText = `✅ 导航成功!\n\n`;
|
|
756
|
+
responseText += `方向: ${result.direction === 'back' ? '⬅️ 后退' : '➡️ 前进'}\n`;
|
|
757
|
+
responseText += `页面标题: ${result.title}\n`;
|
|
758
|
+
responseText += `页面 URL: ${result.url}\n`;
|
|
759
|
+
return {
|
|
760
|
+
content: [{
|
|
761
|
+
type: 'text',
|
|
762
|
+
text: responseText,
|
|
763
|
+
}],
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
return {
|
|
768
|
+
content: [{
|
|
769
|
+
type: 'text',
|
|
770
|
+
text: `❌ 导航失败:${result.error || '未知错误'}`,
|
|
771
|
+
}],
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (name === 'upload-file') {
|
|
776
|
+
const validatedArgs = tools[14].inputSchema.parse(args);
|
|
777
|
+
console.error(`上传文件: ${validatedArgs.filePath} -> ${validatedArgs.selector}`);
|
|
778
|
+
const result = await uploadFile(validatedArgs.url, validatedArgs.selector, validatedArgs.filePath, validatedArgs.useExistingPage !== false);
|
|
779
|
+
if (result.success) {
|
|
780
|
+
let responseText = `✅ 文件上传成功!\n\n`;
|
|
781
|
+
responseText += `文件路径: ${result.filePath}\n`;
|
|
782
|
+
if (result.elementInfo) {
|
|
783
|
+
responseText += `\n上传控件信息:\n`;
|
|
784
|
+
responseText += `- 标签: ${result.elementInfo.tagName}\n`;
|
|
785
|
+
if (result.elementInfo.name)
|
|
786
|
+
responseText += `- Name: ${result.elementInfo.name}\n`;
|
|
787
|
+
if (result.elementInfo.id)
|
|
788
|
+
responseText += `- ID: ${result.elementInfo.id}\n`;
|
|
789
|
+
if (result.elementInfo.accept)
|
|
790
|
+
responseText += `- 接受的文件类型: ${result.elementInfo.accept}\n`;
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
content: [{
|
|
794
|
+
type: 'text',
|
|
795
|
+
text: responseText,
|
|
796
|
+
}],
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
return {
|
|
801
|
+
content: [{
|
|
802
|
+
type: 'text',
|
|
803
|
+
text: `❌ 文件上传失败:${result.error || '未知错误'}`,
|
|
804
|
+
}],
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (name === 'execute-js') {
|
|
809
|
+
const validatedArgs = tools[15].inputSchema.parse(args);
|
|
810
|
+
console.error(`执行 JavaScript 代码: ${validatedArgs.script.slice(0, 100)}...`);
|
|
811
|
+
const result = await executeJs(validatedArgs.url, validatedArgs.script, validatedArgs.useExistingPage !== false);
|
|
812
|
+
if (result.success) {
|
|
813
|
+
let responseText = `✅ 执行成功!\n\n`;
|
|
814
|
+
responseText += `返回类型: ${result.resultType}\n`;
|
|
815
|
+
if (result.result !== undefined && result.result !== null) {
|
|
816
|
+
responseText += `\n📦 返回结果:\n`;
|
|
817
|
+
if (typeof result.result === 'object') {
|
|
818
|
+
responseText += `\`\`\`json\n${JSON.stringify(result.result, null, 2)}\n\`\`\`\n`;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
responseText += `${result.result}\n`;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else if (result.result === null) {
|
|
825
|
+
responseText += `\n📦 返回结果: null\n`;
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
responseText += `\n📦 返回结果: undefined(无返回值)\n`;
|
|
829
|
+
}
|
|
830
|
+
if (result.consoleOutput && result.consoleOutput.length > 0) {
|
|
831
|
+
responseText += `\n📋 控制台输出:\n`;
|
|
832
|
+
result.consoleOutput.forEach((line) => {
|
|
833
|
+
responseText += `${line}\n`;
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
content: [{
|
|
838
|
+
type: 'text',
|
|
839
|
+
text: responseText,
|
|
840
|
+
}],
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
return {
|
|
845
|
+
content: [{
|
|
846
|
+
type: 'text',
|
|
847
|
+
text: `❌ 执行失败:${result.error || '未知错误'}`,
|
|
848
|
+
}],
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
305
852
|
throw new Error(`未知工具:${name}`);
|
|
306
853
|
});
|
|
307
854
|
// 启动服务器
|