@taole/dd-playwright 0.1.14 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/SKILL.md +7 -7
- package/bin/cli.mjs +21 -3
- package/bin/mcp-server.mjs +8 -12
- package/package.json +1 -1
- package/src/browser-manager.mjs +39 -45
- package/src/doctor.mjs +160 -0
- package/src/server/daemon.mjs +22 -1
- package/src/server/web-server.mjs +14 -19
- package/src/updater.mjs +13 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
| 工具 | 说明 |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `browser_navigate` | 访问 URL
|
|
11
|
+
| `browser_navigate` | 访问 URL。每次必传 `identity: { mode: "default" }`、`{ mode: "anonymous" }` 或 `{ mode: "uid", uid: "账号UID" }`;支持 `device` 设备仿真和会话级 `userAgent` |
|
|
12
12
|
| `browser_resize` | 调整当前页面渲染视口(对标 playwright-mcp)。只改 innerWidth/innerHeight,不改 UA/触摸;完整移动仿真请用 `browser_navigate` 的 `device` 参数 |
|
|
13
13
|
| `browser_screenshot` | 页面/元素截图,落盘到数据目录并返回路径 |
|
|
14
14
|
| `browser_get_console_errors` | 收集 console.error 与未捕获异常 |
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
| `browser_load_proxy_rules_dir` | 批量加载项目规则目录(如 `.whistle-rules`),全量注入并激活域名映射与 Mock 规则 |
|
|
25
25
|
| `browser_status` | 查询登录态挂载情况、当前 UID、代理状态、数据目录,排查免登问题先调它 |
|
|
26
26
|
|
|
27
|
+
页面操作复用当前页面及身份;没有可用页面时须先导航。相同身份的导航复用上下文,切换身份会关闭旧页面并重建上下文。
|
|
28
|
+
|
|
27
29
|
## 安装与挂载
|
|
28
30
|
|
|
29
31
|
需要 Node.js >=22.13(使用内置 `node:sqlite`)。在本仓库目录执行:
|
|
@@ -67,9 +69,18 @@ dd-pw statusweb
|
|
|
67
69
|
# 停止后台运行的服务:
|
|
68
70
|
dd-pw stopweb
|
|
69
71
|
|
|
72
|
+
# 查看当前安装的版本:
|
|
73
|
+
dd-pw version
|
|
74
|
+
|
|
70
75
|
# 一键检查并升级到最新版本:
|
|
71
76
|
dd-pw update
|
|
72
77
|
|
|
78
|
+
# 环境诊断(Node 版本 / Chromium 内核 / 账号库 / Web 服务 / 版本检查):
|
|
79
|
+
dd-pw doctor
|
|
80
|
+
|
|
81
|
+
# 诊断并自动修复安全项(当前为缺失的 Chromium 内核):
|
|
82
|
+
dd-pw doctor --fix
|
|
83
|
+
|
|
73
84
|
# (可选)前台阻塞运行(Ctrl+C 退出):
|
|
74
85
|
dd-pw serve [--port 12309]
|
|
75
86
|
```
|
package/SKILL.md
CHANGED
|
@@ -10,9 +10,9 @@ description: >-
|
|
|
10
10
|
本技能必须通过 **`dd-playwright` MCP 服务**提供的 `browser_*` 系列工具执行。
|
|
11
11
|
|
|
12
12
|
## 1. 登录态与身份切换
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
13
|
+
- **每次导航必须显式选择身份**:默认账号使用 `identity: { mode: "default" }`;游客使用 `identity: { mode: "anonymous" }`;指定账号使用 `identity: { mode: "uid", uid: "28689781" }`。缺少身份或参数冲突会报错。
|
|
14
|
+
- **身份隔离**:相同身份导航复用上下文;切换身份会关闭当前页面并重建上下文。游客模式不注入登录态,默认账号模式使用当前激活账号或配置的凭据。
|
|
15
|
+
- **页面交互**:填写、点击、截图等操作复用当前页面和身份,无需重复传身份;没有可用页面时先调用 `browser_navigate`。
|
|
16
16
|
|
|
17
17
|
## 2. 本地开发服务联调(Dev Server 代理映射)
|
|
18
18
|
- 当本地启动了前端开发服务器(如 Vite/Webpack 监听本地端口)时,**严禁修改系统 hosts**。
|
|
@@ -20,7 +20,7 @@ description: >-
|
|
|
20
20
|
- **单域名映射示例**:
|
|
21
21
|
- 本地 dev server 运行在 `127.0.0.1:9527` 时,可映射为自定义域名 `p9527-test.tuwan.com`:
|
|
22
22
|
`browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })`
|
|
23
|
-
- 随后调用 `browser_navigate({ url: "http://p9527-test.tuwan.com/" })` 访问,进程内隔离 Whistle
|
|
23
|
+
- 随后调用 `browser_navigate({ url: "http://p9527-test.tuwan.com/", identity: { mode: "default" } })` 访问,进程内隔离 Whistle 代理会自动转发流量并按 Cookie 规则携带登录态。
|
|
24
24
|
- **项目规则目录批量加载(推荐)**:
|
|
25
25
|
- 若项目中存在 `.whistle-rules` 等包含多份代理/Mock 规则文件的目录,直接调用 `browser_load_proxy_rules_dir({ rulesDir: "绝对路径" })` 全量注入激活。
|
|
26
26
|
|
|
@@ -40,16 +40,16 @@ description: >-
|
|
|
40
40
|
- **典型示例**:
|
|
41
41
|
```
|
|
42
42
|
// 完整移动端仿真(视口 390x844 + iPhone UA + 触摸),会话级保持
|
|
43
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13" })
|
|
43
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "iPhone 13" })
|
|
44
44
|
|
|
45
45
|
// 模拟 App 内 WebView:设备仿真 + 自定义 UA(只换 UA,视口/触摸仍按 iPhone 13)
|
|
46
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13", userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) TuwanApp/8.0.0" })
|
|
46
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "iPhone 13", userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) TuwanApp/8.0.0" })
|
|
47
47
|
|
|
48
48
|
// 只调视口验证响应式断点(不改 UA / 触摸)
|
|
49
49
|
browser_resize({ width: 375, height: 812 })
|
|
50
50
|
|
|
51
51
|
// 恢复桌面模式并清除自定义 UA
|
|
52
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAgent: "default" })
|
|
52
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "desktop", userAgent: "default" })
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
## 4. 页面交互规范
|
package/bin/cli.mjs
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
startWebDaemon,
|
|
7
7
|
stopWebDaemon,
|
|
8
8
|
getWebDaemonStatus,
|
|
9
|
+
getStoredWebPort,
|
|
9
10
|
} from "../src/server/daemon.mjs";
|
|
10
|
-
import { handleUpdateCommand } from "../src/updater.mjs";
|
|
11
|
+
import { getLocalVersion, handleUpdateCommand } from "../src/updater.mjs";
|
|
12
|
+
import { runDoctor, formatDoctorReport } from "../src/doctor.mjs";
|
|
11
13
|
|
|
12
14
|
const args = process.argv.slice(2);
|
|
13
15
|
const command = args[0];
|
|
@@ -21,6 +23,8 @@ dd-playwright (别名: dd-pw) CLI
|
|
|
21
23
|
dd-pw stopweb 停止后台运行的 Web 控制台
|
|
22
24
|
dd-pw statusweb 查看后台 Web 控制台运行状态与端口
|
|
23
25
|
dd-pw serve [--port 12309] 前台阻塞启动 Web 控制台(按 Ctrl+C 退出)
|
|
26
|
+
dd-pw version, --version, -v 查看当前版本
|
|
27
|
+
dd-pw doctor [--fix] 诊断环境(Node/Chromium/数据库/Web 服务/版本)
|
|
24
28
|
dd-pw update 检查远端最新版本并自动升级
|
|
25
29
|
dd-pw mcp 以 stdio 模式启动 MCP 浏览器服务
|
|
26
30
|
dd-pw --help, -h 查看帮助信息
|
|
@@ -37,7 +41,9 @@ async function main() {
|
|
|
37
41
|
process.exit(0);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
if (command === "
|
|
44
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
45
|
+
console.log(getLocalVersion());
|
|
46
|
+
} else if (command === "startweb") {
|
|
41
47
|
let port = 12309;
|
|
42
48
|
const portIdx = args.indexOf("--port");
|
|
43
49
|
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
@@ -96,7 +102,10 @@ async function main() {
|
|
|
96
102
|
} else if (command === "statusweb") {
|
|
97
103
|
const config = resolveConfig();
|
|
98
104
|
try {
|
|
99
|
-
const status = await getWebDaemonStatus(
|
|
105
|
+
const status = await getWebDaemonStatus(
|
|
106
|
+
config.webPidFile,
|
|
107
|
+
getStoredWebPort(config.dataDir) || 12309,
|
|
108
|
+
);
|
|
100
109
|
if (status.running) {
|
|
101
110
|
console.log(`
|
|
102
111
|
[dd-playwright] 后台 Web 服务状态:
|
|
@@ -160,6 +169,15 @@ async function main() {
|
|
|
160
169
|
console.error(`[dd-playwright] 启动失败:`, err);
|
|
161
170
|
process.exit(1);
|
|
162
171
|
}
|
|
172
|
+
} else if (command === "doctor") {
|
|
173
|
+
const report = await runDoctor({
|
|
174
|
+
config: resolveConfig(),
|
|
175
|
+
fix: args.includes("--fix"),
|
|
176
|
+
});
|
|
177
|
+
console.log(formatDoctorReport(report));
|
|
178
|
+
// 用 exitCode 而非 process.exit():Windows 下显式退出与在途 fetch 的
|
|
179
|
+
// handle 关闭竞争会触发 uv async.c 断言崩溃
|
|
180
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
163
181
|
} else if (command === "update" || command === "upgrade") {
|
|
164
182
|
await handleUpdateCommand();
|
|
165
183
|
} else if (command === "mcp") {
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -37,20 +37,16 @@ export function createBrowserMcpServer({ manager, config }) {
|
|
|
37
37
|
{
|
|
38
38
|
description:
|
|
39
39
|
"使用内置 Playwright 访问目标 URL。" +
|
|
40
|
-
"
|
|
41
|
-
"若传 anonymous: true 则以完全未登录的游客身份访问;" +
|
|
40
|
+
"必须通过 identity 指定匿名、默认账号或指定 UID 的身份;" +
|
|
42
41
|
"移动端/H5 验收可传 device 启用完整设备仿真(视口+UA+触摸),也可单独传 userAgent 自定义 UA。" +
|
|
43
42
|
"返回 HTTP 状态码、页面标题与控制台报错统计。",
|
|
44
43
|
inputSchema: {
|
|
45
44
|
url: z.string().describe("需要访问的目标页面 URL"),
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
anonymous
|
|
51
|
-
.boolean()
|
|
52
|
-
.optional()
|
|
53
|
-
.describe("是否以匿名/未登录状态访问(默认 false)。若为 true,则不注入任何 Cookie/storageState"),
|
|
45
|
+
identity: z.discriminatedUnion("mode", [
|
|
46
|
+
z.strictObject({ mode: z.literal("anonymous") }),
|
|
47
|
+
z.strictObject({ mode: z.literal("default") }),
|
|
48
|
+
z.strictObject({ mode: z.literal("uid"), uid: z.string().min(1).refine((uid) => uid.trim().length > 0) }),
|
|
49
|
+
]).describe("必填身份:anonymous、default 或 uid(需提供非空 uid)"),
|
|
54
50
|
device: z
|
|
55
51
|
.string()
|
|
56
52
|
.optional()
|
|
@@ -78,10 +74,10 @@ export function createBrowserMcpServer({ manager, config }) {
|
|
|
78
74
|
.describe("导航与等待超时时间(毫秒),默认 15000"),
|
|
79
75
|
},
|
|
80
76
|
},
|
|
81
|
-
async ({ url,
|
|
77
|
+
async ({ url, identity, device, userAgent, waitFor, timeout }) => {
|
|
82
78
|
try {
|
|
83
79
|
return textResult(
|
|
84
|
-
await manager.navigate({ url,
|
|
80
|
+
await manager.navigate({ url, identity, device, userAgent, waitFor, timeout }),
|
|
85
81
|
);
|
|
86
82
|
} catch (err) {
|
|
87
83
|
return errorResult("导航失败", err);
|
package/package.json
CHANGED
package/src/browser-manager.mjs
CHANGED
|
@@ -137,8 +137,7 @@ export class PlaywrightBrowserManager {
|
|
|
137
137
|
storageState !== undefined
|
|
138
138
|
? storageState
|
|
139
139
|
: resolveStorageState({ dbPath, storageStateFile });
|
|
140
|
-
this.
|
|
141
|
-
this.isAnonymous = false;
|
|
140
|
+
this.identity = null;
|
|
142
141
|
this.device = null; // 当前设备描述符名;null 表示桌面模式
|
|
143
142
|
this.sessionUserAgent = null; // 会话级 UA(工具参数设置);null 表示回落到 this.userAgent / 设备描述符 / 默认
|
|
144
143
|
this.browser = null;
|
|
@@ -185,14 +184,17 @@ export class PlaywrightBrowserManager {
|
|
|
185
184
|
|
|
186
185
|
|
|
187
186
|
/**
|
|
188
|
-
*
|
|
189
|
-
* @param {{
|
|
190
|
-
* device 不传则保持当前模式;传 "desktop"/null 恢复桌面;传 Playwright 设备名启用移动端仿真。
|
|
191
|
-
* userAgent 不传则保持当前会话级 UA;传 "default"/null 清除会话级 UA(回落到启动配置/设备描述符/默认 UA)
|
|
187
|
+
* 根据身份、设备描述符与会话级 UA 准备浏览器上下文。
|
|
188
|
+
* @param {{identity: {mode: 'anonymous'|'default'|'uid', uid?: string}, device?: string|null, userAgent?: string|null}} options
|
|
192
189
|
*/
|
|
193
|
-
async ensurePage({
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
async ensurePage({ identity, device, userAgent } = {}) {
|
|
191
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity) ||
|
|
192
|
+
!["anonymous", "default", "uid"].includes(identity.mode) ||
|
|
193
|
+
Object.keys(identity).some((key) => key !== "mode" && key !== "uid") ||
|
|
194
|
+
(identity.mode === "uid" ? typeof identity.uid !== "string" || !identity.uid.trim() : Object.hasOwn(identity, "uid"))) {
|
|
195
|
+
throw new Error("identity 必须指定 anonymous、default 或带非空 uid 的 uid 模式");
|
|
196
|
+
}
|
|
197
|
+
const targetUid = identity.mode === "uid" ? identity.uid : null;
|
|
196
198
|
|
|
197
199
|
// 先解析并校验设备(非法设备名必须在关闭现有上下文之前抛错,避免误毁会话)
|
|
198
200
|
let targetDevice = this.device;
|
|
@@ -211,11 +213,15 @@ export class PlaywrightBrowserManager {
|
|
|
211
213
|
}
|
|
212
214
|
const uaChanged = targetSessionUA !== this.sessionUserAgent;
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
(
|
|
216
|
+
const authChanged = !this.identity || this.identity.mode !== identity.mode ||
|
|
217
|
+
(identity.mode === "uid" && this.identity.uid !== targetUid);
|
|
218
|
+
let state;
|
|
219
|
+
if (authChanged || deviceChanged || uaChanged || !this.context) {
|
|
220
|
+
if (identity.mode === "anonymous") state = null;
|
|
221
|
+
else if (identity.mode === "uid") state = resolveStorageState({ dbPath: this.dbPath, uid: targetUid });
|
|
222
|
+
else if (this.customStorageState !== undefined) state = this.customStorageState;
|
|
223
|
+
else state = resolveStorageState({ dbPath: this.dbPath, storageStateFile: this.storageStateFile });
|
|
224
|
+
}
|
|
219
225
|
|
|
220
226
|
if ((authChanged || deviceChanged || uaChanged) && this.context) {
|
|
221
227
|
if (this.page && !this.page.isClosed()) {
|
|
@@ -236,26 +242,8 @@ export class PlaywrightBrowserManager {
|
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
if (!this.context) {
|
|
239
|
-
let state = null;
|
|
240
|
-
if (targetAnonymous) {
|
|
241
|
-
state = null;
|
|
242
|
-
} else if (targetUid) {
|
|
243
|
-
state = resolveStorageState({
|
|
244
|
-
dbPath: this.dbPath,
|
|
245
|
-
uid: targetUid,
|
|
246
|
-
});
|
|
247
|
-
} else if (this.customStorageState !== undefined) {
|
|
248
|
-
state = this.customStorageState;
|
|
249
|
-
} else {
|
|
250
|
-
state = resolveStorageState({
|
|
251
|
-
dbPath: this.dbPath,
|
|
252
|
-
storageStateFile: this.storageStateFile,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
245
|
this.storageState = state;
|
|
257
|
-
this.
|
|
258
|
-
this.isAnonymous = targetAnonymous;
|
|
246
|
+
this.identity = targetUid === null ? { mode: identity.mode } : { mode: "uid", uid: targetUid };
|
|
259
247
|
this.device = targetDevice;
|
|
260
248
|
this.sessionUserAgent = targetSessionUA;
|
|
261
249
|
|
|
@@ -303,8 +291,8 @@ export class PlaywrightBrowserManager {
|
|
|
303
291
|
return this.page;
|
|
304
292
|
}
|
|
305
293
|
|
|
306
|
-
async navigate({ url, waitFor, timeout = 15000,
|
|
307
|
-
const page = await this.ensurePage({
|
|
294
|
+
async navigate({ url, waitFor, timeout = 15000, identity, device, userAgent }) {
|
|
295
|
+
const page = await this.ensurePage({ identity, device, userAgent });
|
|
308
296
|
const response = await page.goto(url, {
|
|
309
297
|
timeout,
|
|
310
298
|
waitUntil: "domcontentloaded",
|
|
@@ -325,8 +313,13 @@ export class PlaywrightBrowserManager {
|
|
|
325
313
|
};
|
|
326
314
|
}
|
|
327
315
|
|
|
316
|
+
currentPage() {
|
|
317
|
+
if (!this.page || this.page.isClosed()) throw new Error("没有当前页面,请先使用 browser_navigate");
|
|
318
|
+
return this.page;
|
|
319
|
+
}
|
|
320
|
+
|
|
328
321
|
async screenshot({ selector, fullPage = false } = {}) {
|
|
329
|
-
const page =
|
|
322
|
+
const page = this.currentPage();
|
|
330
323
|
const dir = this.screenshotsDir;
|
|
331
324
|
mkdirSync(dir, { recursive: true });
|
|
332
325
|
const filename = `screenshot-${Date.now()}-${Math.random()
|
|
@@ -349,6 +342,7 @@ export class PlaywrightBrowserManager {
|
|
|
349
342
|
}
|
|
350
343
|
|
|
351
344
|
getConsoleErrors() {
|
|
345
|
+
this.currentPage();
|
|
352
346
|
return {
|
|
353
347
|
total: this.consoleErrors.length,
|
|
354
348
|
errors: [...this.consoleErrors],
|
|
@@ -356,7 +350,7 @@ export class PlaywrightBrowserManager {
|
|
|
356
350
|
}
|
|
357
351
|
|
|
358
352
|
async clickElement({ selector, timeout = 10000 }) {
|
|
359
|
-
const page =
|
|
353
|
+
const page = this.currentPage();
|
|
360
354
|
const locator = page.locator(selector).first();
|
|
361
355
|
await locator.click({ timeout });
|
|
362
356
|
return {
|
|
@@ -366,7 +360,7 @@ export class PlaywrightBrowserManager {
|
|
|
366
360
|
};
|
|
367
361
|
}
|
|
368
362
|
async fillText({ selector, text, timeout = 10000 }) {
|
|
369
|
-
const page =
|
|
363
|
+
const page = this.currentPage();
|
|
370
364
|
const locator = page.locator(selector).first();
|
|
371
365
|
await locator.fill(String(text), { timeout });
|
|
372
366
|
return {
|
|
@@ -378,7 +372,7 @@ export class PlaywrightBrowserManager {
|
|
|
378
372
|
}
|
|
379
373
|
|
|
380
374
|
async pressKey({ key, selector, timeout = 10000 }) {
|
|
381
|
-
const page =
|
|
375
|
+
const page = this.currentPage();
|
|
382
376
|
if (selector) {
|
|
383
377
|
const locator = page.locator(selector).first();
|
|
384
378
|
await locator.press(key, { timeout });
|
|
@@ -394,7 +388,7 @@ export class PlaywrightBrowserManager {
|
|
|
394
388
|
}
|
|
395
389
|
|
|
396
390
|
async hover({ selector, timeout = 10000 }) {
|
|
397
|
-
const page =
|
|
391
|
+
const page = this.currentPage();
|
|
398
392
|
const locator = page.locator(selector).first();
|
|
399
393
|
await locator.hover({ timeout });
|
|
400
394
|
return {
|
|
@@ -405,7 +399,7 @@ export class PlaywrightBrowserManager {
|
|
|
405
399
|
}
|
|
406
400
|
|
|
407
401
|
async scroll({ direction = "down", deltaY, selector } = {}) {
|
|
408
|
-
const page =
|
|
402
|
+
const page = this.currentPage();
|
|
409
403
|
if (selector) {
|
|
410
404
|
const locator = page.locator(selector).first();
|
|
411
405
|
await locator.scrollIntoViewIfNeeded();
|
|
@@ -432,7 +426,7 @@ export class PlaywrightBrowserManager {
|
|
|
432
426
|
}
|
|
433
427
|
|
|
434
428
|
async waitFor({ selector, state = "visible", timeout = 15000 }) {
|
|
435
|
-
const page =
|
|
429
|
+
const page = this.currentPage();
|
|
436
430
|
if (selector) {
|
|
437
431
|
await page.waitForSelector(selector, { state, timeout });
|
|
438
432
|
return {
|
|
@@ -451,7 +445,7 @@ export class PlaywrightBrowserManager {
|
|
|
451
445
|
}
|
|
452
446
|
|
|
453
447
|
async getDom({ interactiveOnly = true, maxDepth = 10 } = {}) {
|
|
454
|
-
const page =
|
|
448
|
+
const page = this.currentPage();
|
|
455
449
|
const snapshot = await page.evaluate(
|
|
456
450
|
({ onlyInteractive, maxD }) => {
|
|
457
451
|
function isVisible(el) {
|
|
@@ -554,7 +548,7 @@ export class PlaywrightBrowserManager {
|
|
|
554
548
|
* @param {{width: number, height: number}} params
|
|
555
549
|
*/
|
|
556
550
|
async resize({ width, height }) {
|
|
557
|
-
const page =
|
|
551
|
+
const page = this.currentPage();
|
|
558
552
|
const viewport = { width: Number(width), height: Number(height) };
|
|
559
553
|
await page.setViewportSize(viewport);
|
|
560
554
|
return {
|
|
@@ -565,7 +559,7 @@ export class PlaywrightBrowserManager {
|
|
|
565
559
|
}
|
|
566
560
|
|
|
567
561
|
async evalJs({ script }) {
|
|
568
|
-
const page =
|
|
562
|
+
const page = this.currentPage();
|
|
569
563
|
const result = await page.evaluate((code) => {
|
|
570
564
|
// eslint-disable-next-line no-eval
|
|
571
565
|
return window.eval(code);
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dd-pw doctor:环境健康度诊断。只读检查,不启动浏览器/代理,不做修复。
|
|
3
|
+
* 每项检查返回 { name, ok, detail, hint };exit code 由汇总结果决定。
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, accessSync, constants, readFileSync } from "node:fs";
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getChromiumMetadata } from "./browser-check.mjs";
|
|
9
|
+
import { getLocalVersion, getRemoteLatestVersion, compareSemver } from "./updater.mjs";
|
|
10
|
+
import { getStoredWebPort, getStoredWebPid } from "./server/daemon.mjs";
|
|
11
|
+
|
|
12
|
+
const MIN_NODE = [22, 13, 0];
|
|
13
|
+
|
|
14
|
+
function check(name, fn) {
|
|
15
|
+
return Promise.resolve()
|
|
16
|
+
.then(fn)
|
|
17
|
+
.then((r) => ({ name, ok: r.ok !== false, detail: r.detail ?? "", hint: r.hint ?? "" }))
|
|
18
|
+
.catch((err) => ({ name, ok: false, detail: String(err.message || err), hint: "" }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkNodeVersion() {
|
|
22
|
+
const [maj, min, pat] = process.versions.node.split(".").map((n) => parseInt(n, 10));
|
|
23
|
+
const ok =
|
|
24
|
+
maj > MIN_NODE[0] ||
|
|
25
|
+
(maj === MIN_NODE[0] && min > MIN_NODE[1]) ||
|
|
26
|
+
(maj === MIN_NODE[0] && min === MIN_NODE[1] && pat >= MIN_NODE[2]);
|
|
27
|
+
return {
|
|
28
|
+
detail: `当前 ${process.version},要求 >= ${MIN_NODE.join(".")}`,
|
|
29
|
+
ok,
|
|
30
|
+
hint: ok ? "" : "升级 Node.js 后重试(依赖内置 node:sqlite)",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function checkChromium(fix) {
|
|
35
|
+
const meta = await getChromiumMetadata();
|
|
36
|
+
if (meta.installed && meta.version) {
|
|
37
|
+
return { detail: `${meta.version} (${meta.executablePath})` };
|
|
38
|
+
}
|
|
39
|
+
const hint = "执行 npx playwright install chromium 补齐内核";
|
|
40
|
+
if (fix) {
|
|
41
|
+
const { ensureChromiumInstalled } = await import("./browser-check.mjs");
|
|
42
|
+
const done = await ensureChromiumInstalled({ silent: true });
|
|
43
|
+
if (done) {
|
|
44
|
+
const after = await getChromiumMetadata();
|
|
45
|
+
return { detail: `已自动修复:${after.version} (${after.executablePath})` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, detail: meta.error || `内核缺失:${meta.executablePath || "未知路径"}`, hint };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkDataDir(config) {
|
|
52
|
+
const dir = config.dataDir;
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
return { ok: true, detail: `${dir} 尚不存在(首次使用时自动创建)` };
|
|
55
|
+
}
|
|
56
|
+
accessSync(dir, constants.R_OK | constants.W_OK);
|
|
57
|
+
return { detail: `${dir} 可读写` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkDatabase(config) {
|
|
61
|
+
const dbPath = config.dbPath;
|
|
62
|
+
if (!existsSync(dbPath)) {
|
|
63
|
+
return { ok: true, detail: `${dbPath} 不存在(未绑定账号属正常)` };
|
|
64
|
+
}
|
|
65
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
66
|
+
try {
|
|
67
|
+
const tables = db
|
|
68
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
69
|
+
.all()
|
|
70
|
+
.map((r) => r.name);
|
|
71
|
+
const missing = ["diandian_accounts", "app_settings"].filter((t) => !tables.includes(t));
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
detail: `数据库缺少表:${missing.join(", ")}`,
|
|
76
|
+
hint: "数据库可能来自旧版本或已损坏;可备份后删除 runtime.db 重新扫码绑定",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const row = db
|
|
80
|
+
.prepare("SELECT account_id, nickname, storage_state FROM diandian_accounts WHERE is_active = 1 LIMIT 1")
|
|
81
|
+
.get();
|
|
82
|
+
if (!row) {
|
|
83
|
+
return { ok: true, detail: "数据库正常,未设置激活账号" };
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
JSON.parse(row.storage_state);
|
|
87
|
+
return { detail: `激活账号 ${row.account_id}(${row.nickname || "未知昵称"}),storageState 可解析` };
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
detail: `激活账号 ${row.account_id} 的 storageState 无法解析`,
|
|
92
|
+
hint: "在 Web 控制台重新扫码绑定该账号",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function checkWebDaemon(config) {
|
|
101
|
+
const pid = getStoredWebPid(config.webPidFile);
|
|
102
|
+
if (!pid) {
|
|
103
|
+
return { ok: true, detail: "后台 Web 控制台未运行(dd-pw startweb 启动)" };
|
|
104
|
+
}
|
|
105
|
+
const storedPort = getStoredWebPort(config.dataDir) || 12309;
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(`http://127.0.0.1:${storedPort}/api/accounts`, {
|
|
108
|
+
signal: AbortSignal.timeout(2000),
|
|
109
|
+
});
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
return { detail: `运行中 PID=${pid},http://127.0.0.1:${storedPort} 可访问` };
|
|
112
|
+
}
|
|
113
|
+
return { ok: false, detail: `PID=${pid} 存在但端口 ${storedPort} 响应 ${res.status}`, hint: "dd-pw stopweb 后重新 startweb" };
|
|
114
|
+
} catch {
|
|
115
|
+
return { ok: false, detail: `PID=${pid} 存在但端口 ${storedPort} 不可访问`, hint: "PID 文件可能残留,执行 dd-pw stopweb 清理后重新 startweb" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkVersion() {
|
|
120
|
+
const local = getLocalVersion();
|
|
121
|
+
const remote = await getRemoteLatestVersion();
|
|
122
|
+
if (!remote) {
|
|
123
|
+
return { ok: true, detail: `本地 ${local},远端版本查询失败(网络问题,不影响使用)` };
|
|
124
|
+
}
|
|
125
|
+
const cmp = compareSemver(local, remote);
|
|
126
|
+
if (cmp < 0) {
|
|
127
|
+
return { ok: false, detail: `本地 ${local} 落后于远端 ${remote}`, hint: "执行 dd-pw update 升级" };
|
|
128
|
+
}
|
|
129
|
+
return { detail: `本地 ${local} 已是最新` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 执行全部诊断
|
|
134
|
+
* @param {object} options
|
|
135
|
+
* @param {object} options.config resolveConfig() 结果
|
|
136
|
+
* @param {boolean} [options.fix] 允许自动修复安全项(当前仅 Chromium 内核缺失)
|
|
137
|
+
* @returns {Promise<{ok: boolean, results: Array}>}
|
|
138
|
+
*/
|
|
139
|
+
export async function runDoctor({ config, fix = false }) {
|
|
140
|
+
const results = await Promise.all([
|
|
141
|
+
check("Node 版本", checkNodeVersion),
|
|
142
|
+
check("Chromium 内核", () => checkChromium(fix)),
|
|
143
|
+
check("数据目录", () => checkDataDir(config)),
|
|
144
|
+
check("账号数据库", () => checkDatabase(config)),
|
|
145
|
+
check("Web 控制台", () => checkWebDaemon(config)),
|
|
146
|
+
check("包版本", checkVersion),
|
|
147
|
+
]);
|
|
148
|
+
return { ok: results.every((r) => r.ok), results };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatDoctorReport({ ok, results }) {
|
|
152
|
+
const lines = results.map((r) => {
|
|
153
|
+
const mark = r.ok ? "✔" : "✘";
|
|
154
|
+
let line = `${mark} ${r.name}: ${r.detail}`;
|
|
155
|
+
if (!r.ok && r.hint) line += `\n 建议: ${r.hint}`;
|
|
156
|
+
return line;
|
|
157
|
+
});
|
|
158
|
+
lines.push(ok ? "\n全部检查通过。" : `\n发现 ${results.filter((r) => !r.ok).length} 项问题。`);
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
package/src/server/daemon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, openSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -110,6 +110,10 @@ export async function startWebDaemon({ port, dataDir, pidFile, logFile }) {
|
|
|
110
110
|
|
|
111
111
|
const pid = child.pid;
|
|
112
112
|
writeFileSync(pidFile, String(pid), "utf8");
|
|
113
|
+
// 记录监听端口,供 update 重启时恢复原端口(避免回落到默认 12309)
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(join(dataDir, "web.port"), String(port), "utf8");
|
|
116
|
+
} catch {}
|
|
113
117
|
child.unref();
|
|
114
118
|
|
|
115
119
|
// 等待最多 5 秒检测端口服务是否就绪
|
|
@@ -135,6 +139,21 @@ export async function startWebDaemon({ port, dataDir, pidFile, logFile }) {
|
|
|
135
139
|
};
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* 读取后台 Web 服务启动时记录的监听端口
|
|
144
|
+
* @param {string} dataDir
|
|
145
|
+
* @returns {number|null} 端口;未记录或非法时返回 null(调用方回落默认端口)
|
|
146
|
+
*/
|
|
147
|
+
export function getStoredWebPort(dataDir) {
|
|
148
|
+
try {
|
|
149
|
+
const raw = readFileSync(join(dataDir, "web.port"), "utf8").trim();
|
|
150
|
+
const port = parseInt(raw, 10);
|
|
151
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
/**
|
|
139
158
|
* 停止后台常驻 Web 服务
|
|
140
159
|
* @param {string} pidFile
|
|
@@ -151,6 +170,8 @@ export async function stopWebDaemon(pidFile) {
|
|
|
151
170
|
if (existsSync(pidFile)) {
|
|
152
171
|
unlinkSync(pidFile);
|
|
153
172
|
}
|
|
173
|
+
// 同步清理端口记录;update 场景需先读取端口再调用本函数
|
|
174
|
+
unlinkSync(join(dirname(pidFile), "web.port"));
|
|
154
175
|
} catch {}
|
|
155
176
|
|
|
156
177
|
return { stopped: true, pid };
|
|
@@ -347,9 +347,9 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
347
347
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
348
348
|
<div class="card-title" style="margin-bottom: 0;">Skill 规范说明 (SKILL.md)</div>
|
|
349
349
|
<div style="display: flex; gap: 8px;">
|
|
350
|
-
<a href="/api/skill/download" download="SKILL.
|
|
350
|
+
<a href="/api/skill/download" download="DD-PLAYWRIGHT-SKILL.MD" class="btn" style="background: #334155; color: white; display: flex; align-items: center; gap: 6px; font-weight: 500; padding: 7px 12px; text-decoration: none;">
|
|
351
351
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
352
|
-
下载 SKILL.
|
|
352
|
+
下载 DD-PLAYWRIGHT-SKILL.MD
|
|
353
353
|
</a>
|
|
354
354
|
<button class="btn" onclick="installSkillToCursor()" style="background: #0284c7; color: white; display: flex; align-items: center; gap: 7px; font-weight: 600; padding: 7px 14px;">
|
|
355
355
|
<svg width="14" height="16" viewBox="0 0 467 533" fill="currentColor"><path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/></svg>
|
|
@@ -368,8 +368,8 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
368
368
|
|
|
369
369
|
<div style="background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 14px; margin-top: 10px; font-size: 13px; line-height: 1.8; color: #cbd5e1;">
|
|
370
370
|
<b>1. 登录态与身份切换:</b><br>
|
|
371
|
-
•
|
|
372
|
-
•
|
|
371
|
+
• 每次导航必须通过 <code>identity</code> 选择身份:默认账号 <code>{ mode: "default" }</code>、游客 <code>{ mode: "anonymous" }</code> 或指定账号 <code>{ mode: "uid", uid: "账号UID" }</code>。<br>
|
|
372
|
+
• 切换身份会重建上下文;页面交互复用当前页面与身份。<br><br>
|
|
373
373
|
<b>2. 本地开发服务联调(Dev Server 代理映射):</b><br>
|
|
374
374
|
• 本地服务启动后严禁改动系统 hosts,可通过单域名映射或项目规则目录全量注入:<br>
|
|
375
375
|
• <b>单条映射</b>:<code>browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })</code><br>
|
|
@@ -399,8 +399,8 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
399
399
|
<div class="code-block" style="color: #93c5fd; font-size: 12px; margin-bottom: 12px;">// 方式 A:单域名映射
|
|
400
400
|
1. 本地 Vite 启动在 127.0.0.1:9527
|
|
401
401
|
2. 映射代理域名:browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })
|
|
402
|
-
3. 免登访问页面:browser_navigate({ url: "http://p9527-test.tuwan.com/" })
|
|
403
|
-
->
|
|
402
|
+
3. 免登访问页面:browser_navigate({ url: "http://p9527-test.tuwan.com/", identity: { mode: "default" } })
|
|
403
|
+
-> 按浏览器 Cookie 规则携带登录态,无需改动系统 hosts!
|
|
404
404
|
|
|
405
405
|
// 方式 B:项目规则目录批量导入(推荐)
|
|
406
406
|
1. 项目中已存在 .whistle-rules 目录(含域名映射、接口 Mock 等多份规则)
|
|
@@ -409,13 +409,13 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
409
409
|
|
|
410
410
|
<div class="code-title"><span>流程 2:游客拦截 vs 正常登录态对比测试</span></div>
|
|
411
411
|
<div class="code-block" style="color: #93c5fd; font-size: 12px; margin-bottom: 12px;">// 1. 游客态验证(验证未登录拦截弹窗/导流流程)
|
|
412
|
-
browser_navigate({ url: "https://y.tuwan.com/activity/...",
|
|
412
|
+
browser_navigate({ url: "https://y.tuwan.com/activity/...", identity: { mode: "anonymous" } })
|
|
413
413
|
|
|
414
414
|
// 2. 默认登录态验证(自动携带默认账号正常参与活动)
|
|
415
|
-
browser_navigate({ url: "https://y.tuwan.com/activity/..." })
|
|
415
|
+
browser_navigate({ url: "https://y.tuwan.com/activity/...", identity: { mode: "default" } })
|
|
416
416
|
|
|
417
417
|
// 3. 指定测试账号(切换特定身份排查权限或对账)
|
|
418
|
-
browser_navigate({ url: "https://y.tuwan.com/activity/...", uid: "28689781" })</div>
|
|
418
|
+
browser_navigate({ url: "https://y.tuwan.com/activity/...", identity: { mode: "uid", uid: "28689781" } })</div>
|
|
419
419
|
|
|
420
420
|
<div class="code-title"><span>流程 3:交互排查与元素定位</span></div>
|
|
421
421
|
<div class="code-block" style="color: #93c5fd; font-size: 12px; margin-bottom: 0;">// 1. 获取页面结构化交互元素,杜绝盲猜选择器
|
|
@@ -431,16 +431,16 @@ const errors = browser_get_console_errors()</div>
|
|
|
431
431
|
|
|
432
432
|
<div class="code-title"><span>流程 4:移动端 / H5 仿真验收(设备 · UA · 视口)</span></div>
|
|
433
433
|
<div class="code-block" style="color: #93c5fd; font-size: 12px; margin-bottom: 0;">// 1. 完整移动端仿真:视口 390x844 + iPhone UA + 触摸,会话级保持
|
|
434
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13" })
|
|
434
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "iPhone 13" })
|
|
435
435
|
|
|
436
436
|
// 2. 模拟 App 内 WebView:设备仿真 + 自定义 UA(只换 UA,视口/触摸仍按 iPhone 13)
|
|
437
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13", userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) TuwanApp/8.0.0" })
|
|
437
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "iPhone 13", userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) TuwanApp/8.0.0" })
|
|
438
438
|
|
|
439
439
|
// 3. 只调视口验证响应式断点(不改 UA / 触摸)
|
|
440
440
|
browser_resize({ width: 375, height: 812 })
|
|
441
441
|
|
|
442
442
|
// 4. 恢复桌面模式并清除自定义 UA
|
|
443
|
-
browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAgent: "default" })</div>
|
|
443
|
+
browser_navigate({ url: "https://xdd-test.tuwan.com/", identity: { mode: "default" }, device: "desktop", userAgent: "default" })</div>
|
|
444
444
|
</div>
|
|
445
445
|
</div>
|
|
446
446
|
</div>
|
|
@@ -463,11 +463,6 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
|
|
|
463
463
|
<span id="headlessKnob" style="position: absolute; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .2s; border-radius: 50%;"></span>
|
|
464
464
|
</label>
|
|
465
465
|
</div>
|
|
466
|
-
<div style="margin-top: 16px; font-size: 12px; color: var(--muted); line-height: 1.6; padding: 12px; background: #090d16; border-radius: 6px; border: 1px solid var(--border);">
|
|
467
|
-
📁 <b>数据存储说明:</b><br>
|
|
468
|
-
本服务所有数据与账号库已默认固化在用户主目录下的 <code>~/.dd-browser-mcp/runtime.db</code>,无需配置任何复杂环境变量,开箱即用。
|
|
469
|
-
</div>
|
|
470
|
-
|
|
471
466
|
<div style="margin-top: 20px; padding: 16px; background: #0f172a; border-radius: 8px; border: 1px solid var(--border);">
|
|
472
467
|
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">软件更新</div>
|
|
473
468
|
<div id="updateInfo" style="font-size: 12px; color: var(--muted); line-height: 1.8; margin-bottom: 12px;" aria-live="polite">点击检查更新,查看是否有新版本。</div>
|
|
@@ -697,7 +692,7 @@ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAg
|
|
|
697
692
|
'---',
|
|
698
693
|
'# dd-playwright 自动化指南',
|
|
699
694
|
'本技能必须通过 dd-playwright MCP 服务提供的 browser_* 系列工具执行:',
|
|
700
|
-
'1.
|
|
695
|
+
'1. 登录态与身份切换:每次 browser_navigate 必须传 identity:默认账号 { mode: "default" },游客 { mode: "anonymous" },指定账号 { mode: "uid", uid: "账号UID" }。页面操作复用当前身份。',
|
|
701
696
|
'2. 本地 Dev Server 联调:严禁改动系统 hosts。支持 browser_set_proxy_rule 单条映射,或 browser_load_proxy_rules_dir 批量加载 .whistle-rules 目录(语法如:域名映射 p9527-test.tuwan.com http://127.0.0.1:9527、接口 Mock yapi.tuwan.com/act resBody://{mock.json}、官方文档 https://wproxy.org/whistle/rules/)。',
|
|
702
697
|
'3. 页面交互规范:定位困难时优先调用 browser_get_dom 提取可见元素结构,严禁盲猜选择器;表单填写使用 browser_fill_text,按键使用 browser_press_key。'
|
|
703
698
|
].join('\\n');
|
|
@@ -910,7 +905,7 @@ export function createWebServer({ accountStore, authManager, updater, port = 123
|
|
|
910
905
|
const content = readFileSync(skillPath, "utf8");
|
|
911
906
|
res.writeHead(200, {
|
|
912
907
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
913
|
-
"Content-Disposition": 'attachment; filename="SKILL.
|
|
908
|
+
"Content-Disposition": 'attachment; filename="DD-PLAYWRIGHT-SKILL.MD"',
|
|
914
909
|
"Cache-Control": "no-cache",
|
|
915
910
|
});
|
|
916
911
|
res.end(content);
|
package/src/updater.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { resolveConfig } from "./config.mjs";
|
|
5
5
|
import {
|
|
6
6
|
getStoredWebPid,
|
|
7
|
+
getStoredWebPort,
|
|
7
8
|
stopWebDaemon,
|
|
8
9
|
startWebDaemon,
|
|
9
10
|
} from "./server/daemon.mjs";
|
|
@@ -100,6 +101,8 @@ export async function handleUpdateCommand(overrideCurrentVersion = null) {
|
|
|
100
101
|
const config = resolveConfig();
|
|
101
102
|
const runningPid = getStoredWebPid(config.webPidFile);
|
|
102
103
|
const wasWebRunning = Boolean(runningPid);
|
|
104
|
+
// 停止前先读取服务启动时记录的端口,重启时恢复原端口
|
|
105
|
+
const webPort = getStoredWebPort(config.dataDir) || 12309;
|
|
103
106
|
|
|
104
107
|
if (wasWebRunning) {
|
|
105
108
|
console.log(
|
|
@@ -119,10 +122,19 @@ export async function handleUpdateCommand(overrideCurrentVersion = null) {
|
|
|
119
122
|
});
|
|
120
123
|
console.log(`\n🎉 自动升级成功!已成功升级至最新版本 v${latestVersion}。\n`);
|
|
121
124
|
|
|
125
|
+
// 升级后 Playwright 期望的 Chromium revision 可能变化,自动补齐内核,
|
|
126
|
+
// 避免下次启动 MCP 时报“浏览器二进制缺失”
|
|
127
|
+
try {
|
|
128
|
+
const { ensureChromiumInstalled } = await import("./browser-check.mjs");
|
|
129
|
+
await ensureChromiumInstalled();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(`[dd-playwright] Chromium 内核自动补齐失败: ${err.message}`);
|
|
132
|
+
console.log(`可稍后执行 dd-pw doctor --fix 重试。\n`);
|
|
133
|
+
}
|
|
122
134
|
if (wasWebRunning) {
|
|
123
135
|
console.log(`[dd-playwright] 正在自动为您重启后台 Web 服务...`);
|
|
124
136
|
const restartRes = await startWebDaemon({
|
|
125
|
-
port:
|
|
137
|
+
port: webPort,
|
|
126
138
|
dataDir: config.dataDir,
|
|
127
139
|
pidFile: config.webPidFile,
|
|
128
140
|
logFile: config.webLogFile,
|