@taole/dd-playwright 0.1.11 → 0.1.12

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 CHANGED
@@ -8,7 +8,8 @@
8
8
 
9
9
  | 工具 | 说明 |
10
10
  |---|---|
11
- | `browser_navigate` | 访问 URL。支持显式指定 `uid` 选择绑定账号(默认使用当前默认账号);支持通过 `anonymous: true` 模拟未登录游客状态访问 |
11
+ | `browser_navigate` | 访问 URL。支持显式指定 `uid` 选择绑定账号(默认使用当前默认账号);支持通过 `anonymous: true` 模拟未登录游客状态访问;支持 `device`(如 `iPhone 13`)启用完整移动端仿真(视口+UA+触摸),传 `desktop` 恢复桌面模式;支持 `userAgent` 会话级自定义 UA,传 `default` 清除 |
12
+ | `browser_resize` | 调整当前页面渲染视口(对标 playwright-mcp)。只改 innerWidth/innerHeight,不改 UA/触摸;完整移动仿真请用 `browser_navigate` 的 `device` 参数 |
12
13
  | `browser_screenshot` | 页面/元素截图,落盘到数据目录并返回路径 |
13
14
  | `browser_get_console_errors` | 收集 console.error 与未捕获异常 |
14
15
  | `browser_click_element` | 按选择器模拟用户点击元素 |
@@ -42,6 +43,7 @@ npx playwright install chromium
42
43
  - **数据目录**:默认自动固定为 `~/.dd-browser-mcp`(账号库 `runtime.db` 与截图目录),无需手动设置环境变量。
43
44
  - **无头模式 (Headless)**:默认为 `true`(静默后台运行),可在 Web 控制台(`http://127.0.0.1:12309`)的「全局运行配置」中一键开关切换为可见窗口。
44
45
  - **域名白名单**:默认为 `tuwan.com`,可通过 `BROWSER_MCP_DOMAIN_SUFFIX` 按需调整。
46
+ - **自定义 UA**:优先用 `browser_navigate` 的 `userAgent` 参数(会话级,随调随改);也可用环境变量 `BROWSER_MCP_USER_AGENT` 设置启动级默认值。UA 优先级:工具参数 > 环境变量 > `device` 设备描述符 > 浏览器默认;自定义 UA 只替换 UA 字符串,视口与触摸仿真仍按 `device` 生效。
45
47
 
46
48
  登录态来源优先级:`BROWSER_MCP_STORAGE_STATE` 文件 > `~/.dd-browser-mcp/runtime.db` 中激活的账号 > 无登录态。
47
49
 
package/SKILL.md CHANGED
@@ -31,7 +31,28 @@ description: >-
31
31
  4. **请求头注入**:`y.tuwan.com reqHeaders://{debug: '1'}`
32
32
  > 更多高级协议与规则参考官方文档:https://wproxy.org/whistle/rules/
33
33
 
