@becrafter/prompt-manager 0.0.18 → 0.1.1
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/IFLOW.md +175 -0
- package/README.md +145 -234
- package/app/desktop/assets/app.1.png +0 -0
- package/app/desktop/assets/app.png +0 -0
- package/app/desktop/assets/icons/icon.icns +0 -0
- package/app/desktop/assets/icons/icon.ico +0 -0
- package/app/desktop/assets/icons/icon.png +0 -0
- package/app/desktop/assets/icons/tray.png +0 -0
- package/app/desktop/assets/templates/about.html +147 -0
- package/app/desktop/assets/tray.png +0 -0
- package/app/desktop/main.js +187 -732
- package/app/desktop/package-lock.json +723 -522
- package/app/desktop/package.json +54 -25
- package/app/desktop/preload.js +7 -0
- package/app/desktop/src/core/error-handler.js +108 -0
- package/app/desktop/src/core/event-emitter.js +84 -0
- package/app/desktop/src/core/logger.js +108 -0
- package/app/desktop/src/core/state-manager.js +125 -0
- package/app/desktop/src/services/module-loader.js +214 -0
- package/app/desktop/src/services/runtime-manager.js +301 -0
- package/app/desktop/src/services/service-manager.js +169 -0
- package/app/desktop/src/services/update-manager.js +268 -0
- package/app/desktop/src/ui/about-dialog-manager.js +208 -0
- package/app/desktop/src/ui/admin-window-manager.js +757 -0
- package/app/desktop/src/ui/splash-manager.js +253 -0
- package/app/desktop/src/ui/tray-manager.js +186 -0
- package/app/desktop/src/utils/icon-manager.js +133 -0
- package/app/desktop/src/utils/path-utils.js +58 -0
- package/app/desktop/src/utils/resource-paths.js +49 -0
- package/app/desktop/src/utils/resource-sync.js +260 -0
- package/app/desktop/src/utils/runtime-sync.js +241 -0
- package/app/desktop/src/utils/template-renderer.js +284 -0
- package/app/desktop/src/utils/version-utils.js +59 -0
- package/examples/prompts/engineer/engineer-professional.yaml +92 -0
- package/examples/prompts/engineer/laowang-engineer.yaml +132 -0
- package/examples/prompts/engineer/nekomata-engineer.yaml +123 -0
- package/examples/prompts/engineer/ojousama-engineer.yaml +124 -0
- package/examples/prompts/recommend/human_3-0_growth_diagnostic_coach_prompt.yaml +105 -0
- package/examples/prompts/workflow/sixstep-workflow.yaml +192 -0
- package/package.json +18 -9
- package/packages/admin-ui/.babelrc +3 -0
- package/packages/admin-ui/admin.html +237 -4784
- package/packages/admin-ui/css/main.css +2592 -0
- package/packages/admin-ui/css/recommended-prompts.css +610 -0
- package/packages/admin-ui/package-lock.json +6981 -0
- package/packages/admin-ui/package.json +36 -0
- package/packages/admin-ui/src/codemirror.js +53 -0
- package/packages/admin-ui/src/index.js +3188 -0
- package/packages/admin-ui/webpack.config.js +76 -0
- package/packages/resources/tools/chrome-devtools/README.md +310 -0
- package/packages/resources/tools/chrome-devtools/chrome-devtools.tool.js +1703 -0
- package/packages/resources/tools/file-reader/README.md +289 -0
- package/packages/resources/tools/file-reader/file-reader.tool.js +1545 -0
- package/packages/resources/tools/filesystem/README.md +359 -0
- package/packages/resources/tools/filesystem/filesystem.tool.js +538 -0
- package/packages/resources/tools/ollama-remote/README.md +192 -0
- package/packages/resources/tools/ollama-remote/ollama-remote.tool.js +421 -0
- package/packages/resources/tools/pdf-reader/README.md +236 -0
- package/packages/resources/tools/pdf-reader/pdf-reader.tool.js +565 -0
- package/packages/resources/tools/playwright/README.md +306 -0
- package/packages/resources/tools/playwright/playwright.tool.js +1186 -0
- package/packages/resources/tools/todolist/README.md +394 -0
- package/packages/resources/tools/todolist/todolist.tool.js +1312 -0
- package/packages/server/README.md +142 -0
- package/packages/server/api/admin.routes.js +42 -11
- package/packages/server/api/surge.routes.js +43 -0
- package/packages/server/app.js +119 -14
- package/packages/server/index.js +39 -0
- package/packages/server/mcp/mcp.server.js +346 -28
- package/packages/server/mcp/{mcp.handler.js → prompt.handler.js} +108 -9
- package/packages/server/mcp/sequential-thinking.handler.js +318 -0
- package/packages/server/mcp/think-plan.handler.js +274 -0
- package/packages/server/middlewares/auth.middleware.js +6 -0
- package/packages/server/package.json +51 -0
- package/packages/server/server.js +37 -1
- package/packages/server/toolm/index.js +9 -0
- package/packages/server/toolm/package-installer.service.js +267 -0
- package/packages/server/toolm/test-tools.js +264 -0
- package/packages/server/toolm/tool-context.service.js +334 -0
- package/packages/server/toolm/tool-dependency.service.js +168 -0
- package/packages/server/toolm/tool-description-generator-optimized.service.js +375 -0
- package/packages/server/toolm/tool-description-generator.service.js +312 -0
- package/packages/server/toolm/tool-environment.service.js +200 -0
- package/packages/server/toolm/tool-execution.service.js +277 -0
- package/packages/server/toolm/tool-loader.service.js +219 -0
- package/packages/server/toolm/tool-logger.service.js +223 -0
- package/packages/server/toolm/tool-manager.handler.js +65 -0
- package/packages/server/toolm/tool-manual-generator.service.js +389 -0
- package/packages/server/toolm/tool-mode-handlers.service.js +224 -0
- package/packages/server/toolm/tool-storage.service.js +111 -0
- package/packages/server/toolm/tool-sync.service.js +138 -0
- package/packages/server/toolm/tool-utils.js +20 -0
- package/packages/server/toolm/tool-yaml-parser.service.js +81 -0
- package/packages/server/toolm/validate-system.js +421 -0
- package/packages/server/utils/config.js +49 -5
- package/packages/server/utils/util.js +65 -10
- package/scripts/build-icons.js +135 -0
- package/scripts/build.sh +57 -0
- package/scripts/surge/CNAME +1 -0
- package/scripts/surge/README.md +47 -0
- package/scripts/surge/package-lock.json +34 -0
- package/scripts/surge/package.json +20 -0
- package/scripts/surge/sync-to-surge.js +151 -0
- package/packages/admin-ui/js/closebrackets.min.js +0 -8
- package/packages/admin-ui/js/codemirror.min.js +0 -8
- package/packages/admin-ui/js/js-yaml.min.js +0 -2
- package/packages/admin-ui/js/markdown.min.js +0 -8
- /package/app/desktop/assets/{icon.png → tray.1.png} +0 -0
|
@@ -0,0 +1,1703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome DevTools MCP Tool - 基于 chrome-devtools-mcp 的浏览器自动化工具
|
|
3
|
+
*
|
|
4
|
+
* 战略意义:
|
|
5
|
+
* 1. 完全复用官方实现 - 直接使用 chrome-devtools-mcp 的工具实现,避免重复维护代码
|
|
6
|
+
* 2. 功能完整性 - 支持 chrome-devtools-mcp 的所有功能(性能分析、网络监控、控制台监控等)
|
|
7
|
+
* 3. 生态一致性 - 与官方代码库保持同步,官方更新时只需更新依赖版本
|
|
8
|
+
*
|
|
9
|
+
* 设计理念:
|
|
10
|
+
* - 直接导入 chrome-devtools-mcp 的工具模块(inputTools, pagesTools, networkTools 等)
|
|
11
|
+
* - 使用 McpContext 管理浏览器实例和状态
|
|
12
|
+
* - 创建适配层,将我们的工具接口路由到 chrome-devtools-mcp 的工具 handler
|
|
13
|
+
* - 智能管理浏览器生命周期(默认自动关闭,可通过 keepAlive 保持)
|
|
14
|
+
*
|
|
15
|
+
* 生态定位:
|
|
16
|
+
* 为 AI 提供完整的 Chrome DevTools Protocol 能力,包括性能分析、网络监控、控制台监控等高级功能
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// 全局浏览器实例管理器(模块级别,跨执行保持)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const browserInstances = new Map();
|
|
27
|
+
|
|
28
|
+
// 固定工具名称,确保跨执行复用浏览器实例
|
|
29
|
+
const FIXED_TOOL_NAME = 'chrome-devtools';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取或创建浏览器实例管理器
|
|
33
|
+
* 使用固定的工具名称,确保跨执行复用
|
|
34
|
+
*/
|
|
35
|
+
function getBrowserManager() {
|
|
36
|
+
if (!browserInstances.has(FIXED_TOOL_NAME)) {
|
|
37
|
+
browserInstances.set(FIXED_TOOL_NAME, {
|
|
38
|
+
browser: null,
|
|
39
|
+
context: null,
|
|
40
|
+
lastUsed: null,
|
|
41
|
+
lastNavigationUrl: null // 记录最后导航的 URL,用于检测页面变化
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return browserInstances.get(FIXED_TOOL_NAME);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 清理浏览器实例管理器
|
|
49
|
+
*/
|
|
50
|
+
function clearBrowserManager() {
|
|
51
|
+
browserInstances.delete(FIXED_TOOL_NAME);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// 辅助函数
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 规范化图片 MIME 类型
|
|
60
|
+
* 确保返回有效的图片 MIME 类型,避免 "image/undefined" 等问题
|
|
61
|
+
*
|
|
62
|
+
* @param {string|undefined} mimeType - 原始 MIME 类型
|
|
63
|
+
* @param {string} defaultType - 默认类型,默认为 'image/png'
|
|
64
|
+
* @returns {string} 规范化后的 MIME 类型
|
|
65
|
+
*/
|
|
66
|
+
function normalizeImageMimeType(mimeType, defaultType = 'image/png') {
|
|
67
|
+
// 如果 mimeType 无效,返回默认值
|
|
68
|
+
if (!mimeType ||
|
|
69
|
+
typeof mimeType !== 'string' ||
|
|
70
|
+
mimeType.trim() === '' ||
|
|
71
|
+
mimeType === 'undefined' ||
|
|
72
|
+
mimeType.toLowerCase() === 'undefined' ||
|
|
73
|
+
mimeType.toLowerCase() === 'image/undefined') {
|
|
74
|
+
return defaultType;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const trimmed = mimeType.trim();
|
|
78
|
+
|
|
79
|
+
// 如果已经是完整的 MIME 类型(如 "image/png"),直接使用
|
|
80
|
+
if (trimmed.startsWith('image/')) {
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 如果只是类型名(如 "png"),添加 "image/" 前缀
|
|
85
|
+
const typeName = trimmed.toLowerCase();
|
|
86
|
+
if (typeName && typeName !== 'undefined') {
|
|
87
|
+
return `image/${typeName}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return defaultType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 检查是否是 stale snapshot 错误
|
|
95
|
+
*
|
|
96
|
+
* @param {Error} error - 错误对象
|
|
97
|
+
* @returns {boolean} 是否是 stale snapshot 错误
|
|
98
|
+
*/
|
|
99
|
+
function isStaleSnapshotError(error) {
|
|
100
|
+
if (!error?.message) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const message = error.message;
|
|
105
|
+
return message.includes('stale snapshot') ||
|
|
106
|
+
message.includes('stale') ||
|
|
107
|
+
message.includes('This uid is coming from a stale snapshot') ||
|
|
108
|
+
message.includes('Assignment to constant variable');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// 工具接口定义
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
export default {
|
|
116
|
+
// --------------------------------------------------------------------------
|
|
117
|
+
// 工具元信息接口
|
|
118
|
+
// --------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 获取工具依赖
|
|
122
|
+
*/
|
|
123
|
+
getDependencies() {
|
|
124
|
+
return {
|
|
125
|
+
'chrome-devtools-mcp': '^0.10.2'
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 获取工具元信息
|
|
131
|
+
*/
|
|
132
|
+
getMetadata() {
|
|
133
|
+
return {
|
|
134
|
+
id: 'chrome-devtools',
|
|
135
|
+
name: 'Chrome DevTools MCP',
|
|
136
|
+
description: '基于 chrome-devtools-mcp 的浏览器自动化工具,完全复用官方实现。支持页面导航、元素操作、性能分析、网络监控、控制台监控等功能。支持 keepAlive 参数保持浏览器状态,便于连续操作和调试。',
|
|
137
|
+
version: '1.0.0',
|
|
138
|
+
category: 'utility',
|
|
139
|
+
author: 'Prompt Manager',
|
|
140
|
+
tags: ['browser', 'automation', 'chrome-devtools', 'performance', 'network', 'debugging'],
|
|
141
|
+
scenarios: [
|
|
142
|
+
'网页自动化操作',
|
|
143
|
+
'性能分析和优化',
|
|
144
|
+
'网络请求监控',
|
|
145
|
+
'控制台消息监控',
|
|
146
|
+
'页面截图和快照',
|
|
147
|
+
'设备模拟',
|
|
148
|
+
'连续多步骤操作(使用 keepAlive 保持浏览器状态)',
|
|
149
|
+
'调试和测试(保持浏览器打开以便查看)'
|
|
150
|
+
],
|
|
151
|
+
limitations: [
|
|
152
|
+
'仅支持 Chrome/Chromium 浏览器',
|
|
153
|
+
'首次使用时需要安装 Chrome 浏览器(会自动安装)',
|
|
154
|
+
'浏览器二进制文件会下载到 ~/.cache/chrome-devtools-mcp/ 目录',
|
|
155
|
+
'工具数据存储在 ~/.prompt-manager/toolbox/chrome-devtools/data/ 目录下',
|
|
156
|
+
'浏览器操作需要时间,大页面可能较慢',
|
|
157
|
+
'某些网站可能有反爬虫机制',
|
|
158
|
+
'无头模式可能无法处理某些需要真实浏览器的场景',
|
|
159
|
+
'默认情况下操作完成后会自动关闭浏览器(可通过 keepAlive 参数控制)'
|
|
160
|
+
],
|
|
161
|
+
dataStorage: {
|
|
162
|
+
path: '~/.prompt-manager/toolbox/chrome-devtools/data/',
|
|
163
|
+
description: '工具数据存储目录,包括浏览器安装状态、截图文件等数据',
|
|
164
|
+
note: '数据存储在工具所在目录的 data 子目录下'
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 获取参数Schema
|
|
171
|
+
*/
|
|
172
|
+
getSchema() {
|
|
173
|
+
return {
|
|
174
|
+
parameters: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
method: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: '操作方法',
|
|
180
|
+
enum: [
|
|
181
|
+
// 输入自动化
|
|
182
|
+
'click', 'fill', 'hover', 'press_key', 'drag', 'upload_file', 'handle_dialog', 'fill_form',
|
|
183
|
+
// 导航自动化
|
|
184
|
+
'navigate_page', 'new_page', 'close_page', 'list_pages', 'select_page', 'wait_for',
|
|
185
|
+
// 网络监控
|
|
186
|
+
'list_network_requests', 'get_network_request',
|
|
187
|
+
// 性能分析
|
|
188
|
+
'performance_start_trace', 'performance_stop_trace', 'performance_analyze_insight',
|
|
189
|
+
// 调试
|
|
190
|
+
'evaluate_script', 'get_console_message', 'list_console_messages',
|
|
191
|
+
'take_screenshot', 'take_snapshot',
|
|
192
|
+
// 模拟
|
|
193
|
+
'emulate', 'resize_page',
|
|
194
|
+
// 管理
|
|
195
|
+
'close'
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
// 输入自动化参数
|
|
199
|
+
uid: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: '元素的唯一标识符(从页面快照中获取)'
|
|
202
|
+
},
|
|
203
|
+
dblClick: {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
description: '是否双击(click 方法)',
|
|
206
|
+
default: false
|
|
207
|
+
},
|
|
208
|
+
value: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: '要填写的值(fill 方法)'
|
|
211
|
+
},
|
|
212
|
+
from_uid: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: '拖拽源元素的 uid(drag 方法)'
|
|
215
|
+
},
|
|
216
|
+
to_uid: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: '拖拽目标元素的 uid(drag 方法)'
|
|
219
|
+
},
|
|
220
|
+
filePath: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: '文件路径(upload_file 方法)'
|
|
223
|
+
},
|
|
224
|
+
key: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: '按键或组合键(press_key 方法,如 "Enter", "Control+A")'
|
|
227
|
+
},
|
|
228
|
+
action: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: '对话框操作(handle_dialog 方法:accept/dismiss)',
|
|
231
|
+
enum: ['accept', 'dismiss']
|
|
232
|
+
},
|
|
233
|
+
promptText: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: '提示框文本(handle_dialog 方法,action=accept 时)'
|
|
236
|
+
},
|
|
237
|
+
elements: {
|
|
238
|
+
type: 'array',
|
|
239
|
+
description: '表单元素数组(fill_form 方法)',
|
|
240
|
+
items: {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
uid: { type: 'string' },
|
|
244
|
+
value: { type: 'string' }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
// 导航参数
|
|
249
|
+
url: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: '目标URL(navigate_page、new_page 方法)。对于 navigate_page 方法,当 type 为 back/forward/reload 时可选'
|
|
252
|
+
},
|
|
253
|
+
type: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: '导航类型(navigate_page 方法:url/back/forward/reload)',
|
|
256
|
+
enum: ['url', 'back', 'forward', 'reload'],
|
|
257
|
+
default: 'url'
|
|
258
|
+
},
|
|
259
|
+
ignoreCache: {
|
|
260
|
+
type: 'boolean',
|
|
261
|
+
description: '是否忽略缓存(navigate_page 方法)',
|
|
262
|
+
default: false
|
|
263
|
+
},
|
|
264
|
+
pageIdx: {
|
|
265
|
+
type: 'number',
|
|
266
|
+
description: '页面索引(select_page、close_page 方法)'
|
|
267
|
+
},
|
|
268
|
+
text: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
description: '要等待的文本(wait_for 方法)'
|
|
271
|
+
},
|
|
272
|
+
// 网络监控参数
|
|
273
|
+
includePreservedRequests: {
|
|
274
|
+
type: 'boolean',
|
|
275
|
+
description: '是否包含保留的请求(list_network_requests 方法)',
|
|
276
|
+
default: false
|
|
277
|
+
},
|
|
278
|
+
resourceTypes: {
|
|
279
|
+
type: 'array',
|
|
280
|
+
description: '资源类型过滤(list_network_requests 方法)',
|
|
281
|
+
items: { type: 'string' }
|
|
282
|
+
},
|
|
283
|
+
pageSize: {
|
|
284
|
+
type: 'number',
|
|
285
|
+
description: '最大返回数量(list_network_requests、list_console_messages 方法)'
|
|
286
|
+
},
|
|
287
|
+
reqid: {
|
|
288
|
+
type: 'number',
|
|
289
|
+
description: '网络请求ID(get_network_request 方法,可选,省略时返回当前选中的请求)'
|
|
290
|
+
},
|
|
291
|
+
// 性能分析参数
|
|
292
|
+
reload: {
|
|
293
|
+
type: 'boolean',
|
|
294
|
+
description: '是否重新加载页面(performance_start_trace 方法,必需)'
|
|
295
|
+
},
|
|
296
|
+
autoStop: {
|
|
297
|
+
type: 'boolean',
|
|
298
|
+
description: '是否自动停止追踪(performance_start_trace 方法,必需)'
|
|
299
|
+
},
|
|
300
|
+
insightName: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
description: '性能洞察名称(performance_analyze_insight 方法,必需)'
|
|
303
|
+
},
|
|
304
|
+
insightSetId: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: '性能洞察集合ID(performance_analyze_insight 方法,必需)'
|
|
307
|
+
},
|
|
308
|
+
// 调试参数
|
|
309
|
+
function: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: '要执行的JavaScript函数(evaluate_script 方法)'
|
|
312
|
+
},
|
|
313
|
+
args: {
|
|
314
|
+
type: 'array',
|
|
315
|
+
description: '函数参数(evaluate_script 方法)'
|
|
316
|
+
},
|
|
317
|
+
msgid: {
|
|
318
|
+
type: 'number',
|
|
319
|
+
description: '控制台消息ID(get_console_message 方法)'
|
|
320
|
+
},
|
|
321
|
+
includePreservedMessages: {
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
description: '是否包含保留的消息(list_console_messages 方法)',
|
|
324
|
+
default: false
|
|
325
|
+
},
|
|
326
|
+
types: {
|
|
327
|
+
type: 'array',
|
|
328
|
+
description: '消息类型过滤(list_console_messages 方法)',
|
|
329
|
+
items: { type: 'string', enum: ['error', 'warning', 'info', 'log'] }
|
|
330
|
+
},
|
|
331
|
+
verbose: {
|
|
332
|
+
type: 'boolean',
|
|
333
|
+
description: '是否详细模式(take_snapshot 方法)',
|
|
334
|
+
default: false
|
|
335
|
+
},
|
|
336
|
+
// 截图参数
|
|
337
|
+
format: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: '截图格式(take_screenshot 方法:png/jpeg/webp)',
|
|
340
|
+
enum: ['png', 'jpeg', 'webp'],
|
|
341
|
+
default: 'png'
|
|
342
|
+
},
|
|
343
|
+
fullPage: {
|
|
344
|
+
type: 'boolean',
|
|
345
|
+
description: '是否截取整页(take_screenshot 方法,与 uid 不兼容)',
|
|
346
|
+
default: false
|
|
347
|
+
},
|
|
348
|
+
quality: {
|
|
349
|
+
type: 'number',
|
|
350
|
+
description: '压缩质量(take_screenshot 方法,0-100,仅用于 JPEG 和 WebP)',
|
|
351
|
+
minimum: 0,
|
|
352
|
+
maximum: 100
|
|
353
|
+
},
|
|
354
|
+
// 模拟参数
|
|
355
|
+
device: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: '设备名称(emulate 方法,如 "iPhone 12",注意:官方文档中未包含此参数)'
|
|
358
|
+
},
|
|
359
|
+
cpuThrottlingRate: {
|
|
360
|
+
type: 'number',
|
|
361
|
+
description: 'CPU 降速因子(emulate 方法,设置为 1 禁用降速)'
|
|
362
|
+
},
|
|
363
|
+
networkConditions: {
|
|
364
|
+
type: 'string',
|
|
365
|
+
description: '网络条件(emulate 方法)',
|
|
366
|
+
enum: ['No emulation', 'Offline', 'Slow 3G', 'Fast 3G', 'Slow 4G', 'Fast 4G']
|
|
367
|
+
},
|
|
368
|
+
width: {
|
|
369
|
+
type: 'number',
|
|
370
|
+
description: '视口宽度(resize_page 方法)'
|
|
371
|
+
},
|
|
372
|
+
height: {
|
|
373
|
+
type: 'number',
|
|
374
|
+
description: '视口高度(resize_page 方法)'
|
|
375
|
+
},
|
|
376
|
+
// 通用参数
|
|
377
|
+
keepAlive: {
|
|
378
|
+
type: 'boolean',
|
|
379
|
+
description: '操作完成后是否保持浏览器打开(默认false,操作完成后自动关闭)。设置为true时,浏览器会保持打开状态,可用于连续操作或调试',
|
|
380
|
+
default: false
|
|
381
|
+
},
|
|
382
|
+
timeout: {
|
|
383
|
+
type: 'number',
|
|
384
|
+
description: '超时时间(毫秒)',
|
|
385
|
+
default: 30000
|
|
386
|
+
},
|
|
387
|
+
options: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
description: '操作选项',
|
|
390
|
+
properties: {
|
|
391
|
+
headless: {
|
|
392
|
+
type: 'boolean',
|
|
393
|
+
description: '是否无头模式',
|
|
394
|
+
default: true
|
|
395
|
+
},
|
|
396
|
+
channel: {
|
|
397
|
+
type: 'string',
|
|
398
|
+
enum: ['stable', 'canary', 'beta', 'dev'],
|
|
399
|
+
description: 'Chrome 渠道',
|
|
400
|
+
default: 'stable'
|
|
401
|
+
},
|
|
402
|
+
isolated: {
|
|
403
|
+
type: 'boolean',
|
|
404
|
+
description: '是否使用隔离的用户数据目录',
|
|
405
|
+
default: false
|
|
406
|
+
},
|
|
407
|
+
keepAlive: {
|
|
408
|
+
type: 'boolean',
|
|
409
|
+
description: '操作完成后是否保持浏览器打开',
|
|
410
|
+
default: false
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
required: ['method']
|
|
416
|
+
},
|
|
417
|
+
environment: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
CHROME_HEADLESS: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: '是否无头模式(true/false)',
|
|
423
|
+
default: 'true'
|
|
424
|
+
},
|
|
425
|
+
CHROME_CHANNEL: {
|
|
426
|
+
type: 'string',
|
|
427
|
+
description: 'Chrome 渠道(stable/canary/beta/dev)',
|
|
428
|
+
default: 'stable'
|
|
429
|
+
},
|
|
430
|
+
CHROME_ISOLATED: {
|
|
431
|
+
type: 'string',
|
|
432
|
+
description: '是否使用隔离的用户数据目录(true/false)',
|
|
433
|
+
default: 'false'
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
required: []
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 获取业务错误定义
|
|
443
|
+
*/
|
|
444
|
+
getBusinessErrors() {
|
|
445
|
+
return [
|
|
446
|
+
{
|
|
447
|
+
code: 'BROWSER_LAUNCH_FAILED',
|
|
448
|
+
description: '浏览器启动失败',
|
|
449
|
+
match: /browser.*launch|无法启动浏览器|Browser launch failed/i,
|
|
450
|
+
solution: '检查 Chrome 是否正确安装,尝试重新安装 Chrome',
|
|
451
|
+
retryable: true
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
code: 'NAVIGATION_FAILED',
|
|
455
|
+
description: '页面导航失败',
|
|
456
|
+
match: /navigation.*failed|页面加载失败|Navigation timeout/i,
|
|
457
|
+
solution: '检查URL是否正确,网络是否正常,或增加超时时间',
|
|
458
|
+
retryable: true
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
code: 'ELEMENT_NOT_FOUND',
|
|
462
|
+
description: '元素未找到',
|
|
463
|
+
match: /element.*not found|uid.*not found|No such element|stale snapshot/i,
|
|
464
|
+
solution: '请先调用 take_snapshot 获取页面快照,然后使用快照中的 uid',
|
|
465
|
+
retryable: false
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
code: 'SCREENSHOT_FAILED',
|
|
469
|
+
description: '截图失败',
|
|
470
|
+
match: /screenshot.*failed|无法保存截图/i,
|
|
471
|
+
solution: '检查保存路径是否有效,是否有写入权限',
|
|
472
|
+
retryable: false
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
code: 'NETWORK_ERROR',
|
|
476
|
+
description: '网络错误',
|
|
477
|
+
match: /network.*error|连接失败|ECONNREFUSED|ETIMEDOUT/i,
|
|
478
|
+
solution: '检查网络连接,URL是否可访问',
|
|
479
|
+
retryable: true
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
code: 'INVALID_UID',
|
|
483
|
+
description: '无效的元素标识符',
|
|
484
|
+
match: /invalid.*uid|uid.*invalid|No snapshot found/i,
|
|
485
|
+
solution: '请先调用 take_snapshot 获取页面快照,然后使用快照中的 uid',
|
|
486
|
+
retryable: false
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
code: 'TIMEOUT',
|
|
490
|
+
description: '操作超时',
|
|
491
|
+
match: /timeout|超时/i,
|
|
492
|
+
solution: '增加超时时间或检查操作是否正常',
|
|
493
|
+
retryable: true
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
code: 'PAGE_CLOSED',
|
|
497
|
+
description: '页面已关闭',
|
|
498
|
+
match: /page.*closed|The selected page has been closed/i,
|
|
499
|
+
solution: '页面已被关闭,请调用 list_pages 查看可用页面',
|
|
500
|
+
retryable: false
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
// --------------------------------------------------------------------------
|
|
506
|
+
// 主执行方法
|
|
507
|
+
// --------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 执行工具
|
|
511
|
+
*/
|
|
512
|
+
async execute(params) {
|
|
513
|
+
const { api } = this;
|
|
514
|
+
|
|
515
|
+
api?.logger?.info('Chrome DevTools操作开始', {
|
|
516
|
+
method: params.method,
|
|
517
|
+
url: params.url
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
// 1. 参数验证
|
|
522
|
+
this.validateMethodParams(params);
|
|
523
|
+
|
|
524
|
+
// 2. 导入 chrome-devtools-mcp 模块
|
|
525
|
+
const chromeDevToolsMcp = await this.importToolModule('chrome-devtools-mcp');
|
|
526
|
+
|
|
527
|
+
// 3. 获取或创建 McpContext
|
|
528
|
+
let context;
|
|
529
|
+
try {
|
|
530
|
+
context = await this.getMcpContext(params.options);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// 如果浏览器已关闭,清理管理器并重新创建
|
|
533
|
+
if (error.message && (error.message.includes('Target closed') || error.message.includes('Protocol error'))) {
|
|
534
|
+
api?.logger?.warn('检测到浏览器已关闭,清理并重新创建', { error: error.message });
|
|
535
|
+
await this.cleanupBrowser();
|
|
536
|
+
context = await this.getMcpContext(params.options);
|
|
537
|
+
} else {
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 检查浏览器连接状态
|
|
543
|
+
const manager = getBrowserManager();
|
|
544
|
+
if (manager.browser && !manager.browser.isConnected()) {
|
|
545
|
+
api?.logger?.warn('浏览器连接已断开,重新创建上下文');
|
|
546
|
+
await this.cleanupBrowser();
|
|
547
|
+
context = await this.getMcpContext(params.options);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 4. 处理页面导航后的 snapshot 失效问题
|
|
551
|
+
// 如果执行的是 navigate_page 或 new_page 操作,清除旧的 snapshot(页面已改变)
|
|
552
|
+
if ((params.method === 'navigate_page' && params.url) || params.method === 'new_page') {
|
|
553
|
+
const targetUrl = params.url || (params.method === 'navigate_page' ? params.url : '');
|
|
554
|
+
if (targetUrl) {
|
|
555
|
+
await this.clearSnapshotIfNeeded(context, targetUrl);
|
|
556
|
+
} else if (params.method === 'new_page') {
|
|
557
|
+
// new_page 会创建新页面,清除旧快照
|
|
558
|
+
await this.clearSnapshotIfNeeded(context, 'new_page');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 处理 close 方法
|
|
563
|
+
if (params.method === 'close') {
|
|
564
|
+
await this.cleanupBrowser();
|
|
565
|
+
return {
|
|
566
|
+
success: true,
|
|
567
|
+
method: 'close',
|
|
568
|
+
text: '浏览器已关闭'
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 5. 对于需要 snapshot 的操作,如果 snapshot 不存在,自动创建一个
|
|
573
|
+
// 注意:即使没有提供 uid,我们也应该确保 snapshot 存在,因为后续操作可能需要
|
|
574
|
+
if (this.requiresSnapshot(params.method)) {
|
|
575
|
+
await this.ensureSnapshot(context, chromeDevToolsMcp);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 6. 获取工具 handler
|
|
579
|
+
const tool = await this.getToolHandler(params.method, chromeDevToolsMcp);
|
|
580
|
+
if (!tool) {
|
|
581
|
+
throw new Error(`不支持的方法: ${params.method}。请检查 chrome-devtools-mcp 是否正确安装,或该方法是否存在于工具库中。`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 7. 转换参数格式
|
|
585
|
+
const transformedParams = this.transformParams(params.method, params);
|
|
586
|
+
|
|
587
|
+
// 8. 创建请求和响应对象
|
|
588
|
+
const request = { params: transformedParams };
|
|
589
|
+
const response = this.createMcpResponse();
|
|
590
|
+
|
|
591
|
+
// 9. 调用工具 handler(带重试机制处理 stale snapshot)
|
|
592
|
+
const finalResponse = await this.executeWithRetry(
|
|
593
|
+
tool.handler.bind(tool),
|
|
594
|
+
request,
|
|
595
|
+
response,
|
|
596
|
+
context,
|
|
597
|
+
chromeDevToolsMcp,
|
|
598
|
+
() => this.createMcpResponse(),
|
|
599
|
+
params.options,
|
|
600
|
+
params.method // 传递方法名以便重试时使用
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// 10. 处理响应
|
|
604
|
+
const content = await finalResponse.handle(params.method, context);
|
|
605
|
+
|
|
606
|
+
// 调试:检查截图响应内容
|
|
607
|
+
if (params.method === 'take_screenshot') {
|
|
608
|
+
api?.logger?.debug('截图响应内容', {
|
|
609
|
+
contentLength: content.length,
|
|
610
|
+
contentTypes: content.map(item => item.type),
|
|
611
|
+
hasImages: content.some(item => item.type === 'image'),
|
|
612
|
+
responseImagesCount: response.images.length
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 11. 格式化返回结果
|
|
617
|
+
const result = this.formatResponse(content, params.method);
|
|
618
|
+
|
|
619
|
+
// 12. 根据 keepAlive 决定是否关闭浏览器
|
|
620
|
+
if (params.method !== 'close') {
|
|
621
|
+
const keepAlive = this.getKeepAlive(params);
|
|
622
|
+
if (!keepAlive) {
|
|
623
|
+
api?.logger?.info('操作完成,自动关闭浏览器(keepAlive=false)');
|
|
624
|
+
await this.cleanupBrowser();
|
|
625
|
+
} else {
|
|
626
|
+
api?.logger?.info('操作完成,保持浏览器打开(keepAlive=true)');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
api?.logger?.info('Chrome DevTools操作成功', { method: params.method });
|
|
631
|
+
return result;
|
|
632
|
+
|
|
633
|
+
} catch (error) {
|
|
634
|
+
api?.logger?.error('Chrome DevTools操作失败', {
|
|
635
|
+
method: params.method,
|
|
636
|
+
error: error.message,
|
|
637
|
+
stack: error.stack
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// 错误时也尝试清理浏览器(如果 keepAlive=false)
|
|
641
|
+
if (params.method !== 'close') {
|
|
642
|
+
const keepAlive = this.getKeepAlive(params);
|
|
643
|
+
if (!keepAlive) {
|
|
644
|
+
try {
|
|
645
|
+
api?.logger?.info('操作失败,自动关闭浏览器(keepAlive=false)');
|
|
646
|
+
await this.cleanupBrowser();
|
|
647
|
+
} catch (cleanupError) {
|
|
648
|
+
api?.logger?.warn('清理浏览器时出错', { error: cleanupError.message });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// --------------------------------------------------------------------------
|
|
658
|
+
// 辅助方法
|
|
659
|
+
// --------------------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 获取 keepAlive 参数值
|
|
663
|
+
*/
|
|
664
|
+
getKeepAlive(params) {
|
|
665
|
+
return params.options?.keepAlive !== undefined
|
|
666
|
+
? params.options.keepAlive
|
|
667
|
+
: (params.keepAlive !== undefined ? params.keepAlive : false);
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* 验证方法参数(业务层面)
|
|
672
|
+
*/
|
|
673
|
+
validateMethodParams(params) {
|
|
674
|
+
const methodRequirements = {
|
|
675
|
+
'click': ['uid'],
|
|
676
|
+
'fill': ['uid', 'value'],
|
|
677
|
+
'hover': ['uid'],
|
|
678
|
+
'press_key': ['key'],
|
|
679
|
+
'drag': ['from_uid', 'to_uid'],
|
|
680
|
+
'upload_file': ['uid', 'filePath'],
|
|
681
|
+
'handle_dialog': ['action'],
|
|
682
|
+
'fill_form': ['elements'],
|
|
683
|
+
// navigate_page: url 在 type 为 back/forward/reload 时可选
|
|
684
|
+
'navigate_page': (() => {
|
|
685
|
+
const type = params.type || 'url';
|
|
686
|
+
return ['back', 'forward', 'reload'].includes(type) ? [] : ['url'];
|
|
687
|
+
})(),
|
|
688
|
+
'new_page': ['url'],
|
|
689
|
+
'close_page': ['pageIdx'],
|
|
690
|
+
'select_page': ['pageIdx'],
|
|
691
|
+
'wait_for': ['text'],
|
|
692
|
+
'list_network_requests': [],
|
|
693
|
+
// get_network_request: reqid 可选
|
|
694
|
+
'get_network_request': [],
|
|
695
|
+
// performance_start_trace: autoStop 和 reload 必需
|
|
696
|
+
'performance_start_trace': ['autoStop', 'reload'],
|
|
697
|
+
'performance_stop_trace': [],
|
|
698
|
+
// performance_analyze_insight: insightName 和 insightSetId 必需
|
|
699
|
+
'performance_analyze_insight': ['insightName', 'insightSetId'],
|
|
700
|
+
'evaluate_script': ['function'],
|
|
701
|
+
'get_console_message': ['msgid'],
|
|
702
|
+
'list_console_messages': [],
|
|
703
|
+
'take_screenshot': [],
|
|
704
|
+
'take_snapshot': [],
|
|
705
|
+
// emulate: device 不是官方参数,但保留以兼容;官方参数是 cpuThrottlingRate 和 networkConditions(可选)
|
|
706
|
+
'emulate': [],
|
|
707
|
+
'resize_page': ['width', 'height'],
|
|
708
|
+
'close': []
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const required = methodRequirements[params.method];
|
|
712
|
+
if (!required) {
|
|
713
|
+
throw new Error(`不支持的方法: ${params.method}`);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const missing = required.filter(field => params[field] === undefined || params[field] === null);
|
|
717
|
+
if (missing.length > 0) {
|
|
718
|
+
throw new Error(`方法 ${params.method} 缺少必需参数: ${missing.join(', ')}`);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 获取工具 handler
|
|
724
|
+
*/
|
|
725
|
+
async getToolHandler(method, chromeDevToolsMcp) {
|
|
726
|
+
// 根据方法名确定工具类别
|
|
727
|
+
const toolCategory = this.getToolCategory(method);
|
|
728
|
+
if (!toolCategory) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 获取工具名称(chrome-devtools-mcp 中的实际名称)
|
|
733
|
+
const toolName = this.getToolName(method);
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
// 尝试从 build/src/tools 导入工具模块
|
|
737
|
+
const toolModulePath = `chrome-devtools-mcp/build/src/tools/${toolCategory}.js`;
|
|
738
|
+
const toolModule = await this.importToolModule(toolModulePath);
|
|
739
|
+
|
|
740
|
+
if (toolModule && (toolModule[toolName] || toolModule[method])) {
|
|
741
|
+
return toolModule[toolName] || toolModule[method];
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.api?.logger?.debug('无法从 build/src/tools 导入,尝试其他路径', {
|
|
745
|
+
method,
|
|
746
|
+
error: error.message
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 备用方案:尝试从主模块获取
|
|
751
|
+
try {
|
|
752
|
+
if (chromeDevToolsMcp && chromeDevToolsMcp.tools) {
|
|
753
|
+
const categoryTools = chromeDevToolsMcp.tools[toolCategory];
|
|
754
|
+
if (categoryTools && (categoryTools[toolName] || categoryTools[method])) {
|
|
755
|
+
return categoryTools[toolName] || categoryTools[method];
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
this.api?.logger?.warn('无法从主模块获取工具', {
|
|
760
|
+
method,
|
|
761
|
+
error: error.message
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return null;
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* 获取工具类别
|
|
770
|
+
*/
|
|
771
|
+
getToolCategory(method) {
|
|
772
|
+
const categoryMap = {
|
|
773
|
+
// 输入自动化
|
|
774
|
+
'click': 'input',
|
|
775
|
+
'fill': 'input',
|
|
776
|
+
'hover': 'input',
|
|
777
|
+
'press_key': 'input',
|
|
778
|
+
'drag': 'input',
|
|
779
|
+
'upload_file': 'input',
|
|
780
|
+
'fill_form': 'input',
|
|
781
|
+
// 导航自动化
|
|
782
|
+
'navigate_page': 'pages',
|
|
783
|
+
'new_page': 'pages',
|
|
784
|
+
'close_page': 'pages',
|
|
785
|
+
'list_pages': 'pages',
|
|
786
|
+
'select_page': 'pages',
|
|
787
|
+
'handle_dialog': 'pages', // handle_dialog 在 pages.js 中导出
|
|
788
|
+
'resize_page': 'pages', // resize_page 在 pages.js 中导出
|
|
789
|
+
// 网络监控
|
|
790
|
+
'list_network_requests': 'network',
|
|
791
|
+
'get_network_request': 'network',
|
|
792
|
+
// 性能分析
|
|
793
|
+
'performance_start_trace': 'performance',
|
|
794
|
+
'performance_stop_trace': 'performance',
|
|
795
|
+
'performance_analyze_insight': 'performance',
|
|
796
|
+
// 调试
|
|
797
|
+
'evaluate_script': 'script',
|
|
798
|
+
'get_console_message': 'console',
|
|
799
|
+
'list_console_messages': 'console',
|
|
800
|
+
'take_screenshot': 'screenshot',
|
|
801
|
+
'take_snapshot': 'snapshot',
|
|
802
|
+
'wait_for': 'snapshot', // wait_for 在 snapshot.js 中导出
|
|
803
|
+
// 模拟
|
|
804
|
+
'emulate': 'emulation',
|
|
805
|
+
// 管理
|
|
806
|
+
'close': 'pages' // close 用于关闭浏览器,在 pages 工具中处理
|
|
807
|
+
};
|
|
808
|
+
return categoryMap[method];
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* 获取工具名称(chrome-devtools-mcp 中的实际名称)
|
|
813
|
+
* 注意:现在方法名已经是官方格式(下划线命名),但 chrome-devtools-mcp 内部使用的是驼峰命名
|
|
814
|
+
*/
|
|
815
|
+
getToolName(method) {
|
|
816
|
+
const nameMap = {
|
|
817
|
+
'navigate_page': 'navigatePage',
|
|
818
|
+
'new_page': 'newPage',
|
|
819
|
+
'close_page': 'closePage',
|
|
820
|
+
'list_pages': 'listPages',
|
|
821
|
+
'select_page': 'selectPage',
|
|
822
|
+
'press_key': 'pressKey',
|
|
823
|
+
'upload_file': 'uploadFile',
|
|
824
|
+
'handle_dialog': 'handleDialog',
|
|
825
|
+
'fill_form': 'fillForm',
|
|
826
|
+
'wait_for': 'waitFor',
|
|
827
|
+
'performance_start_trace': 'startTrace', // chrome-devtools-mcp 导出的是 'startTrace'
|
|
828
|
+
'performance_stop_trace': 'stopTrace', // chrome-devtools-mcp 导出的是 'stopTrace'
|
|
829
|
+
'performance_analyze_insight': 'analyzeInsight', // chrome-devtools-mcp 导出的是 'analyzeInsight'
|
|
830
|
+
'evaluate_script': 'evaluateScript',
|
|
831
|
+
'get_console_message': 'getConsoleMessage',
|
|
832
|
+
'list_console_messages': 'listConsoleMessages',
|
|
833
|
+
'take_screenshot': 'screenshot', // chrome-devtools-mcp 导出的是 'screenshot'
|
|
834
|
+
'take_snapshot': 'takeSnapshot', // chrome-devtools-mcp 导出的是 'takeSnapshot'
|
|
835
|
+
'get_network_request': 'getNetworkRequest',
|
|
836
|
+
'list_network_requests': 'listNetworkRequests',
|
|
837
|
+
'resize_page': 'resizePage',
|
|
838
|
+
'close': 'close' // close 方法直接使用,用于清理浏览器
|
|
839
|
+
};
|
|
840
|
+
return nameMap[method] || method;
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* 获取或创建 McpContext
|
|
846
|
+
*/
|
|
847
|
+
async getMcpContext(options = {}) {
|
|
848
|
+
const { api } = this;
|
|
849
|
+
const manager = getBrowserManager();
|
|
850
|
+
|
|
851
|
+
// 如果已有上下文且浏览器仍然连接,直接返回
|
|
852
|
+
if (manager.context && manager.context.browser && manager.context.browser.isConnected()) {
|
|
853
|
+
api?.logger?.info('复用已存在的浏览器上下文', {
|
|
854
|
+
note: '使用之前创建的上下文,保持会话状态(跨执行保持)'
|
|
855
|
+
});
|
|
856
|
+
manager.lastUsed = Date.now();
|
|
857
|
+
return manager.context;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 导入 chrome-devtools-mcp 的浏览器和上下文模块
|
|
861
|
+
const chromeDevToolsMcp = await this.importToolModule('chrome-devtools-mcp');
|
|
862
|
+
|
|
863
|
+
// 获取浏览器启动选项
|
|
864
|
+
const headless = options.headless !== undefined
|
|
865
|
+
? options.headless
|
|
866
|
+
: (api.environment.get('CHROME_HEADLESS') === 'true');
|
|
867
|
+
const channel = options.channel || api.environment.get('CHROME_CHANNEL') || 'stable';
|
|
868
|
+
const isolated = options.isolated !== undefined
|
|
869
|
+
? options.isolated
|
|
870
|
+
: (api.environment.get('CHROME_ISOLATED') === 'true');
|
|
871
|
+
|
|
872
|
+
api?.logger?.info('创建新的浏览器实例', {
|
|
873
|
+
headless,
|
|
874
|
+
channel,
|
|
875
|
+
isolated,
|
|
876
|
+
note: '首次创建浏览器实例'
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// 导入浏览器启动函数和上下文模块
|
|
880
|
+
const browserModule = await this.importBrowserModule(chromeDevToolsMcp);
|
|
881
|
+
const contextModule = await this.importContextModule(chromeDevToolsMcp);
|
|
882
|
+
|
|
883
|
+
const { ensureBrowserLaunched } = browserModule;
|
|
884
|
+
const { McpContext } = contextModule;
|
|
885
|
+
|
|
886
|
+
// 启动浏览器
|
|
887
|
+
const browser = await ensureBrowserLaunched({
|
|
888
|
+
headless,
|
|
889
|
+
channel,
|
|
890
|
+
isolated,
|
|
891
|
+
devtools: false
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// 创建上下文
|
|
895
|
+
const context = await McpContext.from(browser, (msg) => api?.logger?.debug(msg), {
|
|
896
|
+
experimentalDevToolsDebugging: false,
|
|
897
|
+
experimentalIncludeAllPages: false
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// 存储到全局管理器
|
|
901
|
+
manager.browser = browser;
|
|
902
|
+
manager.context = context;
|
|
903
|
+
manager.lastUsed = Date.now();
|
|
904
|
+
|
|
905
|
+
return context;
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* 导入浏览器模块
|
|
910
|
+
*/
|
|
911
|
+
async importBrowserModule(chromeDevToolsMcp) {
|
|
912
|
+
try {
|
|
913
|
+
// 尝试从 build/src/browser.js 导入
|
|
914
|
+
return await this.importToolModule('chrome-devtools-mcp/build/src/browser.js');
|
|
915
|
+
} catch {
|
|
916
|
+
// 如果失败,尝试从主模块获取
|
|
917
|
+
if (chromeDevToolsMcp && chromeDevToolsMcp.browser) {
|
|
918
|
+
return chromeDevToolsMcp.browser;
|
|
919
|
+
}
|
|
920
|
+
throw new Error('无法导入浏览器模块,请检查 chrome-devtools-mcp 是否正确安装');
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* 导入上下文模块
|
|
926
|
+
*/
|
|
927
|
+
async importContextModule(chromeDevToolsMcp) {
|
|
928
|
+
try {
|
|
929
|
+
// 尝试从 build/src/McpContext.js 导入
|
|
930
|
+
return await this.importToolModule('chrome-devtools-mcp/build/src/McpContext.js');
|
|
931
|
+
} catch {
|
|
932
|
+
// 如果失败,尝试从主模块获取
|
|
933
|
+
if (chromeDevToolsMcp && chromeDevToolsMcp.McpContext) {
|
|
934
|
+
return { McpContext: chromeDevToolsMcp.McpContext };
|
|
935
|
+
}
|
|
936
|
+
throw new Error('无法导入上下文模块,请检查 chrome-devtools-mcp 是否正确安装');
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* 转换参数格式
|
|
942
|
+
*/
|
|
943
|
+
transformParams(method, params) {
|
|
944
|
+
const transformed = {};
|
|
945
|
+
|
|
946
|
+
switch (method) {
|
|
947
|
+
case 'click':
|
|
948
|
+
transformed.uid = params.uid;
|
|
949
|
+
transformed.dblClick = params.dblClick ?? false;
|
|
950
|
+
break;
|
|
951
|
+
case 'fill':
|
|
952
|
+
transformed.uid = params.uid;
|
|
953
|
+
transformed.value = params.value;
|
|
954
|
+
break;
|
|
955
|
+
case 'hover':
|
|
956
|
+
transformed.uid = params.uid;
|
|
957
|
+
break;
|
|
958
|
+
case 'press_key':
|
|
959
|
+
transformed.key = params.key;
|
|
960
|
+
break;
|
|
961
|
+
case 'drag':
|
|
962
|
+
transformed.from_uid = params.from_uid;
|
|
963
|
+
transformed.to_uid = params.to_uid;
|
|
964
|
+
break;
|
|
965
|
+
case 'upload_file':
|
|
966
|
+
transformed.uid = params.uid;
|
|
967
|
+
transformed.filePath = params.filePath;
|
|
968
|
+
break;
|
|
969
|
+
case 'handle_dialog':
|
|
970
|
+
transformed.action = params.action;
|
|
971
|
+
if (params.promptText) {
|
|
972
|
+
transformed.promptText = params.promptText;
|
|
973
|
+
}
|
|
974
|
+
break;
|
|
975
|
+
case 'fill_form':
|
|
976
|
+
transformed.elements = params.elements;
|
|
977
|
+
break;
|
|
978
|
+
case 'navigate_page':
|
|
979
|
+
transformed.type = params.type || 'url';
|
|
980
|
+
if (params.url) {
|
|
981
|
+
transformed.url = params.url;
|
|
982
|
+
}
|
|
983
|
+
if (params.timeout) {
|
|
984
|
+
transformed.timeout = params.timeout;
|
|
985
|
+
}
|
|
986
|
+
if (params.ignoreCache !== undefined) {
|
|
987
|
+
transformed.ignoreCache = params.ignoreCache;
|
|
988
|
+
}
|
|
989
|
+
break;
|
|
990
|
+
case 'new_page':
|
|
991
|
+
transformed.url = params.url;
|
|
992
|
+
if (params.timeout) {
|
|
993
|
+
transformed.timeout = params.timeout;
|
|
994
|
+
}
|
|
995
|
+
break;
|
|
996
|
+
case 'close_page':
|
|
997
|
+
transformed.pageIdx = params.pageIdx;
|
|
998
|
+
break;
|
|
999
|
+
case 'select_page':
|
|
1000
|
+
transformed.pageIdx = params.pageIdx;
|
|
1001
|
+
break;
|
|
1002
|
+
case 'close':
|
|
1003
|
+
// close 方法不需要参数,用于清理浏览器实例
|
|
1004
|
+
break;
|
|
1005
|
+
case 'wait_for':
|
|
1006
|
+
transformed.text = params.text;
|
|
1007
|
+
if (params.timeout) {
|
|
1008
|
+
transformed.timeout = params.timeout;
|
|
1009
|
+
}
|
|
1010
|
+
break;
|
|
1011
|
+
case 'list_network_requests':
|
|
1012
|
+
if (params.includePreservedRequests !== undefined) {
|
|
1013
|
+
transformed.includePreservedRequests = params.includePreservedRequests;
|
|
1014
|
+
}
|
|
1015
|
+
if (params.resourceTypes) {
|
|
1016
|
+
transformed.resourceTypes = params.resourceTypes;
|
|
1017
|
+
}
|
|
1018
|
+
if (params.pageIdx !== undefined) {
|
|
1019
|
+
transformed.pageIdx = params.pageIdx;
|
|
1020
|
+
}
|
|
1021
|
+
if (params.pageSize !== undefined) {
|
|
1022
|
+
transformed.pageSize = params.pageSize;
|
|
1023
|
+
}
|
|
1024
|
+
break;
|
|
1025
|
+
case 'get_network_request':
|
|
1026
|
+
// reqid 可选,如果提供则传递
|
|
1027
|
+
if (params.reqid !== undefined) {
|
|
1028
|
+
transformed.reqid = params.reqid;
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
case 'performance_start_trace':
|
|
1032
|
+
if (params.reload !== undefined) {
|
|
1033
|
+
transformed.reload = params.reload;
|
|
1034
|
+
}
|
|
1035
|
+
if (params.autoStop !== undefined) {
|
|
1036
|
+
transformed.autoStop = params.autoStop;
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
case 'performance_analyze_insight':
|
|
1040
|
+
transformed.insightName = params.insightName;
|
|
1041
|
+
transformed.insightSetId = params.insightSetId;
|
|
1042
|
+
break;
|
|
1043
|
+
case 'evaluate_script':
|
|
1044
|
+
// chrome-devtools-mcp 期望的是一个函数字符串,它会用 (${function}) 包装
|
|
1045
|
+
// 如果用户传入的是立即执行的函数表达式,需要转换为函数声明
|
|
1046
|
+
let functionStr = params.function;
|
|
1047
|
+
|
|
1048
|
+
// 检查是否是立即执行的函数表达式 (function() {...})() 或 (() => {...})()
|
|
1049
|
+
functionStr = functionStr.trim();
|
|
1050
|
+
|
|
1051
|
+
// 匹配立即执行函数:以 ( 开头,以 )() 结尾
|
|
1052
|
+
if (functionStr.startsWith('(') && functionStr.endsWith(')()')) {
|
|
1053
|
+
// 移除开头的 ( 和结尾的 ()
|
|
1054
|
+
functionStr = functionStr.slice(1, -2);
|
|
1055
|
+
} else if (functionStr.endsWith('()')) {
|
|
1056
|
+
// 只移除结尾的 ()
|
|
1057
|
+
functionStr = functionStr.slice(0, -2);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// 确保结果是有效的函数表达式
|
|
1061
|
+
// chrome-devtools-mcp 会用 (${function}) 包装,所以我们需要确保传入的是函数体
|
|
1062
|
+
// 基本语法检查:确保括号匹配
|
|
1063
|
+
const openParens = (functionStr.match(/\(/g) || []).length;
|
|
1064
|
+
const closeParens = (functionStr.match(/\)/g) || []).length;
|
|
1065
|
+
if (openParens !== closeParens) {
|
|
1066
|
+
api?.logger?.warn('函数括号不匹配,可能导致语法错误', {
|
|
1067
|
+
openParens,
|
|
1068
|
+
closeParens,
|
|
1069
|
+
functionPreview: functionStr.substring(0, 100)
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
transformed.function = functionStr;
|
|
1074
|
+
if (params.args) {
|
|
1075
|
+
transformed.args = params.args;
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
case 'get_console_message':
|
|
1079
|
+
transformed.msgid = params.msgid;
|
|
1080
|
+
break;
|
|
1081
|
+
case 'list_console_messages':
|
|
1082
|
+
if (params.includePreservedMessages !== undefined) {
|
|
1083
|
+
transformed.includePreservedMessages = params.includePreservedMessages;
|
|
1084
|
+
}
|
|
1085
|
+
if (params.types) {
|
|
1086
|
+
transformed.types = params.types;
|
|
1087
|
+
}
|
|
1088
|
+
if (params.pageIdx !== undefined) {
|
|
1089
|
+
transformed.pageIdx = params.pageIdx;
|
|
1090
|
+
}
|
|
1091
|
+
if (params.pageSize !== undefined) {
|
|
1092
|
+
transformed.pageSize = params.pageSize;
|
|
1093
|
+
}
|
|
1094
|
+
break;
|
|
1095
|
+
case 'take_screenshot':
|
|
1096
|
+
if (params.filePath) {
|
|
1097
|
+
transformed.filePath = params.filePath;
|
|
1098
|
+
}
|
|
1099
|
+
if (params.format) {
|
|
1100
|
+
transformed.format = params.format;
|
|
1101
|
+
}
|
|
1102
|
+
if (params.fullPage !== undefined) {
|
|
1103
|
+
transformed.fullPage = params.fullPage;
|
|
1104
|
+
}
|
|
1105
|
+
if (params.quality !== undefined) {
|
|
1106
|
+
transformed.quality = params.quality;
|
|
1107
|
+
}
|
|
1108
|
+
if (params.uid) {
|
|
1109
|
+
transformed.uid = params.uid;
|
|
1110
|
+
}
|
|
1111
|
+
break;
|
|
1112
|
+
case 'take_snapshot':
|
|
1113
|
+
if (params.verbose !== undefined) {
|
|
1114
|
+
transformed.verbose = params.verbose;
|
|
1115
|
+
}
|
|
1116
|
+
if (params.filePath) {
|
|
1117
|
+
transformed.filePath = params.filePath;
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
case 'emulate':
|
|
1121
|
+
// 保留 device 参数以兼容,但优先使用官方参数
|
|
1122
|
+
if (params.device) {
|
|
1123
|
+
transformed.device = params.device;
|
|
1124
|
+
}
|
|
1125
|
+
if (params.cpuThrottlingRate !== undefined) {
|
|
1126
|
+
transformed.cpuThrottlingRate = params.cpuThrottlingRate;
|
|
1127
|
+
}
|
|
1128
|
+
if (params.networkConditions) {
|
|
1129
|
+
transformed.networkConditions = params.networkConditions;
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
case 'resize_page':
|
|
1133
|
+
transformed.width = params.width;
|
|
1134
|
+
transformed.height = params.height;
|
|
1135
|
+
break;
|
|
1136
|
+
default:
|
|
1137
|
+
// 对于其他方法,直接传递所有参数
|
|
1138
|
+
Object.assign(transformed, params);
|
|
1139
|
+
delete transformed.method;
|
|
1140
|
+
delete transformed.keepAlive;
|
|
1141
|
+
delete transformed.options;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return transformed;
|
|
1145
|
+
},
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* 创建 McpResponse 对象
|
|
1149
|
+
*/
|
|
1150
|
+
createMcpResponse() {
|
|
1151
|
+
// 创建一个简单的响应对象,模拟 chrome-devtools-mcp 的 McpResponse
|
|
1152
|
+
const response = {
|
|
1153
|
+
lines: [],
|
|
1154
|
+
includePages: false,
|
|
1155
|
+
includeNetworkRequests: false,
|
|
1156
|
+
includeConsoleData: false,
|
|
1157
|
+
snapshotParams: null,
|
|
1158
|
+
images: [],
|
|
1159
|
+
networkRequestIds: [],
|
|
1160
|
+
consoleMessageIds: [],
|
|
1161
|
+
devToolsData: null,
|
|
1162
|
+
|
|
1163
|
+
appendResponseLine(value) {
|
|
1164
|
+
this.lines.push(value);
|
|
1165
|
+
},
|
|
1166
|
+
|
|
1167
|
+
setIncludePages(value) {
|
|
1168
|
+
this.includePages = value;
|
|
1169
|
+
},
|
|
1170
|
+
|
|
1171
|
+
setIncludeNetworkRequests(value, options) {
|
|
1172
|
+
this.includeNetworkRequests = true;
|
|
1173
|
+
this.networkRequestOptions = options;
|
|
1174
|
+
},
|
|
1175
|
+
|
|
1176
|
+
setIncludeConsoleData(value, options) {
|
|
1177
|
+
this.includeConsoleData = true;
|
|
1178
|
+
this.consoleDataOptions = options;
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
includeSnapshot(params) {
|
|
1182
|
+
this.snapshotParams = params || {};
|
|
1183
|
+
},
|
|
1184
|
+
|
|
1185
|
+
attachImage(value) {
|
|
1186
|
+
this.images.push(value);
|
|
1187
|
+
},
|
|
1188
|
+
|
|
1189
|
+
attachNetworkRequest(reqid) {
|
|
1190
|
+
this.networkRequestIds.push(reqid);
|
|
1191
|
+
},
|
|
1192
|
+
|
|
1193
|
+
attachConsoleMessage(msgid) {
|
|
1194
|
+
this.consoleMessageIds.push(msgid);
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
attachDevToolsData(data) {
|
|
1198
|
+
this.devToolsData = data;
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1201
|
+
// 处理响应并返回内容
|
|
1202
|
+
async handle(toolName, context) {
|
|
1203
|
+
const content = [];
|
|
1204
|
+
|
|
1205
|
+
// 添加响应行
|
|
1206
|
+
for (const line of this.lines) {
|
|
1207
|
+
content.push({
|
|
1208
|
+
type: 'text',
|
|
1209
|
+
text: line
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// 包含页面列表
|
|
1214
|
+
if (this.includePages) {
|
|
1215
|
+
const pages = context.getPages();
|
|
1216
|
+
const selectedPage = context.getSelectedPage();
|
|
1217
|
+
let pagesText = 'Pages:\n';
|
|
1218
|
+
pages.forEach((page, idx) => {
|
|
1219
|
+
const isSelected = context.isPageSelected(page);
|
|
1220
|
+
const title = page.title() || page.url();
|
|
1221
|
+
pagesText += `${idx}: ${title}${isSelected ? ' (selected)' : ''}\n`;
|
|
1222
|
+
});
|
|
1223
|
+
content.push({
|
|
1224
|
+
type: 'text',
|
|
1225
|
+
text: pagesText.trim()
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// 包含网络请求
|
|
1230
|
+
if (this.includeNetworkRequests) {
|
|
1231
|
+
const requests = context.getNetworkRequests(
|
|
1232
|
+
this.networkRequestOptions?.includePreservedRequests
|
|
1233
|
+
);
|
|
1234
|
+
let requestsText = 'Network Requests:\n';
|
|
1235
|
+
requests.forEach((request, idx) => {
|
|
1236
|
+
const reqid = context.getNetworkRequestStableId(request);
|
|
1237
|
+
const method = request.method();
|
|
1238
|
+
const url = request.url();
|
|
1239
|
+
const status = request.response()?.status() || 'pending';
|
|
1240
|
+
requestsText += `[reqid=${reqid}] ${method} ${url} (${status})\n`;
|
|
1241
|
+
});
|
|
1242
|
+
if (requests.length > 0) {
|
|
1243
|
+
content.push({
|
|
1244
|
+
type: 'text',
|
|
1245
|
+
text: requestsText.trim()
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// 包含控制台数据
|
|
1251
|
+
if (this.includeConsoleData) {
|
|
1252
|
+
const messages = context.getConsoleData(
|
|
1253
|
+
this.consoleDataOptions?.includePreservedMessages
|
|
1254
|
+
);
|
|
1255
|
+
let messagesText = 'Console Messages:\n';
|
|
1256
|
+
messages.forEach((message) => {
|
|
1257
|
+
const msgid = context.getConsoleMessageStableId(message);
|
|
1258
|
+
const level = message.level || 'log';
|
|
1259
|
+
const text = message.text || message.message || String(message);
|
|
1260
|
+
messagesText += `[msgid=${msgid}] ${level}: ${text}\n`;
|
|
1261
|
+
});
|
|
1262
|
+
if (messages.length > 0) {
|
|
1263
|
+
content.push({
|
|
1264
|
+
type: 'text',
|
|
1265
|
+
text: messagesText.trim()
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// 包含快照
|
|
1271
|
+
if (this.snapshotParams !== null) {
|
|
1272
|
+
await context.createTextSnapshot(
|
|
1273
|
+
this.snapshotParams.verbose || false,
|
|
1274
|
+
this.devToolsData
|
|
1275
|
+
);
|
|
1276
|
+
const snapshot = context.getTextSnapshot();
|
|
1277
|
+
if (snapshot) {
|
|
1278
|
+
content.push({
|
|
1279
|
+
type: 'text',
|
|
1280
|
+
text: this.formatSnapshot(snapshot)
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// 附加图片
|
|
1286
|
+
for (const image of this.images) {
|
|
1287
|
+
content.push({
|
|
1288
|
+
type: 'image',
|
|
1289
|
+
data: image.data,
|
|
1290
|
+
mimeType: normalizeImageMimeType(image.mimeType)
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// 附加网络请求详情
|
|
1295
|
+
for (const reqid of this.networkRequestIds) {
|
|
1296
|
+
const request = context.getNetworkRequestById(reqid);
|
|
1297
|
+
if (request) {
|
|
1298
|
+
content.push({
|
|
1299
|
+
type: 'text',
|
|
1300
|
+
text: this.formatNetworkRequest(request)
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// 附加控制台消息详情
|
|
1306
|
+
for (const msgid of this.consoleMessageIds) {
|
|
1307
|
+
const message = context.getConsoleMessageById(msgid);
|
|
1308
|
+
if (message) {
|
|
1309
|
+
content.push({
|
|
1310
|
+
type: 'text',
|
|
1311
|
+
text: this.formatConsoleMessage(message)
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
return content;
|
|
1317
|
+
},
|
|
1318
|
+
|
|
1319
|
+
// 格式化快照
|
|
1320
|
+
formatSnapshot(snapshot) {
|
|
1321
|
+
if (!snapshot || !snapshot.root) {
|
|
1322
|
+
return 'No snapshot available';
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
let text = 'Page Snapshot:\n';
|
|
1326
|
+
const formatNode = (node, indent = 0) => {
|
|
1327
|
+
const prefix = ' '.repeat(indent);
|
|
1328
|
+
const role = node.role || 'unknown';
|
|
1329
|
+
const name = node.name || '';
|
|
1330
|
+
const value = node.value || '';
|
|
1331
|
+
text += `${prefix}[${node.id}] ${role}${name ? `: ${name}` : ''}${value ? ` = ${value}` : ''}\n`;
|
|
1332
|
+
if (node.children) {
|
|
1333
|
+
node.children.forEach(child => formatNode(child, indent + 1));
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
formatNode(snapshot.root);
|
|
1338
|
+
return text;
|
|
1339
|
+
},
|
|
1340
|
+
|
|
1341
|
+
// 格式化网络请求
|
|
1342
|
+
formatNetworkRequest(request) {
|
|
1343
|
+
const method = request.method();
|
|
1344
|
+
const url = request.url();
|
|
1345
|
+
const status = request.response()?.status() || 'pending';
|
|
1346
|
+
const headers = request.headers();
|
|
1347
|
+
const postData = request.postData();
|
|
1348
|
+
|
|
1349
|
+
let text = `Network Request:\n`;
|
|
1350
|
+
text += ` Method: ${method}\n`;
|
|
1351
|
+
text += ` URL: ${url}\n`;
|
|
1352
|
+
text += ` Status: ${status}\n`;
|
|
1353
|
+
if (headers) {
|
|
1354
|
+
text += ` Headers: ${JSON.stringify(headers, null, 2)}\n`;
|
|
1355
|
+
}
|
|
1356
|
+
if (postData) {
|
|
1357
|
+
text += ` Post Data: ${postData}\n`;
|
|
1358
|
+
}
|
|
1359
|
+
return text;
|
|
1360
|
+
},
|
|
1361
|
+
|
|
1362
|
+
// 格式化控制台消息
|
|
1363
|
+
formatConsoleMessage(message) {
|
|
1364
|
+
const level = message.level || 'log';
|
|
1365
|
+
const text = message.text || message.message || String(message);
|
|
1366
|
+
const location = message.location || {};
|
|
1367
|
+
const stack = message.stack || '';
|
|
1368
|
+
|
|
1369
|
+
let result = `Console Message [${level}]:\n`;
|
|
1370
|
+
result += ` Text: ${text}\n`;
|
|
1371
|
+
if (location.url) {
|
|
1372
|
+
result += ` Location: ${location.url}:${location.lineNumber || '?'}\n`;
|
|
1373
|
+
}
|
|
1374
|
+
if (stack) {
|
|
1375
|
+
result += ` Stack: ${stack}\n`;
|
|
1376
|
+
}
|
|
1377
|
+
return result;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
return response;
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* 格式化响应结果
|
|
1386
|
+
*/
|
|
1387
|
+
formatResponse(content, method) {
|
|
1388
|
+
const { api } = this;
|
|
1389
|
+
|
|
1390
|
+
if (!content || content.length === 0) {
|
|
1391
|
+
return { success: true, method };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// 提取文本内容
|
|
1395
|
+
const texts = content
|
|
1396
|
+
.filter(item => item.type === 'text')
|
|
1397
|
+
.map(item => item.text)
|
|
1398
|
+
.join('\n');
|
|
1399
|
+
|
|
1400
|
+
// 提取图片
|
|
1401
|
+
let images = content
|
|
1402
|
+
.filter(item => item.type === 'image')
|
|
1403
|
+
.map(item => ({
|
|
1404
|
+
data: item.data,
|
|
1405
|
+
mimeType: normalizeImageMimeType(item.mimeType)
|
|
1406
|
+
}));
|
|
1407
|
+
|
|
1408
|
+
// 对于截图方法,如果响应中没有图片,尝试从文本中提取 base64 图片数据
|
|
1409
|
+
if (method === 'take_screenshot' && images.length === 0 && texts) {
|
|
1410
|
+
// 尝试匹配 base64 图片数据(data:image/...;base64,... 或纯 base64 字符串)
|
|
1411
|
+
const base64Pattern = /data:image\/([^;]+);base64,([A-Za-z0-9+/=]+)/g;
|
|
1412
|
+
const matches = [...texts.matchAll(base64Pattern)];
|
|
1413
|
+
|
|
1414
|
+
if (matches.length > 0) {
|
|
1415
|
+
api?.logger?.debug('从文本中提取 base64 图片数据', { matchesCount: matches.length });
|
|
1416
|
+
images = matches.map(match => ({
|
|
1417
|
+
data: match[2], // base64 数据部分
|
|
1418
|
+
mimeType: `image/${match[1]}` // 图片类型
|
|
1419
|
+
}));
|
|
1420
|
+
} else {
|
|
1421
|
+
// 尝试匹配纯 base64 字符串(可能是截图工具返回的)
|
|
1422
|
+
const pureBase64Pattern = /^[A-Za-z0-9+/=]{100,}$/m; // 至少100个字符的 base64 字符串
|
|
1423
|
+
const base64Match = texts.match(pureBase64Pattern);
|
|
1424
|
+
if (base64Match) {
|
|
1425
|
+
api?.logger?.debug('从文本中提取纯 base64 图片数据');
|
|
1426
|
+
images = [{
|
|
1427
|
+
data: base64Match[0],
|
|
1428
|
+
mimeType: 'image/png' // 默认 PNG
|
|
1429
|
+
}];
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// 调试:检查截图结果
|
|
1435
|
+
if (method === 'take_screenshot') {
|
|
1436
|
+
api?.logger?.debug('格式化截图响应', {
|
|
1437
|
+
imagesCount: images.length,
|
|
1438
|
+
hasImages: images.length > 0,
|
|
1439
|
+
contentItems: content.map(item => ({ type: item.type, hasData: !!item.data })),
|
|
1440
|
+
extractedFromText: images.length > 0 && content.filter(item => item.type === 'image').length === 0
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const result = {
|
|
1445
|
+
success: true,
|
|
1446
|
+
method,
|
|
1447
|
+
text: texts || undefined,
|
|
1448
|
+
images: images.length > 0 ? images : undefined
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// 对于特定方法,提取结构化数据
|
|
1452
|
+
if (method === 'list_pages') {
|
|
1453
|
+
result.pages = this.extractPagesFromText(texts);
|
|
1454
|
+
} else if (method === 'list_network_requests') {
|
|
1455
|
+
result.requests = this.extractRequestsFromText(texts);
|
|
1456
|
+
} else if (method === 'list_console_messages') {
|
|
1457
|
+
result.messages = this.extractMessagesFromText(texts);
|
|
1458
|
+
} else if (method === 'take_snapshot') {
|
|
1459
|
+
result.snapshot = texts;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return result;
|
|
1463
|
+
},
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* 从文本中提取页面信息
|
|
1467
|
+
*/
|
|
1468
|
+
extractPagesFromText(text) {
|
|
1469
|
+
const pages = [];
|
|
1470
|
+
const lines = text.split('\n');
|
|
1471
|
+
for (const line of lines) {
|
|
1472
|
+
const match = line.match(/^(\d+):\s+(.+?)(\s+\(selected\))?$/);
|
|
1473
|
+
if (match) {
|
|
1474
|
+
pages.push({
|
|
1475
|
+
index: parseInt(match[1], 10),
|
|
1476
|
+
title: match[2],
|
|
1477
|
+
selected: !!match[3]
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return pages;
|
|
1482
|
+
},
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* 从文本中提取网络请求信息
|
|
1486
|
+
*/
|
|
1487
|
+
extractRequestsFromText(text) {
|
|
1488
|
+
const requests = [];
|
|
1489
|
+
const lines = text.split('\n');
|
|
1490
|
+
for (const line of lines) {
|
|
1491
|
+
const match = line.match(/^\[reqid=(\d+)\]\s+(\w+)\s+(.+?)\s+\((\d+|pending)\)$/);
|
|
1492
|
+
if (match) {
|
|
1493
|
+
requests.push({
|
|
1494
|
+
reqid: parseInt(match[1], 10),
|
|
1495
|
+
method: match[2],
|
|
1496
|
+
url: match[3],
|
|
1497
|
+
status: match[4] === 'pending' ? null : parseInt(match[4], 10)
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return requests;
|
|
1502
|
+
},
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* 从文本中提取控制台消息信息
|
|
1506
|
+
*/
|
|
1507
|
+
extractMessagesFromText(text) {
|
|
1508
|
+
const messages = [];
|
|
1509
|
+
const lines = text.split('\n');
|
|
1510
|
+
for (const line of lines) {
|
|
1511
|
+
const match = line.match(/^\[msgid=(\d+)\]\s+(\w+):\s+(.+)$/);
|
|
1512
|
+
if (match) {
|
|
1513
|
+
messages.push({
|
|
1514
|
+
msgid: parseInt(match[1], 10),
|
|
1515
|
+
level: match[2],
|
|
1516
|
+
text: match[3]
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return messages;
|
|
1521
|
+
},
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* 检查操作是否需要 snapshot
|
|
1525
|
+
*/
|
|
1526
|
+
requiresSnapshot(method) {
|
|
1527
|
+
const methodsRequiringSnapshot = ['click', 'fill', 'hover', 'drag', 'upload_file'];
|
|
1528
|
+
return methodsRequiringSnapshot.includes(method);
|
|
1529
|
+
},
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* 确保 snapshot 存在(如果不存在则创建)
|
|
1533
|
+
*/
|
|
1534
|
+
async ensureSnapshot(context, chromeDevToolsMcp) {
|
|
1535
|
+
const { api } = this;
|
|
1536
|
+
|
|
1537
|
+
try {
|
|
1538
|
+
// 优先通过工具 handler 创建 snapshot,这是最可靠的方式
|
|
1539
|
+
api?.logger?.debug('通过工具 handler 创建 snapshot');
|
|
1540
|
+
const snapshotTool = await this.getToolHandler('take_snapshot', chromeDevToolsMcp);
|
|
1541
|
+
if (snapshotTool) {
|
|
1542
|
+
const request = { params: { verbose: false } };
|
|
1543
|
+
const response = this.createMcpResponse();
|
|
1544
|
+
response.includeSnapshot({ verbose: false });
|
|
1545
|
+
await snapshotTool.handler(request, response, context);
|
|
1546
|
+
// 处理响应以创建 snapshot
|
|
1547
|
+
await response.handle('take_snapshot', context);
|
|
1548
|
+
api?.logger?.debug('Snapshot 创建成功');
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// 备用方案:如果 context 有 createTextSnapshot 方法,使用它
|
|
1553
|
+
if (context.createTextSnapshot && typeof context.createTextSnapshot === 'function') {
|
|
1554
|
+
await context.createTextSnapshot(false, null);
|
|
1555
|
+
api?.logger?.debug('通过 createTextSnapshot 创建 snapshot');
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
api?.logger?.warn('无法创建 snapshot:找不到可用的方法');
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
// 如果创建 snapshot 失败,记录警告但不抛出错误
|
|
1562
|
+
// 让原始操作继续执行,chrome-devtools-mcp 可能会在需要时自动创建
|
|
1563
|
+
api?.logger?.warn('自动创建 snapshot 失败,将继续执行操作', { error: error.message });
|
|
1564
|
+
}
|
|
1565
|
+
},
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* 执行工具 handler,带重试机制处理 stale snapshot 错误
|
|
1569
|
+
*
|
|
1570
|
+
* @param {Function} handler - 工具 handler 函数
|
|
1571
|
+
* @param {object} request - 请求对象
|
|
1572
|
+
* @param {object} response - 响应对象
|
|
1573
|
+
* @param {object} context - MCP 上下文
|
|
1574
|
+
* @param {object} chromeDevToolsMcp - chrome-devtools-mcp 模块
|
|
1575
|
+
* @param {Function} createResponse - 创建新响应对象的函数
|
|
1576
|
+
* @param {object} options - 浏览器选项
|
|
1577
|
+
* @param {string} methodName - 方法名,用于重试时判断是否需要 snapshot
|
|
1578
|
+
* @returns {object} 最终的响应对象
|
|
1579
|
+
*/
|
|
1580
|
+
async executeWithRetry(handler, request, response, context, chromeDevToolsMcp, createResponse, options = {}, methodName = '') {
|
|
1581
|
+
const { api } = this;
|
|
1582
|
+
const maxRetries = 2; // 增加重试次数
|
|
1583
|
+
let retryCount = 0;
|
|
1584
|
+
let currentResponse = response;
|
|
1585
|
+
|
|
1586
|
+
while (retryCount <= maxRetries) {
|
|
1587
|
+
try {
|
|
1588
|
+
await handler(request, currentResponse, context);
|
|
1589
|
+
return currentResponse; // 成功则返回响应对象
|
|
1590
|
+
} catch (handlerError) {
|
|
1591
|
+
const errorMessage = handlerError.message || '';
|
|
1592
|
+
|
|
1593
|
+
// 检查是否是 stale snapshot 错误
|
|
1594
|
+
if (isStaleSnapshotError(handlerError) && retryCount < maxRetries) {
|
|
1595
|
+
api?.logger?.warn('检测到 stale snapshot 错误,重新创建 snapshot 后重试', {
|
|
1596
|
+
error: errorMessage,
|
|
1597
|
+
retryCount: retryCount + 1
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// 重新创建 snapshot
|
|
1601
|
+
await this.ensureSnapshot(context, chromeDevToolsMcp);
|
|
1602
|
+
|
|
1603
|
+
// 重新创建响应对象
|
|
1604
|
+
currentResponse = createResponse();
|
|
1605
|
+
|
|
1606
|
+
retryCount++;
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// 检查是否是 Target closed 错误
|
|
1611
|
+
if ((errorMessage.includes('Target closed') || errorMessage.includes('Protocol error')) && retryCount < maxRetries) {
|
|
1612
|
+
api?.logger?.warn('检测到浏览器目标已关闭,重新创建上下文后重试', {
|
|
1613
|
+
error: errorMessage,
|
|
1614
|
+
retryCount: retryCount + 1
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// 清理并重新创建上下文
|
|
1618
|
+
await this.cleanupBrowser();
|
|
1619
|
+
const newContext = await this.getMcpContext(options);
|
|
1620
|
+
|
|
1621
|
+
// 更新 context 引用
|
|
1622
|
+
context = newContext;
|
|
1623
|
+
|
|
1624
|
+
// 重新创建 snapshot(如果需要)
|
|
1625
|
+
if (methodName && this.requiresSnapshot(methodName)) {
|
|
1626
|
+
await this.ensureSnapshot(context, chromeDevToolsMcp);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// 重新创建响应对象
|
|
1630
|
+
currentResponse = createResponse();
|
|
1631
|
+
|
|
1632
|
+
retryCount++;
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// 不是可重试的错误,或者已经重试过,抛出错误
|
|
1637
|
+
throw handlerError;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* 在页面导航后清除旧的 snapshot
|
|
1644
|
+
*/
|
|
1645
|
+
async clearSnapshotIfNeeded(context, newUrl) {
|
|
1646
|
+
const { api } = this;
|
|
1647
|
+
const manager = getBrowserManager();
|
|
1648
|
+
|
|
1649
|
+
// 如果 URL 发生变化,清除旧的 snapshot
|
|
1650
|
+
if (manager.lastNavigationUrl && manager.lastNavigationUrl !== newUrl) {
|
|
1651
|
+
api?.logger?.info('检测到页面导航,清除旧的 snapshot', {
|
|
1652
|
+
oldUrl: manager.lastNavigationUrl,
|
|
1653
|
+
newUrl: newUrl
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// 清除 snapshot(通过重新创建来清除旧的)
|
|
1657
|
+
try {
|
|
1658
|
+
if (context.clearTextSnapshot && typeof context.clearTextSnapshot === 'function') {
|
|
1659
|
+
context.clearTextSnapshot();
|
|
1660
|
+
} else if (context.getTextSnapshot) {
|
|
1661
|
+
// 如果 context 没有 clearTextSnapshot 方法,尝试通过设置 null 来清除
|
|
1662
|
+
// 这取决于 chrome-devtools-mcp 的实现
|
|
1663
|
+
const snapshot = context.getTextSnapshot();
|
|
1664
|
+
if (snapshot) {
|
|
1665
|
+
// 导航后 snapshot 会自动失效,这里只是记录日志
|
|
1666
|
+
api?.logger?.debug('旧的 snapshot 将在下次操作时自动失效');
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
api?.logger?.warn('清除 snapshot 时出错', { error: error.message });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// 更新最后导航的 URL
|
|
1675
|
+
manager.lastNavigationUrl = newUrl;
|
|
1676
|
+
},
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* 清理浏览器资源
|
|
1680
|
+
*/
|
|
1681
|
+
async cleanupBrowser() {
|
|
1682
|
+
const { api } = this;
|
|
1683
|
+
const manager = getBrowserManager();
|
|
1684
|
+
|
|
1685
|
+
try {
|
|
1686
|
+
// 清理上下文
|
|
1687
|
+
if (manager.context && typeof manager.context.dispose === 'function') {
|
|
1688
|
+
manager.context.dispose();
|
|
1689
|
+
api?.logger?.info('浏览器上下文已清理');
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// 关闭浏览器
|
|
1693
|
+
if (manager.browser && manager.browser.isConnected()) {
|
|
1694
|
+
await manager.browser.close();
|
|
1695
|
+
api?.logger?.info('浏览器已关闭');
|
|
1696
|
+
}
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
api?.logger?.warn('清理浏览器时出错', { error: error.message });
|
|
1699
|
+
} finally {
|
|
1700
|
+
clearBrowserManager();
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
};
|