@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,1186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Browser Automation Tool - 基于 Playwright 的浏览器自动化工具
|
|
3
|
+
*
|
|
4
|
+
* 战略意义:
|
|
5
|
+
* 1. 架构隔离性 - 专门的浏览器自动化工具,确保AI操作安全
|
|
6
|
+
* 2. 平台独立性 - 不依赖特定AI平台的浏览器自动化能力
|
|
7
|
+
* 3. 生态自主性 - 作为Prompt Manager生态的关键组件
|
|
8
|
+
*
|
|
9
|
+
* 设计理念:
|
|
10
|
+
* - 封装 Playwright 核心功能,提供简洁的浏览器自动化接口
|
|
11
|
+
* - 支持多种浏览器(Chromium、Firefox、WebKit)
|
|
12
|
+
* - 提供页面导航、元素操作、截图、内容提取等功能
|
|
13
|
+
* - 智能管理浏览器生命周期(默认自动关闭,可通过 keepAlive 保持)
|
|
14
|
+
* - 一个指令只使用一个窗口/页面实例,确保操作的可预测性
|
|
15
|
+
*
|
|
16
|
+
* 生态定位:
|
|
17
|
+
* 为 AI 提供强大的浏览器自动化能力,支持网页交互、数据抓取、自动化测试等场景
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// 全局浏览器实例管理器(模块级别,跨执行保持)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
const browserInstances = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取或创建浏览器实例管理器
|
|
31
|
+
*/
|
|
32
|
+
function getBrowserManager(toolName) {
|
|
33
|
+
if (!browserInstances.has(toolName)) {
|
|
34
|
+
browserInstances.set(toolName, {
|
|
35
|
+
browser: null,
|
|
36
|
+
context: null,
|
|
37
|
+
page: null,
|
|
38
|
+
lastUsed: null
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return browserInstances.get(toolName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 清理浏览器实例管理器
|
|
46
|
+
*/
|
|
47
|
+
function clearBrowserManager(toolName) {
|
|
48
|
+
browserInstances.delete(toolName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// 工具接口定义
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
export default {
|
|
56
|
+
// --------------------------------------------------------------------------
|
|
57
|
+
// 工具元信息接口
|
|
58
|
+
// --------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取工具依赖
|
|
62
|
+
*/
|
|
63
|
+
getDependencies() {
|
|
64
|
+
return {
|
|
65
|
+
'playwright': '^1.40.0'
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取工具元信息
|
|
71
|
+
*/
|
|
72
|
+
getMetadata() {
|
|
73
|
+
return {
|
|
74
|
+
id: 'playwright',
|
|
75
|
+
name: 'Playwright Browser Automation',
|
|
76
|
+
description: '基于 Playwright 的浏览器自动化工具,支持页面导航、元素操作、截图、内容提取等功能。支持 keepAlive 参数保持浏览器状态,便于连续操作和调试。工具数据存储在工具目录的 data 目录下(~/.prompt-manager/toolbox/playwright/data/)',
|
|
77
|
+
version: '1.1.0',
|
|
78
|
+
category: 'utility',
|
|
79
|
+
author: 'Prompt Manager',
|
|
80
|
+
tags: ['browser', 'automation', 'playwright', 'web', 'scraping', 'testing'],
|
|
81
|
+
scenarios: [
|
|
82
|
+
'网页自动化操作',
|
|
83
|
+
'网页内容抓取',
|
|
84
|
+
'表单自动填写',
|
|
85
|
+
'页面截图',
|
|
86
|
+
'网页交互测试',
|
|
87
|
+
'数据采集',
|
|
88
|
+
'连续多步骤操作(使用 keepAlive 保持浏览器状态)',
|
|
89
|
+
'调试和测试(保持浏览器打开以便查看)'
|
|
90
|
+
],
|
|
91
|
+
limitations: [
|
|
92
|
+
'首次使用时需要安装 Playwright 浏览器(会自动安装,可能需要几分钟)',
|
|
93
|
+
'浏览器二进制文件会下载到 ~/.cache/ms-playwright/ 目录',
|
|
94
|
+
'工具数据存储在 ~/.prompt-manager/toolbox/playwright/data/ 目录下',
|
|
95
|
+
'浏览器操作需要时间,大页面可能较慢',
|
|
96
|
+
'某些网站可能有反爬虫机制',
|
|
97
|
+
'无头模式可能无法处理某些需要真实浏览器的场景',
|
|
98
|
+
'默认情况下操作完成后会自动关闭浏览器(可通过 keepAlive 参数控制)'
|
|
99
|
+
],
|
|
100
|
+
dataStorage: {
|
|
101
|
+
path: '~/.prompt-manager/toolbox/playwright/data/',
|
|
102
|
+
description: '工具数据存储目录,包括浏览器安装状态、截图文件等数据',
|
|
103
|
+
note: '数据存储在工具所在目录的 data 子目录下'
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取参数Schema
|
|
110
|
+
*/
|
|
111
|
+
getSchema() {
|
|
112
|
+
return {
|
|
113
|
+
parameters: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
method: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: '操作方法',
|
|
119
|
+
enum: [
|
|
120
|
+
'navigate', 'click', 'fill', 'screenshot', 'getContent',
|
|
121
|
+
'waitForSelector', 'evaluate', 'getTitle', 'getUrl',
|
|
122
|
+
'goBack', 'goForward', 'reload', 'close'
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
url: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: '目标URL(navigate方法必需)'
|
|
128
|
+
},
|
|
129
|
+
selector: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'CSS选择器(click、fill、waitForSelector等方法需要)'
|
|
132
|
+
},
|
|
133
|
+
text: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: '要填写的文本(fill方法需要)'
|
|
136
|
+
},
|
|
137
|
+
screenshotPath: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: '截图保存路径(screenshot方法可选)'
|
|
140
|
+
},
|
|
141
|
+
keepAlive: {
|
|
142
|
+
type: 'boolean',
|
|
143
|
+
description: '操作完成后是否保持浏览器打开(默认false,操作完成后自动关闭)。设置为true时,浏览器会保持打开状态,可用于连续操作或调试',
|
|
144
|
+
default: false
|
|
145
|
+
},
|
|
146
|
+
options: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
description: '操作选项',
|
|
149
|
+
properties: {
|
|
150
|
+
browser: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
enum: ['chromium', 'firefox', 'webkit'],
|
|
153
|
+
description: '浏览器类型',
|
|
154
|
+
default: 'chromium'
|
|
155
|
+
},
|
|
156
|
+
headless: {
|
|
157
|
+
type: 'boolean',
|
|
158
|
+
description: '是否无头模式',
|
|
159
|
+
default: true
|
|
160
|
+
},
|
|
161
|
+
timeout: {
|
|
162
|
+
type: 'number',
|
|
163
|
+
description: '超时时间(毫秒)',
|
|
164
|
+
default: 30000
|
|
165
|
+
},
|
|
166
|
+
waitUntil: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
enum: ['load', 'domcontentloaded', 'networkidle'],
|
|
169
|
+
description: '等待页面加载状态',
|
|
170
|
+
default: 'load'
|
|
171
|
+
},
|
|
172
|
+
keepAlive: {
|
|
173
|
+
type: 'boolean',
|
|
174
|
+
description: '操作完成后是否保持浏览器打开(默认false)',
|
|
175
|
+
default: false
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
script: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: '要执行的JavaScript代码(evaluate方法需要)'
|
|
182
|
+
},
|
|
183
|
+
fullPage: {
|
|
184
|
+
type: 'boolean',
|
|
185
|
+
description: '是否截取整页(screenshot方法)',
|
|
186
|
+
default: false
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
required: ['method']
|
|
190
|
+
},
|
|
191
|
+
environment: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
PLAYWRIGHT_BROWSER: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
description: '默认浏览器类型(chromium, firefox, webkit)',
|
|
197
|
+
default: 'chromium'
|
|
198
|
+
},
|
|
199
|
+
PLAYWRIGHT_HEADLESS: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: '是否无头模式(true/false)',
|
|
202
|
+
default: 'true'
|
|
203
|
+
},
|
|
204
|
+
PLAYWRIGHT_TIMEOUT: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
description: '默认超时时间(毫秒)',
|
|
207
|
+
default: '30000'
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
required: []
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 获取业务错误定义
|
|
217
|
+
*/
|
|
218
|
+
getBusinessErrors() {
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
code: 'BROWSER_LAUNCH_FAILED',
|
|
222
|
+
description: '浏览器启动失败',
|
|
223
|
+
match: /browser.*launch|无法启动浏览器|Browser launch failed/i,
|
|
224
|
+
solution: '检查 Playwright 是否正确安装,尝试运行 npx playwright install',
|
|
225
|
+
retryable: true
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
code: 'NAVIGATION_FAILED',
|
|
229
|
+
description: '页面导航失败',
|
|
230
|
+
match: /navigation.*failed|页面加载失败|Navigation timeout/i,
|
|
231
|
+
solution: '检查URL是否正确,网络是否正常,或增加超时时间',
|
|
232
|
+
retryable: true
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
code: 'ELEMENT_NOT_FOUND',
|
|
236
|
+
description: '元素未找到',
|
|
237
|
+
match: /element.*not found|selector.*not found|等待元素超时/i,
|
|
238
|
+
solution: '检查选择器是否正确,元素是否存在,或增加等待时间',
|
|
239
|
+
retryable: false
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
code: 'SCREENSHOT_FAILED',
|
|
243
|
+
description: '截图失败',
|
|
244
|
+
match: /screenshot.*failed|无法保存截图/i,
|
|
245
|
+
solution: '检查保存路径是否有效,是否有写入权限',
|
|
246
|
+
retryable: false
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
code: 'NETWORK_ERROR',
|
|
250
|
+
description: '网络错误',
|
|
251
|
+
match: /network.*error|连接失败|ECONNREFUSED|ETIMEDOUT/i,
|
|
252
|
+
solution: '检查网络连接,URL是否可访问',
|
|
253
|
+
retryable: true
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
code: 'INVALID_SELECTOR',
|
|
257
|
+
description: '无效的选择器',
|
|
258
|
+
match: /invalid.*selector|选择器格式错误/i,
|
|
259
|
+
solution: '检查CSS选择器语法是否正确',
|
|
260
|
+
retryable: false
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
code: 'TIMEOUT',
|
|
264
|
+
description: '操作超时',
|
|
265
|
+
match: /timeout|超时/i,
|
|
266
|
+
solution: '增加超时时间或检查操作是否正常',
|
|
267
|
+
retryable: true
|
|
268
|
+
}
|
|
269
|
+
];
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// --------------------------------------------------------------------------
|
|
273
|
+
// 主执行方法
|
|
274
|
+
// --------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 执行工具
|
|
278
|
+
*
|
|
279
|
+
* 重要说明:
|
|
280
|
+
* - 一个指令(一个 execute 调用)中只会使用一个浏览器窗口/页面实例
|
|
281
|
+
* - 即使需要访问多个 URL,也是在同一个页面中导航,不会创建多个窗口
|
|
282
|
+
* - 这样可以确保操作的可预测性和一致性
|
|
283
|
+
*/
|
|
284
|
+
async execute(params) {
|
|
285
|
+
const { api } = this;
|
|
286
|
+
|
|
287
|
+
api?.logger?.info('Playwright操作开始', {
|
|
288
|
+
method: params.method,
|
|
289
|
+
url: params.url
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// 1. 参数验证
|
|
294
|
+
this.validateMethodParams(params);
|
|
295
|
+
|
|
296
|
+
// 2. 导入 Playwright
|
|
297
|
+
const playwright = await this.importToolModule('playwright');
|
|
298
|
+
|
|
299
|
+
// 3. 确保浏览器已安装
|
|
300
|
+
await this.ensureBrowsersInstalled(playwright, params.options);
|
|
301
|
+
|
|
302
|
+
// 4. 获取浏览器实例(使用缓存或创建新实例)
|
|
303
|
+
const browser = await this.getBrowser(playwright, params.options);
|
|
304
|
+
const context = await this.getContext(browser, params.options);
|
|
305
|
+
const page = await this.getPage(context);
|
|
306
|
+
|
|
307
|
+
// 5. 记录当前使用的页面信息
|
|
308
|
+
const currentUrl = page.url();
|
|
309
|
+
const keepAlive = this.getKeepAlive(params);
|
|
310
|
+
|
|
311
|
+
api?.logger?.info('使用页面实例', {
|
|
312
|
+
pageUrl: currentUrl,
|
|
313
|
+
method: params.method,
|
|
314
|
+
keepAlive: keepAlive,
|
|
315
|
+
note: '一个指令中只使用一个页面实例,多个操作在同一页面中完成'
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// 6. 执行操作方法
|
|
319
|
+
const result = await this.executeMethod(page, params, browser, context);
|
|
320
|
+
|
|
321
|
+
// 7. 根据 keepAlive 决定是否关闭浏览器
|
|
322
|
+
if (params.method !== 'close') {
|
|
323
|
+
if (!keepAlive) {
|
|
324
|
+
api?.logger?.info('操作完成,自动关闭浏览器(keepAlive=false)');
|
|
325
|
+
await this.cleanupBrowser();
|
|
326
|
+
} else {
|
|
327
|
+
api?.logger?.info('操作完成,保持浏览器打开(keepAlive=true)', {
|
|
328
|
+
currentUrl: page.url(),
|
|
329
|
+
note: '浏览器将保持打开状态,可在后续操作中继续使用'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
api?.logger?.info('Playwright操作成功', { method: params.method });
|
|
335
|
+
return result;
|
|
336
|
+
|
|
337
|
+
} catch (error) {
|
|
338
|
+
api?.logger?.error('Playwright操作失败', {
|
|
339
|
+
method: params.method,
|
|
340
|
+
error: error.message,
|
|
341
|
+
stack: error.stack
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 错误时也尝试清理浏览器(如果 keepAlive=false)
|
|
345
|
+
if (params.method !== 'close') {
|
|
346
|
+
const keepAlive = this.getKeepAlive(params);
|
|
347
|
+
if (!keepAlive) {
|
|
348
|
+
try {
|
|
349
|
+
api?.logger?.info('操作失败,自动关闭浏览器(keepAlive=false)');
|
|
350
|
+
await this.cleanupBrowser();
|
|
351
|
+
} catch (cleanupError) {
|
|
352
|
+
api?.logger?.warn('清理浏览器时出错', { error: cleanupError.message });
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
api?.logger?.info('操作失败,保持浏览器打开(keepAlive=true)');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// --------------------------------------------------------------------------
|
|
364
|
+
// 辅助方法(参数处理、验证等)
|
|
365
|
+
// --------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 获取 keepAlive 参数值
|
|
369
|
+
*/
|
|
370
|
+
getKeepAlive(params) {
|
|
371
|
+
return params.options?.keepAlive !== undefined
|
|
372
|
+
? params.options.keepAlive
|
|
373
|
+
: (params.keepAlive !== undefined ? params.keepAlive : false);
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 获取浏览器类型
|
|
378
|
+
*/
|
|
379
|
+
getBrowserType(options = {}) {
|
|
380
|
+
const { api } = this;
|
|
381
|
+
return options.browser ||
|
|
382
|
+
api.environment.get('PLAYWRIGHT_BROWSER') ||
|
|
383
|
+
'chromium';
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 获取超时时间
|
|
388
|
+
*/
|
|
389
|
+
getTimeout(options = {}) {
|
|
390
|
+
const { api } = this;
|
|
391
|
+
return options.timeout ||
|
|
392
|
+
parseInt(api.environment.get('PLAYWRIGHT_TIMEOUT') || '30000', 10);
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 验证方法参数(业务层面)
|
|
397
|
+
*/
|
|
398
|
+
validateMethodParams(params) {
|
|
399
|
+
const methodRequirements = {
|
|
400
|
+
'navigate': ['url'],
|
|
401
|
+
'click': ['selector'],
|
|
402
|
+
'fill': ['selector', 'text'],
|
|
403
|
+
'screenshot': [], // 可选参数
|
|
404
|
+
'getContent': [],
|
|
405
|
+
'waitForSelector': ['selector'],
|
|
406
|
+
'evaluate': ['script'],
|
|
407
|
+
'getTitle': [],
|
|
408
|
+
'getUrl': [],
|
|
409
|
+
'goBack': [],
|
|
410
|
+
'goForward': [],
|
|
411
|
+
'reload': [],
|
|
412
|
+
'close': []
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const required = methodRequirements[params.method];
|
|
416
|
+
if (!required) {
|
|
417
|
+
throw new Error(`不支持的方法: ${params.method}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const missing = required.filter(field => params[field] === undefined || params[field] === null);
|
|
421
|
+
if (missing.length > 0) {
|
|
422
|
+
throw new Error(`方法 ${params.method} 缺少必需参数: ${missing.join(', ')}`);
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 执行操作方法(路由到对应的处理方法)
|
|
428
|
+
*/
|
|
429
|
+
async executeMethod(page, params, browser, context) {
|
|
430
|
+
switch (params.method) {
|
|
431
|
+
case 'navigate':
|
|
432
|
+
return await this.handleNavigate(page, params);
|
|
433
|
+
case 'click':
|
|
434
|
+
return await this.handleClick(page, params);
|
|
435
|
+
case 'fill':
|
|
436
|
+
return await this.handleFill(page, params);
|
|
437
|
+
case 'screenshot':
|
|
438
|
+
return await this.handleScreenshot(page, params);
|
|
439
|
+
case 'getContent':
|
|
440
|
+
return await this.handleGetContent(page, params);
|
|
441
|
+
case 'waitForSelector':
|
|
442
|
+
return await this.handleWaitForSelector(page, params);
|
|
443
|
+
case 'evaluate':
|
|
444
|
+
return await this.handleEvaluate(page, params);
|
|
445
|
+
case 'getTitle':
|
|
446
|
+
return await this.handleGetTitle(page, params);
|
|
447
|
+
case 'getUrl':
|
|
448
|
+
return await this.handleGetUrl(page, params);
|
|
449
|
+
case 'goBack':
|
|
450
|
+
return await this.handleGoBack(page, params);
|
|
451
|
+
case 'goForward':
|
|
452
|
+
return await this.handleGoForward(page, params);
|
|
453
|
+
case 'reload':
|
|
454
|
+
return await this.handleReload(page, params);
|
|
455
|
+
case 'close':
|
|
456
|
+
return await this.handleClose(browser, context, page, params);
|
|
457
|
+
default:
|
|
458
|
+
throw new Error(`不支持的方法: ${params.method}`);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
// --------------------------------------------------------------------------
|
|
463
|
+
// 浏览器安装和检查
|
|
464
|
+
// --------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 确保浏览器已安装(首次使用时自动安装)
|
|
468
|
+
* 先检查系统是否已存在浏览器,存在则直接使用,不存在才安装
|
|
469
|
+
*/
|
|
470
|
+
async ensureBrowsersInstalled(playwright, options = {}) {
|
|
471
|
+
const { api } = this;
|
|
472
|
+
const browserType = this.getBrowserType(options);
|
|
473
|
+
|
|
474
|
+
// 步骤1:检查 storage 记录(快速检查)
|
|
475
|
+
const browserInstalled = api.storage.getItem('browsers_installed');
|
|
476
|
+
if (browserInstalled && browserInstalled.browserType === browserType) {
|
|
477
|
+
api?.logger?.debug('根据 storage 记录,浏览器已安装,验证可用性...');
|
|
478
|
+
const isAvailable = await this.checkBrowserAvailable(playwright, browserType);
|
|
479
|
+
if (isAvailable) {
|
|
480
|
+
api?.logger?.debug('浏览器验证通过,可直接使用');
|
|
481
|
+
return;
|
|
482
|
+
} else {
|
|
483
|
+
api?.logger?.warn('storage 记录显示已安装,但浏览器不可用,将重新安装');
|
|
484
|
+
api.storage.setItem('browsers_installed', null);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 步骤2:实际检查浏览器是否已安装(尝试启动浏览器)
|
|
489
|
+
api?.logger?.info('检查系统是否已安装 Playwright 浏览器...', { browserType });
|
|
490
|
+
const isAvailable = await this.checkBrowserAvailable(playwright, browserType);
|
|
491
|
+
|
|
492
|
+
if (isAvailable) {
|
|
493
|
+
api?.logger?.info('检测到系统已存在 Playwright 浏览器,直接使用', { browserType });
|
|
494
|
+
api.storage.setItem('browsers_installed', {
|
|
495
|
+
browserType,
|
|
496
|
+
installedAt: Date.now(),
|
|
497
|
+
source: 'system_existing'
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 步骤3:浏览器不存在,执行安装
|
|
503
|
+
api?.logger?.info('未检测到已安装的浏览器,开始安装 Playwright 浏览器...', {
|
|
504
|
+
browserType,
|
|
505
|
+
note: '这可能需要几分钟时间,请耐心等待'
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await this.installBrowser(browserType);
|
|
509
|
+
|
|
510
|
+
// 验证安装是否成功
|
|
511
|
+
const verifyAvailable = await this.checkBrowserAvailable(playwright, browserType);
|
|
512
|
+
if (!verifyAvailable) {
|
|
513
|
+
throw new Error('浏览器安装完成,但验证失败。请检查安装日志');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 标记浏览器已安装
|
|
517
|
+
api.storage.setItem('browsers_installed', {
|
|
518
|
+
browserType,
|
|
519
|
+
installedAt: Date.now(),
|
|
520
|
+
source: 'auto_installed'
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
api?.logger?.info('浏览器安装完成并验证通过', { browserType });
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 检查浏览器是否可用(尝试启动浏览器进行验证)
|
|
528
|
+
*/
|
|
529
|
+
async checkBrowserAvailable(playwright, browserType = 'chromium') {
|
|
530
|
+
const { api } = this;
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
api?.logger?.debug('验证浏览器是否可用', { browserType });
|
|
534
|
+
|
|
535
|
+
// 获取对应的浏览器类型
|
|
536
|
+
const browserLauncher = this.getBrowserLauncher(playwright, browserType);
|
|
537
|
+
|
|
538
|
+
// 尝试启动浏览器(无头模式,快速验证)
|
|
539
|
+
const browser = await browserLauncher.launch({
|
|
540
|
+
headless: true,
|
|
541
|
+
timeout: 10000 // 10秒超时,快速验证
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// 如果能启动,说明浏览器已安装
|
|
545
|
+
await browser.close();
|
|
546
|
+
|
|
547
|
+
api?.logger?.debug('浏览器验证成功,已安装且可用', { browserType });
|
|
548
|
+
return true;
|
|
549
|
+
|
|
550
|
+
} catch (error) {
|
|
551
|
+
api?.logger?.debug('浏览器验证失败', {
|
|
552
|
+
browserType,
|
|
553
|
+
error: error.message,
|
|
554
|
+
note: '浏览器可能未安装或不可用'
|
|
555
|
+
});
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 获取浏览器启动器
|
|
562
|
+
*/
|
|
563
|
+
getBrowserLauncher(playwright, browserType) {
|
|
564
|
+
switch (browserType) {
|
|
565
|
+
case 'firefox':
|
|
566
|
+
return playwright.firefox;
|
|
567
|
+
case 'webkit':
|
|
568
|
+
return playwright.webkit;
|
|
569
|
+
case 'chromium':
|
|
570
|
+
default:
|
|
571
|
+
return playwright.chromium;
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 安装浏览器
|
|
577
|
+
*/
|
|
578
|
+
async installBrowser(browserType) {
|
|
579
|
+
const { api } = this;
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
const { exec } = await import('child_process');
|
|
583
|
+
const { promisify } = await import('util');
|
|
584
|
+
const execAsync = promisify(exec);
|
|
585
|
+
const fs = await import('fs/promises');
|
|
586
|
+
|
|
587
|
+
const toolDir = this.__toolDir;
|
|
588
|
+
const nodeModulesPath = path.join(toolDir, 'node_modules');
|
|
589
|
+
const playwrightBinPath = path.join(nodeModulesPath, '.bin', 'playwright');
|
|
590
|
+
|
|
591
|
+
// 构建脚本运行时环境
|
|
592
|
+
const runtimeEnv = this.buildRuntimeEnvironment(toolDir);
|
|
593
|
+
|
|
594
|
+
api?.logger?.info('使用脚本运行时环境安装浏览器', {
|
|
595
|
+
cwd: toolDir,
|
|
596
|
+
nodeModulesPath,
|
|
597
|
+
note: '不继承用户环境变量,使用独立的运行时环境'
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// 构建安装命令
|
|
601
|
+
const installCommand = await this.buildInstallCommand(
|
|
602
|
+
playwrightBinPath,
|
|
603
|
+
nodeModulesPath,
|
|
604
|
+
browserType,
|
|
605
|
+
fs
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// 执行安装命令
|
|
609
|
+
const { stdout, stderr } = await execAsync(installCommand, {
|
|
610
|
+
cwd: toolDir,
|
|
611
|
+
timeout: 600000, // 10分钟超时
|
|
612
|
+
env: runtimeEnv
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (stdout) {
|
|
616
|
+
api?.logger?.info('浏览器安装输出', { stdout: stdout.substring(0, 500) });
|
|
617
|
+
}
|
|
618
|
+
if (stderr && !stderr.includes('warning')) {
|
|
619
|
+
api?.logger?.warn('浏览器安装警告', { stderr: stderr.substring(0, 500) });
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
} catch (error) {
|
|
623
|
+
api?.logger?.error('浏览器安装失败', {
|
|
624
|
+
error: error.message,
|
|
625
|
+
note: '请手动运行: npx playwright install ' + browserType
|
|
626
|
+
});
|
|
627
|
+
throw new Error(`Playwright 浏览器未安装。请运行: npx playwright install ${browserType}\n错误详情: ${error.message}`);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* 构建运行时环境变量
|
|
633
|
+
*/
|
|
634
|
+
buildRuntimeEnvironment(toolDir) {
|
|
635
|
+
const nodeModulesBinPath = path.join(toolDir, 'node_modules', '.bin');
|
|
636
|
+
return {
|
|
637
|
+
// 系统必需变量
|
|
638
|
+
PATH: `${nodeModulesBinPath}:${process.env.PATH || '/usr/local/bin:/usr/bin:/bin'}`,
|
|
639
|
+
HOME: process.env.HOME || os.homedir(),
|
|
640
|
+
USER: process.env.USER || 'user',
|
|
641
|
+
// Node.js 相关
|
|
642
|
+
NODE_PATH: path.join(toolDir, 'node_modules'),
|
|
643
|
+
// Playwright 浏览器路径(使用默认路径)
|
|
644
|
+
PLAYWRIGHT_BROWSERS_PATH: '0',
|
|
645
|
+
// 确保使用工具目录的 node_modules
|
|
646
|
+
npm_config_prefix: toolDir,
|
|
647
|
+
// 平台相关
|
|
648
|
+
...(process.platform === 'win32' ? {
|
|
649
|
+
PATHEXT: process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM'
|
|
650
|
+
} : {})
|
|
651
|
+
};
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 构建安装命令
|
|
656
|
+
*/
|
|
657
|
+
async buildInstallCommand(playwrightBinPath, nodeModulesPath, browserType, fs) {
|
|
658
|
+
const { api } = this;
|
|
659
|
+
|
|
660
|
+
// 优先:使用本地 playwright CLI
|
|
661
|
+
try {
|
|
662
|
+
await fs.access(playwrightBinPath);
|
|
663
|
+
const installCommand = `"${playwrightBinPath}" install ${browserType}`;
|
|
664
|
+
api?.logger?.info('使用本地 playwright CLI 安装浏览器', {
|
|
665
|
+
command: installCommand,
|
|
666
|
+
playwrightPath: playwrightBinPath
|
|
667
|
+
});
|
|
668
|
+
return installCommand;
|
|
669
|
+
} catch {
|
|
670
|
+
// 其次:使用 playwright/cli.js
|
|
671
|
+
const playwrightCliPath = path.join(nodeModulesPath, 'playwright', 'cli.js');
|
|
672
|
+
try {
|
|
673
|
+
await fs.access(playwrightCliPath);
|
|
674
|
+
const nodePath = process.execPath;
|
|
675
|
+
const installCommand = `"${nodePath}" "${playwrightCliPath}" install ${browserType}`;
|
|
676
|
+
api?.logger?.info('使用 playwright/cli.js 安装浏览器', {
|
|
677
|
+
command: installCommand,
|
|
678
|
+
nodePath,
|
|
679
|
+
cliPath: playwrightCliPath
|
|
680
|
+
});
|
|
681
|
+
return installCommand;
|
|
682
|
+
} catch {
|
|
683
|
+
// 最后:使用 npx(回退方案)
|
|
684
|
+
const nodePath = process.execPath;
|
|
685
|
+
const installCommand = `"${nodePath}" -e "require('child_process').spawnSync('npx', ['--yes', 'playwright@latest', 'install', '${browserType}'], {stdio: 'inherit', env: process.env})"`;
|
|
686
|
+
api?.logger?.warn('使用 npx 安装浏览器(回退方案,可能较慢)', {
|
|
687
|
+
command: installCommand,
|
|
688
|
+
note: '建议确保 playwright 已正确安装到工具目录'
|
|
689
|
+
});
|
|
690
|
+
return installCommand;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
// --------------------------------------------------------------------------
|
|
696
|
+
// 浏览器实例管理
|
|
697
|
+
// --------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 获取浏览器实例(使用全局管理器,跨执行保持)
|
|
701
|
+
*/
|
|
702
|
+
async getBrowser(playwright, options = {}) {
|
|
703
|
+
const { api } = this;
|
|
704
|
+
const manager = getBrowserManager(this.__toolName);
|
|
705
|
+
|
|
706
|
+
// 如果已有浏览器实例且仍然连接,直接返回
|
|
707
|
+
if (manager.browser && manager.browser.isConnected()) {
|
|
708
|
+
api?.logger?.info('复用已存在的浏览器实例', {
|
|
709
|
+
note: '使用之前创建的浏览器,保持会话状态(跨执行保持)'
|
|
710
|
+
});
|
|
711
|
+
manager.lastUsed = Date.now();
|
|
712
|
+
return manager.browser;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 创建新浏览器实例
|
|
716
|
+
const browserType = this.getBrowserType(options);
|
|
717
|
+
const headless = options.headless !== undefined
|
|
718
|
+
? options.headless
|
|
719
|
+
: (api.environment.get('PLAYWRIGHT_HEADLESS') === 'true');
|
|
720
|
+
|
|
721
|
+
api?.logger?.info('创建新浏览器实例', {
|
|
722
|
+
browserType,
|
|
723
|
+
headless,
|
|
724
|
+
note: '首次创建浏览器实例'
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const browserLauncher = this.getBrowserLauncher(playwright, browserType);
|
|
728
|
+
const browser = await browserLauncher.launch({ headless });
|
|
729
|
+
|
|
730
|
+
// 存储到全局管理器
|
|
731
|
+
manager.browser = browser;
|
|
732
|
+
manager.lastUsed = Date.now();
|
|
733
|
+
|
|
734
|
+
return browser;
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 获取浏览器上下文(使用全局管理器,跨执行保持)
|
|
739
|
+
*/
|
|
740
|
+
async getContext(browser, options = {}) {
|
|
741
|
+
const { api } = this;
|
|
742
|
+
const manager = getBrowserManager(this.__toolName);
|
|
743
|
+
|
|
744
|
+
// 如果已有上下文且未关闭,直接返回
|
|
745
|
+
if (manager.context) {
|
|
746
|
+
try {
|
|
747
|
+
if (!manager.context.isClosed()) {
|
|
748
|
+
api?.logger?.info('复用已存在的浏览器上下文', {
|
|
749
|
+
note: '使用之前创建的上下文,保持会话状态(跨执行保持)'
|
|
750
|
+
});
|
|
751
|
+
manager.lastUsed = Date.now();
|
|
752
|
+
return manager.context;
|
|
753
|
+
}
|
|
754
|
+
} catch {
|
|
755
|
+
// 上下文已关闭,继续创建新上下文
|
|
756
|
+
manager.context = null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// 创建新上下文
|
|
761
|
+
api?.logger?.info('创建新浏览器上下文', {
|
|
762
|
+
note: '首次创建浏览器上下文'
|
|
763
|
+
});
|
|
764
|
+
const context = await browser.newContext();
|
|
765
|
+
|
|
766
|
+
// 存储到全局管理器
|
|
767
|
+
manager.context = context;
|
|
768
|
+
manager.lastUsed = Date.now();
|
|
769
|
+
|
|
770
|
+
return context;
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* 获取页面实例(使用全局管理器,跨执行保持,确保一个指令只使用一个页面)
|
|
775
|
+
*/
|
|
776
|
+
async getPage(context) {
|
|
777
|
+
const { api } = this;
|
|
778
|
+
const manager = getBrowserManager(this.__toolName);
|
|
779
|
+
|
|
780
|
+
// 如果已有页面且未关闭,直接返回
|
|
781
|
+
if (manager.page) {
|
|
782
|
+
try {
|
|
783
|
+
if (!manager.page.isClosed()) {
|
|
784
|
+
const currentUrl = manager.page.url();
|
|
785
|
+
api?.logger?.info('复用已存在的页面实例', {
|
|
786
|
+
url: currentUrl,
|
|
787
|
+
note: '使用之前创建的页面,保持页面状态(跨执行保持)'
|
|
788
|
+
});
|
|
789
|
+
manager.lastUsed = Date.now();
|
|
790
|
+
return manager.page;
|
|
791
|
+
}
|
|
792
|
+
} catch {
|
|
793
|
+
// 页面已关闭,继续创建新页面
|
|
794
|
+
manager.page = null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 创建新页面
|
|
799
|
+
api?.logger?.info('创建新页面实例', {
|
|
800
|
+
note: '首次创建页面,一个指令中只创建一个页面实例'
|
|
801
|
+
});
|
|
802
|
+
const page = await context.newPage();
|
|
803
|
+
|
|
804
|
+
// 存储到全局管理器
|
|
805
|
+
manager.page = page;
|
|
806
|
+
manager.lastUsed = Date.now();
|
|
807
|
+
|
|
808
|
+
return page;
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
// --------------------------------------------------------------------------
|
|
812
|
+
// 页面操作方法
|
|
813
|
+
// --------------------------------------------------------------------------
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 处理导航操作
|
|
817
|
+
*/
|
|
818
|
+
async handleNavigate(page, params) {
|
|
819
|
+
if (!params.url) {
|
|
820
|
+
throw new Error('navigate方法需要url参数');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const { api } = this;
|
|
824
|
+
const options = params.options || {};
|
|
825
|
+
const timeout = this.getTimeout(options);
|
|
826
|
+
const waitUntil = options.waitUntil || 'load';
|
|
827
|
+
|
|
828
|
+
api?.logger?.info('导航到URL', { url: params.url, waitUntil, timeout });
|
|
829
|
+
|
|
830
|
+
await page.goto(params.url, { waitUntil, timeout });
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
success: true,
|
|
834
|
+
url: page.url(),
|
|
835
|
+
title: await page.title()
|
|
836
|
+
};
|
|
837
|
+
},
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* 处理点击操作
|
|
841
|
+
*/
|
|
842
|
+
async handleClick(page, params) {
|
|
843
|
+
if (!params.selector) {
|
|
844
|
+
throw new Error('click方法需要selector参数');
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const { api } = this;
|
|
848
|
+
const timeout = this.getTimeout(params.options);
|
|
849
|
+
|
|
850
|
+
api?.logger?.info('点击元素', { selector: params.selector });
|
|
851
|
+
await page.click(params.selector, { timeout });
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
success: true,
|
|
855
|
+
selector: params.selector
|
|
856
|
+
};
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* 处理填写操作
|
|
861
|
+
*/
|
|
862
|
+
async handleFill(page, params) {
|
|
863
|
+
if (!params.selector) {
|
|
864
|
+
throw new Error('fill方法需要selector参数');
|
|
865
|
+
}
|
|
866
|
+
if (params.text === undefined) {
|
|
867
|
+
throw new Error('fill方法需要text参数');
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const { api } = this;
|
|
871
|
+
const timeout = this.getTimeout(params.options);
|
|
872
|
+
|
|
873
|
+
api?.logger?.info('填写表单', { selector: params.selector });
|
|
874
|
+
await page.fill(params.selector, params.text, { timeout });
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
success: true,
|
|
878
|
+
selector: params.selector,
|
|
879
|
+
text: params.text
|
|
880
|
+
};
|
|
881
|
+
},
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* 处理截图操作
|
|
885
|
+
*/
|
|
886
|
+
async handleScreenshot(page, params) {
|
|
887
|
+
const { api } = this;
|
|
888
|
+
|
|
889
|
+
// 检查是否需要导航
|
|
890
|
+
const currentUrl = page.url();
|
|
891
|
+
const needsNavigation = params.url &&
|
|
892
|
+
(currentUrl === 'about:blank' || currentUrl === '' || currentUrl !== params.url);
|
|
893
|
+
|
|
894
|
+
if (needsNavigation) {
|
|
895
|
+
await this.navigateForScreenshot(page, params);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// 处理截图路径
|
|
899
|
+
const screenshotPath = await this.resolveScreenshotPath(params);
|
|
900
|
+
|
|
901
|
+
// 验证页面状态
|
|
902
|
+
const finalUrl = page.url();
|
|
903
|
+
if (finalUrl === 'about:blank' || finalUrl === '') {
|
|
904
|
+
throw new Error('无法截图:页面为空。请提供 url 参数或先调用 navigate 方法导航到目标页面');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// 执行截图
|
|
908
|
+
const timeout = this.getTimeout(params.options);
|
|
909
|
+
api?.logger?.info('准备截图', {
|
|
910
|
+
path: screenshotPath,
|
|
911
|
+
fullPage: params.fullPage,
|
|
912
|
+
currentUrl: finalUrl
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
await page.screenshot({
|
|
916
|
+
path: screenshotPath,
|
|
917
|
+
fullPage: params.fullPage || false,
|
|
918
|
+
timeout
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
api?.logger?.info('截图完成', { path: screenshotPath, url: finalUrl });
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
success: true,
|
|
925
|
+
path: screenshotPath,
|
|
926
|
+
url: finalUrl
|
|
927
|
+
};
|
|
928
|
+
},
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* 为截图导航到URL
|
|
932
|
+
*/
|
|
933
|
+
async navigateForScreenshot(page, params) {
|
|
934
|
+
const { api } = this;
|
|
935
|
+
const options = params.options || {};
|
|
936
|
+
const timeout = this.getTimeout(options);
|
|
937
|
+
const waitUntil = options.waitUntil || 'networkidle';
|
|
938
|
+
|
|
939
|
+
api?.logger?.info('需要导航到URL', {
|
|
940
|
+
currentUrl: page.url(),
|
|
941
|
+
targetUrl: params.url,
|
|
942
|
+
note: '在同一页面中导航,不创建新页面'
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// 导航并等待页面完全加载
|
|
946
|
+
await page.goto(params.url, { waitUntil, timeout });
|
|
947
|
+
|
|
948
|
+
// 额外等待网络空闲
|
|
949
|
+
try {
|
|
950
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
951
|
+
api?.logger?.info('页面网络空闲,准备截图');
|
|
952
|
+
} catch (waitError) {
|
|
953
|
+
api?.logger?.warn('等待 networkidle 超时,使用 domcontentloaded', {
|
|
954
|
+
error: waitError.message
|
|
955
|
+
});
|
|
956
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 再等待一小段时间,确保页面完全渲染
|
|
960
|
+
await page.waitForTimeout(1000);
|
|
961
|
+
|
|
962
|
+
api?.logger?.info('导航完成,页面已完全加载', {
|
|
963
|
+
url: page.url(),
|
|
964
|
+
note: '准备截图'
|
|
965
|
+
});
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* 解析截图路径
|
|
970
|
+
* 默认保存到工具目录的 data 子目录下
|
|
971
|
+
*/
|
|
972
|
+
async resolveScreenshotPath(params) {
|
|
973
|
+
let screenshotPath = params.screenshotPath;
|
|
974
|
+
|
|
975
|
+
if (!screenshotPath) {
|
|
976
|
+
// 默认保存到 data 目录
|
|
977
|
+
const timestamp = Date.now();
|
|
978
|
+
const dataDir = path.join(this.__toolDir, 'data');
|
|
979
|
+
screenshotPath = path.join(dataDir, `screenshot-${timestamp}.png`);
|
|
980
|
+
} else {
|
|
981
|
+
// 初始化文件系统并解析路径(如果以~开头)
|
|
982
|
+
await this.initializeFilesystem();
|
|
983
|
+
screenshotPath = this.resolvePromptManagerPath(screenshotPath);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// 确保目录存在
|
|
987
|
+
const fs = await import('fs/promises');
|
|
988
|
+
const dir = path.dirname(screenshotPath);
|
|
989
|
+
await fs.mkdir(dir, { recursive: true });
|
|
990
|
+
|
|
991
|
+
return screenshotPath;
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* 处理获取内容操作
|
|
996
|
+
*/
|
|
997
|
+
async handleGetContent(page, params) {
|
|
998
|
+
const { api } = this;
|
|
999
|
+
|
|
1000
|
+
api?.logger?.info('获取页面内容');
|
|
1001
|
+
const content = await page.content();
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
success: true,
|
|
1005
|
+
content,
|
|
1006
|
+
length: content.length
|
|
1007
|
+
};
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* 处理等待选择器操作
|
|
1012
|
+
*/
|
|
1013
|
+
async handleWaitForSelector(page, params) {
|
|
1014
|
+
if (!params.selector) {
|
|
1015
|
+
throw new Error('waitForSelector方法需要selector参数');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const { api } = this;
|
|
1019
|
+
const timeout = this.getTimeout(params.options);
|
|
1020
|
+
|
|
1021
|
+
api?.logger?.info('等待元素', { selector: params.selector, timeout });
|
|
1022
|
+
await page.waitForSelector(params.selector, { timeout });
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
success: true,
|
|
1026
|
+
selector: params.selector
|
|
1027
|
+
};
|
|
1028
|
+
},
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* 处理执行脚本操作
|
|
1032
|
+
*/
|
|
1033
|
+
async handleEvaluate(page, params) {
|
|
1034
|
+
if (!params.script) {
|
|
1035
|
+
throw new Error('evaluate方法需要script参数');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const { api } = this;
|
|
1039
|
+
|
|
1040
|
+
api?.logger?.info('执行JavaScript', { scriptLength: params.script.length });
|
|
1041
|
+
const result = await page.evaluate(params.script);
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
success: true,
|
|
1045
|
+
result
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* 处理获取标题操作
|
|
1051
|
+
*/
|
|
1052
|
+
async handleGetTitle(page, params) {
|
|
1053
|
+
const { api } = this;
|
|
1054
|
+
|
|
1055
|
+
api?.logger?.info('获取页面标题');
|
|
1056
|
+
const title = await page.title();
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
success: true,
|
|
1060
|
+
title
|
|
1061
|
+
};
|
|
1062
|
+
},
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* 处理获取URL操作
|
|
1066
|
+
*/
|
|
1067
|
+
async handleGetUrl(page, params) {
|
|
1068
|
+
const { api } = this;
|
|
1069
|
+
|
|
1070
|
+
api?.logger?.info('获取页面URL');
|
|
1071
|
+
const url = page.url();
|
|
1072
|
+
|
|
1073
|
+
return {
|
|
1074
|
+
success: true,
|
|
1075
|
+
url
|
|
1076
|
+
};
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* 处理后退操作
|
|
1081
|
+
*/
|
|
1082
|
+
async handleGoBack(page, params) {
|
|
1083
|
+
const { api } = this;
|
|
1084
|
+
|
|
1085
|
+
api?.logger?.info('浏览器后退');
|
|
1086
|
+
await page.goBack();
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
success: true,
|
|
1090
|
+
url: page.url()
|
|
1091
|
+
};
|
|
1092
|
+
},
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 处理前进操作
|
|
1096
|
+
*/
|
|
1097
|
+
async handleGoForward(page, params) {
|
|
1098
|
+
const { api } = this;
|
|
1099
|
+
|
|
1100
|
+
api?.logger?.info('浏览器前进');
|
|
1101
|
+
await page.goForward();
|
|
1102
|
+
|
|
1103
|
+
return {
|
|
1104
|
+
success: true,
|
|
1105
|
+
url: page.url()
|
|
1106
|
+
};
|
|
1107
|
+
},
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* 处理刷新操作
|
|
1111
|
+
*/
|
|
1112
|
+
async handleReload(page, params) {
|
|
1113
|
+
const { api } = this;
|
|
1114
|
+
const options = params.options || {};
|
|
1115
|
+
const waitUntil = options.waitUntil || 'load';
|
|
1116
|
+
|
|
1117
|
+
api?.logger?.info('刷新页面', { waitUntil });
|
|
1118
|
+
await page.reload({ waitUntil });
|
|
1119
|
+
|
|
1120
|
+
return {
|
|
1121
|
+
success: true,
|
|
1122
|
+
url: page.url(),
|
|
1123
|
+
title: await page.title()
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* 处理关闭操作
|
|
1129
|
+
*/
|
|
1130
|
+
async handleClose(browser, context, page, params) {
|
|
1131
|
+
const { api } = this;
|
|
1132
|
+
|
|
1133
|
+
api?.logger?.info('关闭浏览器');
|
|
1134
|
+
await this.cleanupBrowser();
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
success: true,
|
|
1138
|
+
message: '浏览器已关闭'
|
|
1139
|
+
};
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
// --------------------------------------------------------------------------
|
|
1143
|
+
// 资源清理
|
|
1144
|
+
// --------------------------------------------------------------------------
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* 清理浏览器资源(关闭页面、上下文和浏览器)
|
|
1148
|
+
*/
|
|
1149
|
+
async cleanupBrowser() {
|
|
1150
|
+
const { api } = this;
|
|
1151
|
+
const manager = getBrowserManager(this.__toolName);
|
|
1152
|
+
|
|
1153
|
+
// 关闭页面
|
|
1154
|
+
try {
|
|
1155
|
+
if (manager.page && !manager.page.isClosed()) {
|
|
1156
|
+
await manager.page.close();
|
|
1157
|
+
api?.logger?.info('页面已关闭');
|
|
1158
|
+
}
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
api?.logger?.warn('关闭页面时出错', { error: error.message });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 关闭上下文
|
|
1164
|
+
try {
|
|
1165
|
+
if (manager.context && !manager.context.isClosed()) {
|
|
1166
|
+
await manager.context.close();
|
|
1167
|
+
api?.logger?.info('浏览器上下文已关闭');
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
api?.logger?.warn('关闭上下文时出错', { error: error.message });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// 关闭浏览器
|
|
1174
|
+
try {
|
|
1175
|
+
if (manager.browser && manager.browser.isConnected()) {
|
|
1176
|
+
await manager.browser.close();
|
|
1177
|
+
api?.logger?.info('浏览器已关闭');
|
|
1178
|
+
}
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
api?.logger?.warn('关闭浏览器时出错', { error: error.message });
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// 清理全局管理器
|
|
1184
|
+
clearBrowserManager(this.__toolName);
|
|
1185
|
+
}
|
|
1186
|
+
};
|