34
- ## 3. 页面交互规范
34
+ ## 3. 设备与视口仿真(移动端/H5 验收)
35
+ - **移动端验收**:调用 `browser_navigate` 时传 `device`(Playwright 设备描述符名,如 `"iPhone 13"`、`"Pixel 7"`),一次性启用完整移动仿真:移动视口 + 移动 UA + isMobile + 触摸事件(hasTouch)。设置后整个会话保持,后续导航无需重复传;传 `"desktop"` 恢复默认 1280×800 桌面模式。
36
+ - **切换时机**:切换 device 会重建浏览器上下文(当前页面关闭后重开目标 URL,登录态自动重挂),必须在导航时一并指定,严禁在已打开页面的会话中途切换。
37
+ - **仅调视口**:只需改变 innerWidth/innerHeight(如响应式断点验证)时调用 `browser_resize({ width, height })`;它不改 UA 与触摸仿真,页面内执行 `window.resizeTo` 对 Playwright 视口无效,不要用它尝试改视口。
38
+ - **常见设备名参考**:`iPhone 13`(390×844)、`iPhone 15 Pro`(393×852)、`Pixel 7`(412×915)、`iPad Mini`(768×1024);传错名称时工具会返回错误提示与示例。
39
+ - **自定义 UA**:调用 `browser_navigate` 时传 `userAgent`(如模拟 App 内 WebView:`"WebView/9.9 ..."`),会话级生效、整个会话保持;只替换 UA 字符串,不影响视口与触摸仿真,可与 `device` 同时指定。传 `"default"` 清除会话级设置。UA 优先级:工具参数 > 环境变量 `BROWSER_MCP_USER_AGENT`(启动级默认值)> `device` 描述符 > 浏览器默认。与 `device` 一样,切换 UA 会重建上下文,必须在导航时一并指定;`browser_status.sessionUserAgent` / `envUserAgent` 可确认当前生效值。
40
+ - **典型示例**:
41
+ ```
42
+ // 完整移动端仿真(视口 390x844 + iPhone UA + 触摸),会话级保持
43
+ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13" })
44
+
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" })
47
+
48
+ // 只调视口验证响应式断点(不改 UA / 触摸)
49
+ browser_resize({ width: 375, height: 812 })
50
+
51
+ // 恢复桌面模式并清除自定义 UA
52
+ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAgent: "default" })
53
+ ```
54
+
55
+ ## 4. 页面交互规范
35
56
  - **元素定位**:在动态页面或选择器不确定时,优先调用 `browser_get_dom` 提取当前可见的交互元素结构(包含按钮、输入框、链接及其选择器),严禁盲猜选择器。
36
57
  - **表单与按键**:表单输入调用 `browser_fill_text`(原生触发 `input` 与 `change` 事件);键盘按键/回车提交调用 `browser_press_key`。
37
58
  - **状态排查**:排查免登异常前先调用 `browser_status` 确认 MCP 服务凭据挂载状态与当前 UID。
@@ -11,7 +11,7 @@ import { resolveConfig } from "../src/config.mjs";
11
11
  import { PlaywrightBrowserManager } from "../src/browser-manager.mjs";
12
12
  import { pathToFileURL } from "node:url";
13
13
 
14
- export const MCP_VERSION = "0.1.11";
14
+ export const MCP_VERSION = "0.1.12";
15
15
 
16
16
  function textResult(payload) {
17
17
  return {
@@ -38,7 +38,8 @@ export function createBrowserMcpServer({ manager, config }) {
38
38
  description:
39
39
  "使用内置 Playwright 访问目标 URL。" +
40
40
  "可显式指定 uid 选择绑定的点点账号(不传则使用默认激活账号);" +
41
- "若传 anonymous: true 则以完全未登录的游客身份访问。" +
41
+ "若传 anonymous: true 则以完全未登录的游客身份访问;" +
42
+ "移动端/H5 验收可传 device 启用完整设备仿真(视口+UA+触摸),也可单独传 userAgent 自定义 UA。" +
42
43
  "返回 HTTP 状态码、页面标题与控制台报错统计。",
43
44
  inputSchema: {
44
45
  url: z.string().describe("需要访问的目标页面 URL"),
@@ -50,6 +51,23 @@ export function createBrowserMcpServer({ manager, config }) {
50
51
  .boolean()
51
52
  .optional()
52
53
  .describe("是否以匿名/未登录状态访问(默认 false)。若为 true,则不注入任何 Cookie/storageState"),
54
+ device: z
55
+ .string()
56
+ .optional()
57
+ .describe(
58
+ "可选 Playwright 设备描述符名称(如 'iPhone 13'、'iPhone 15 Pro'、'Pixel 7'),启用完整移动端仿真:" +
59
+ "视口 + 移动 UA + isMobile + hasTouch。设置后整个会话保持该设备,后续导航无需重复传。" +
60
+ "传 'desktop' 恢复默认 1280x800 桌面模式。" +
61
+ "注意:切换设备会重建浏览器上下文(当前页面关闭后重开目标 URL,登录态自动重挂),请在导航时一并指定,不要在中途切换。",
62
+ ),
63
+ userAgent: z
64
+ .string()
65
+ .optional()
66
+ .describe(
67
+ "可选自定义 User-Agent(会话级,设置后整个会话保持,后续导航无需重复传)。" +
68
+ "优先级高于设备描述符自带 UA 与环境变量 BROWSER_MCP_USER_AGENT;只替换 UA 字符串,不影响视口与触摸仿真。" +
69
+ "传 'default' 清除会话级设置。注意:切换 UA 会重建浏览器上下文(当前页面关闭后重开目标 URL,登录态自动重挂),请在导航时一并指定。",
70
+ ),
53
71
  waitFor: z
54
72
  .string()
55
73
  .optional()
@@ -60,10 +78,10 @@ export function createBrowserMcpServer({ manager, config }) {
60
78
  .describe("导航与等待超时时间(毫秒),默认 15000"),
61
79
  },
62
80
  },
63
- async ({ url, uid, anonymous, waitFor, timeout }) => {
81
+ async ({ url, uid, anonymous, device, userAgent, waitFor, timeout }) => {
64
82
  try {
65
83
  return textResult(
66
- await manager.navigate({ url, uid, anonymous, waitFor, timeout }),
84
+ await manager.navigate({ url, uid, anonymous, device, userAgent, waitFor, timeout }),
67
85
  );
68
86
  } catch (err) {
69
87
  return errorResult("导航失败", err);
@@ -268,6 +286,27 @@ export function createBrowserMcpServer({ manager, config }) {
268
286
  },
269
287
  );
270
288
 
289
+ server.registerTool(
290
+ "browser_resize",
291
+ {
292
+ description:
293
+ "调整当前页面的渲染视口尺寸(对标 playwright-mcp 的 browser_resize)。" +
294
+ "只影响 innerWidth/innerHeight 与布局视口,不修改 UA 与触摸仿真;" +
295
+ "若页面按 UA 判断端别或依赖触摸事件,请改用 browser_navigate 的 device 参数做完整移动端仿真。",
296
+ inputSchema: {
297
+ width: z.number().int().positive().describe("视口宽度(像素)"),
298
+ height: z.number().int().positive().describe("视口高度(像素)"),
299
+ },
300
+ },
301
+ async ({ width, height }) => {
302
+ try {
303
+ return textResult(await manager.resize({ width, height }));
304
+ } catch (err) {
305
+ return errorResult("调整视口失败", err);
306
+ }
307
+ },
308
+ );
309
+
271
310
  server.registerTool(
272
311
  "browser_get_dom",
273
312
  {
@@ -399,6 +438,9 @@ export function createBrowserMcpServer({ manager, config }) {
399
438
  storageStateLoaded: manager.hasAuth,
400
439
  currentUid: manager.currentUid,
401
440
  isAnonymous: manager.isAnonymous,
441
+ device: manager.device,
442
+ sessionUserAgent: manager.sessionUserAgent,
443
+ envUserAgent: manager.userAgent,
402
444
  headless: manager.headless,
403
445
  proxyStarted: manager.whistle.started,
404
446
  proxyRule: manager.proxyRule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taole/dd-playwright",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Standalone stdio MCP & CLI: Playwright browser automation + isolated whistle proxy + diandian account storageState injection",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -4,10 +4,36 @@
4
4
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
  import { join } from "node:path";
7
- import { chromium } from "playwright";
7
+ import { chromium, devices } from "playwright";
8
8
  import { ensureChromiumInstalled } from "./browser-check.mjs";
9
9
  import { WhistleProxyManager } from "./whistle-proxy.mjs";
10
10
 
11
+ /** 桌面模式默认视口 */
12
+ const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
13
+
14
+ /** 常用设备描述符示例(用于报错提示) */
15
+ const DEVICE_EXAMPLES = ["iPhone 13", "iPhone 15 Pro", "Pixel 7", "Galaxy S9+", "iPad Mini"];
16
+
17
+ /**
18
+ * 解析设备描述符为 newContext 选项。
19
+ * @param {string|null} device null/"desktop" 表示桌面模式
20
+ * @returns {object} context 选项(桌面模式仅含默认视口)
21
+ */
22
+ function resolveDeviceOptions(device) {
23
+ if (!device || device === "desktop") {
24
+ return { viewport: DEFAULT_VIEWPORT };
25
+ }
26
+ const descriptor = devices[device];
27
+ if (!descriptor) {
28
+ throw new Error(
29
+ `未知设备描述符 [${device}]。取值参考 Playwright devices,例如: ${DEVICE_EXAMPLES.join(", ")};传 "desktop" 恢复桌面模式`,
30
+ );
31
+ }
32
+ // 剔除仅用于 launch 的字段,其余(viewport/userAgent/isMobile/hasTouch/deviceScaleFactor 等)全量合入
33
+ const { defaultBrowserType, ...contextOptions } = descriptor;
34
+ return contextOptions;
35
+ }
36
+
11
37
  /**
12
38
  * 从账号库或文件加载指定账号或激活账号的 storageState。
13
39
  * 优先级:
@@ -89,6 +115,7 @@ export class PlaywrightBrowserManager {
89
115
  * @param {boolean} [options.headless]
90
116
  * @param {string} [options.dbPath]
91
117
  * @param {string|null} [options.storageStateFile]
118
+ * @param {string|null} [options.userAgent] 启动级自定义 UA(环境变量),作为会话级 UA 参数的默认值
92
119
  * @param {object} [options.storageState] 直接注入,跳过自动加载
93
120
  */
94
121
  constructor({
@@ -97,9 +124,11 @@ export class PlaywrightBrowserManager {
97
124
  headless = true,
98
125
  dbPath,
99
126
  storageStateFile = null,
127
+ userAgent = null,
100
128
  storageState,
101
129
  } = {}) {
102
130
  this.headless = headless;
131
+ this.userAgent = userAgent?.trim() || null;
103
132
  this.screenshotsDir = screenshotsDir;
104
133
  this.dbPath = dbPath;
105
134
  this.storageStateFile = storageStateFile;
@@ -110,6 +139,8 @@ export class PlaywrightBrowserManager {
110
139
  : resolveStorageState({ dbPath, storageStateFile });
111
140
  this.currentUid = null;
112
141
  this.isAnonymous = false;
142
+ this.device = null; // 当前设备描述符名;null 表示桌面模式
143
+ this.sessionUserAgent = null; // 会话级 UA(工具参数设置);null 表示回落到 this.userAgent / 设备描述符 / 默认
113
144
  this.browser = null;
114
145
  this.context = null;
115
146
  this.page = null;
@@ -154,20 +185,39 @@ export class PlaywrightBrowserManager {
154
185
 
155
186
 
156
187
  /**
157
- * 根据指定的 uid 或 anonymous 状态准备浏览器上下文。
158
- * @param {{uid?: string|null, anonymous?: boolean}} [authOptions]
188
+ * 根据指定的 uid、anonymous 状态、设备描述符与会话级 UA 准备浏览器上下文。
189
+ * @param {{uid?: string|null, anonymous?: boolean, device?: string|null, userAgent?: string|null}} [options]
190
+ * device 不传则保持当前模式;传 "desktop"/null 恢复桌面;传 Playwright 设备名启用移动端仿真。
191
+ * userAgent 不传则保持当前会话级 UA;传 "default"/null 清除会话级 UA(回落到启动配置/设备描述符/默认 UA)
159
192
  */
160
- async ensurePage({ uid = null, anonymous = false } = {}) {
193
+ async ensurePage({ uid = null, anonymous = false, device, userAgent } = {}) {
161
194
  const targetAnonymous = Boolean(anonymous);
162
195
  const targetUid = targetAnonymous ? null : (uid ? String(uid) : null);
163
196
 
197
+ // 先解析并校验设备(非法设备名必须在关闭现有上下文之前抛错,避免误毁会话)
198
+ let targetDevice = this.device;
199
+ if (device !== undefined) {
200
+ targetDevice = !device || device === "desktop" ? null : String(device);
201
+ if (targetDevice) {
202
+ resolveDeviceOptions(targetDevice); // 仅校验,非法名抛错
203
+ }
204
+ }
205
+ const deviceChanged = targetDevice !== this.device;
206
+
207
+ // 解析会话级 UA(同样在任何上下文变更之前完成)
208
+ let targetSessionUA = this.sessionUserAgent;
209
+ if (userAgent !== undefined) {
210
+ targetSessionUA = !userAgent || userAgent === "default" ? null : String(userAgent);
211
+ }
212
+ const uaChanged = targetSessionUA !== this.sessionUserAgent;
213
+
164
214
  // 检查是否需要重建上下文(当前模式与目标不一致)
165
215
  const authChanged =
166
216
  this.isAnonymous !== targetAnonymous ||
167
217
  (targetUid !== null && this.currentUid !== targetUid) ||
168
218
  (targetUid === null && this.currentUid !== null && !targetAnonymous);
169
219
 
170
- if (authChanged && this.context) {
220
+ if ((authChanged || deviceChanged || uaChanged) && this.context) {
171
221
  if (this.page && !this.page.isClosed()) {
172
222
  await this.page.close().catch(() => {});
173
223
  this.page = null;
@@ -206,10 +256,16 @@ export class PlaywrightBrowserManager {
206
256
  this.storageState = state;
207
257
  this.currentUid = targetUid;
208
258
  this.isAnonymous = targetAnonymous;
209
-
210
- const contextOptions = {
211
- viewport: { width: 1280, height: 800 },
212
- };
259
+ this.device = targetDevice;
260
+ this.sessionUserAgent = targetSessionUA;
261
+
262
+ // 对标 Playwright devices 仿真:设备模式下合入 viewport/userAgent/isMobile/hasTouch 等完整描述符
263
+ const contextOptions = resolveDeviceOptions(targetDevice);
264
+ // UA 优先级:会话级(工具参数)> 启动级(环境变量)> 设备描述符 > 浏览器默认
265
+ const effectiveUA = this.sessionUserAgent || this.userAgent;
266
+ if (effectiveUA) {
267
+ contextOptions.userAgent = effectiveUA;
268
+ }
213
269
  if (state) {
214
270
  contextOptions.storageState = state;
215
271
  }
@@ -247,8 +303,8 @@ export class PlaywrightBrowserManager {
247
303
  return this.page;
248
304
  }
249
305
 
250
- async navigate({ url, waitFor, timeout = 15000, uid, anonymous = false }) {
251
- const page = await this.ensurePage({ uid, anonymous });
306
+ async navigate({ url, waitFor, timeout = 15000, uid, anonymous = false, device, userAgent }) {
307
+ const page = await this.ensurePage({ uid, anonymous, device, userAgent });
252
308
  const response = await page.goto(url, {
253
309
  timeout,
254
310
  waitUntil: "domcontentloaded",
@@ -491,6 +547,23 @@ export class PlaywrightBrowserManager {
491
547
  return snapshot;
492
548
  }
493
549
 
550
+ /**
551
+ * 调整当前页面渲染视口(对标 playwright-mcp 的 browser_resize)。
552
+ * 只影响 innerWidth/innerHeight 与布局视口,不改 UA / 触摸仿真;
553
+ * 完整移动端仿真请使用 navigate 的 device 参数。
554
+ * @param {{width: number, height: number}} params
555
+ */
556
+ async resize({ width, height }) {
557
+ const page = await this.ensurePage();
558
+ const viewport = { width: Number(width), height: Number(height) };
559
+ await page.setViewportSize(viewport);
560
+ return {
561
+ resized: true,
562
+ viewport,
563
+ url: page.url(),
564
+ };
565
+ }
566
+
494
567
  async evalJs({ script }) {
495
568
  const page = await this.ensurePage();
496
569
  const result = await page.evaluate((code) => {
package/src/config.mjs CHANGED
@@ -47,6 +47,8 @@ export function resolveConfig(env = process.env) {
47
47
  domainRegex: buildDomainRegex(domainSuffix),
48
48
  headless,
49
49
  storageStateFile: env.BROWSER_MCP_STORAGE_STATE?.trim() || null,
50
+ // 启动级自定义 UA;设置后覆盖默认桌面 UA 与 device 设备描述符自带的 UA
51
+ userAgent: env.BROWSER_MCP_USER_AGENT?.trim() || null,
50
52
  };
51
53
  }
52
54
 
@@ -390,7 +390,11 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
390
390
  &nbsp;&nbsp;3) 静态替换:<code>act.tuwan.com/cdn/index.js D:/dist/index.js</code><br>
391
391
  &nbsp;&nbsp;4) 请求头注入:<code>y.tuwan.com reqHeaders://{debug: '1'}</code><br>
392
392
  &nbsp;&nbsp;👉 官方完整语法参考:<a href="https://wproxy.org/whistle/rules/" target="_blank" style="color: #38bdf8; text-decoration: underline;">https://wproxy.org/whistle/rules/</a><br><br>
393
- <b>3. 页面交互规范:</b><br>
393
+ <b>3. 设备与视口仿真(移动端/H5 验收):</b><br>
394
+ • 移动端验收在导航时传 <code>device</code>(如 <code>"iPhone 13"</code>)启用完整仿真:移动视口 + 移动 UA + 触摸;传 <code>"desktop"</code> 恢复桌面。<br>
395
+ • 自定义 UA 传 <code>userAgent</code>(会话级保持,可与 device 叠加,传 <code>"default"</code> 清除)。<br>
396
+ • 只调视口用 <code>browser_resize({ width, height })</code>;页面内 <code>window.resizeTo</code> 对 Playwright 视口无效。<br><br>
397
+ <b>4. 页面交互规范:</b><br>
394
398
  • 复杂或动态页面定位困难时,优先调用 <code>browser_get_dom</code> 获取可见元素清单,严禁盲猜选择器。<br>
395
399
  • 表单填写使用 <code>browser_fill_text</code>,按键使用 <code>browser_press_key</code>。
396
400
  </div>
@@ -434,6 +438,19 @@ browser_press_key({ selector: "input#keyword", key: "Enter" })
434
438
  // 3. 结果截图与控制台报错收集
435
439
  browser_screenshot({ selector: ".result-box" })
436
440
  const errors = browser_get_console_errors()</div>
441
+
442
+ <div class="code-title"><span>流程 4:移动端 / H5 仿真验收(设备 · UA · 视口)</span></div>
443
+ <div class="code-block" style="color: #93c5fd; font-size: 12px; margin-bottom: 0;">// 1. 完整移动端仿真:视口 390x844 + iPhone UA + 触摸,会话级保持
444
+ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "iPhone 13" })
445
+
446
+ // 2. 模拟 App 内 WebView:设备仿真 + 自定义 UA(只换 UA,视口/触摸仍按 iPhone 13)
447
+ 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" })
448
+
449
+ // 3. 只调视口验证响应式断点(不改 UA / 触摸)
450
+ browser_resize({ width: 375, height: 812 })
451
+
452
+ // 4. 恢复桌面模式并清除自定义 UA
453
+ browser_navigate({ url: "https://xdd-test.tuwan.com/", device: "desktop", userAgent: "default" })</div>
437
454
  </div>
438
455
  </div>
439
456
  </div>