@taole/dd-playwright 0.1.6 → 0.1.7

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
@@ -19,6 +19,7 @@
19
19
  | `browser_get_dom` | 提取当前页面的结构化可交互 DOM 树(按钮/链接/输入框及其选择器),免去盲猜 |
20
20
  | `browser_eval_js` | 页面上下文执行 JS |
21
21
  | `browser_set_proxy_rule` | 声明「本地 dev server → 干净 `*.tuwan.com` 域名」映射,隔离 whistle 代理自动托管 |
22
+ | `browser_load_proxy_rules_dir` | 批量加载项目规则目录(如 `.whistle-rules`),全量注入并激活域名映射与 Mock 规则 |
22
23
  | `browser_status` | 查询登录态挂载情况、当前 UID、代理状态、数据目录,排查免登问题先调它 |
23
24
 
24
25
  ## 安装与挂载
@@ -30,20 +31,11 @@ npm install
30
31
  npx playwright install chromium
31
32
  ```
32
33
 
33
- MCP 客户端配置(以 omp / Cursor / Claude 为例):
34
-
35
- ```json
36
- {
37
- "mcpServers": {
38
- "dd-playwright": {
39
- "command": "dd-playwright-mcp"
40
- }
41
- }
42
- }
43
- ```
44
-
45
- 或者直接指向本地入口:`node D:/tools/dd-browser-mcp/bin/mcp-server.mjs`
34
+ ### MCP 客户端挂载配置
46
35
 
36
+ 启动后台控制台后,直接在浏览器打开 **`http://127.0.0.1:12309`**:
37
+ - 切换到 **「MCP 客户端配置」** 选项卡。
38
+ - 页面会自动根据你当前的系统与安装路径生成准确的配置 JSON,支持**一键复制**或**一键安装到 Cursor**,开箱即用,无需手动拼接路径。
47
39
  ## 数据与配置
48
40
 
49
41
  - **数据目录**:默认自动固定为 `~/.dd-browser-mcp`(账号库 `runtime.db` 与截图目录),无需手动设置环境变量。
package/SKILL.md CHANGED
@@ -17,10 +17,19 @@ description: >-
17
17
  ## 2. 本地开发服务联调(Dev Server 代理映射)
18
18
  - 当本地启动了前端开发服务器(如 Vite/Webpack 监听本地端口)时,**严禁修改系统 hosts**。
19
19
  - 必须先调用本 MCP 的 `browser_set_proxy_rule` 将本地端口映射到合法的点点子域名(子域名不要求在线上真实存在,支持任意自定义前缀)。
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
23
  - 随后调用 `browser_navigate({ url: "http://p9527-test.tuwan.com/" })` 访问,进程内隔离 Whistle 代理会自动转发流量并正确携带点点线上 Cookie。
24
+ - **项目规则目录批量加载(推荐)**:
25
+ - 若项目中存在 `.whistle-rules` 等包含多份代理/Mock 规则文件的目录,直接调用 `browser_load_proxy_rules_dir({ rulesDir: "绝对路径" })` 全量注入激活。
26
+
27
+ ### 附:常用 Whistle 规则语法参考(编写 .whistle-rules 目录时)
28
+ 1. **域名与端口映射**:`p9527-test.tuwan.com http://127.0.0.1:9527`
29
+ 2. **接口 Mock / 响应替换**:`yapi.tuwan.com/act resBody://D:/mock/data.json`
30
+ 3. **静态资源替换**:`act.tuwan.com/cdn/index.js D:/dist/index.js`
31
+ 4. **请求头注入**:`y.tuwan.com reqHeaders://{debug: '1'}`
32
+ > 更多高级协议与规则参考官方文档:https://wproxy.org/whistle/rules/
24
33
 
25
34
  ## 3. 页面交互规范
26
35
  - **元素定位**:在动态页面或选择器不确定时,优先调用 `browser_get_dom` 提取当前可见的交互元素结构(包含按钮、输入框、链接及其选择器),严禁盲猜选择器。
@@ -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.6";
14
+ export const MCP_VERSION = "0.1.7";
15
15
 
16
16
  function textResult(payload) {
17
17
  return {
@@ -336,6 +336,37 @@ export function createBrowserMcpServer({ manager, config }) {
336
336
  }
337
337
  },
338
338
  );
339
+ server.registerTool(
340
+ "browser_load_proxy_rules_dir",
341
+ {
342
+ description:
343
+ "从指定本地目录批量加载 Whistle 规则文件(如项目的 .whistle-rules 目录),将其全量注入并激活到进程内 Whistle 代理中。" +
344
+ "支持目录下包含多个 .rule / .txt / .whistle 文件(如域名端口映射、Mock 接口 resBody、静态资源重定向等)。" +
345
+ "调用后浏览器会自动重启连接以加载新规则。",
346
+ inputSchema: {
347
+ rulesDir: z
348
+ .string()
349
+ .describe("包含 Whistle 规则文件的本地绝对路径目录"),
350
+ clearExisting: z
351
+ .boolean()
352
+ .optional()
353
+ .describe("是否清空已有的旧规则,默认 false(增量合并注入)"),
354
+ },
355
+ },
356
+ async ({ rulesDir, clearExisting = false }) => {
357
+ try {
358
+ const res = await manager.loadProxyRulesDir({ rulesDir, clearExisting });
359
+ return textResult({
360
+ success: true,
361
+ message: `已成功加载目录规则:${rulesDir}`,
362
+ ...res,
363
+ });
364
+ } catch (err) {
365
+ return errorResult("加载目录代理规则失败", err);
366
+ }
367
+ },
368
+ );
369
+
339
370
 
340
371
  server.registerTool(
341
372
  "browser_eval_js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taole/dd-playwright",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -140,6 +140,18 @@ export class PlaywrightBrowserManager {
140
140
  await this._closeBrowserOnly();
141
141
  return this.proxyRule;
142
142
  }
143
+ /**
144
+ * 从指定目录批量加载 Whistle 规则并重启浏览器加载规则。
145
+ * @param {{rulesDir: string, clearExisting?: boolean}} params
146
+ */
147
+ async loadProxyRulesDir({ rulesDir, clearExisting = false }) {
148
+ const whistle = await this.ensureProxy();
149
+ const result = await whistle.loadRulesFromDirectory(rulesDir, { clearExisting });
150
+ // 重启浏览器以清除连接与 DNS 缓存,确保新规则立即生效
151
+ await this._closeBrowserOnly();
152
+ return result;
153
+ }
154
+
143
155
 
144
156
  /**
145
157
  * 根据指定的 uid 或 anonymous 状态准备浏览器上下文。
@@ -381,10 +381,9 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
381
381
  • 访问点点业务页面必须调用本 MCP 的 <code>browser_navigate</code> 工具,自动注入当前激活账号的 storageState(含 <code>Tuwan_Passport</code> 鉴权 Cookie)免登。<br>
382
382
  • 多角色或指定账号测试时传入特定 <code>uid</code>,自动隔离并重建独立浏览器上下文。<br><br>
383
383
  <b>2. 本地开发服务联调(Dev Server 代理映射):</b><br>
384
- • 本地服务启动后严禁改动系统 hosts,必须先调用 <code>browser_set_proxy_rule</code> 映射到点点子域名。<br>
385
- • <b>示例</b>:本地 dev server 运行在 <code>127.0.0.1:9527</code> 时,可映射为自定义域名 <code>p9527-test.tuwan.com</code>(域名无需线上真实存在):<br>
386
- <code style="color: #38bdf8;">browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })</code><br>
387
- 随后访问该域名,内部隔离 Whistle 代理会自动转发流量并正确携带线上 Cookie。<br><br>
384
+ • 本地服务启动后严禁改动系统 hosts,可通过单域名映射或项目规则目录全量注入:<br>
385
+ • <b>单条映射</b>:<code>browser_set_proxy_rule({ domain: "p9527-test.tuwan.com", port: 9527 })</code><br>
386
+ • <b>目录批量规则(推荐)</b>:若项目有 <code>.whistle-rules</code> 目录,直接调用 <code>browser_load_proxy_rules_dir({ rulesDir: "目录绝对路径" })</code> 全量激活代理与 Mock。<br><br>
388
387
  <b>3. 页面交互规范:</b><br>
389
388
  • 复杂或动态页面定位困难时,优先调用 <code>browser_get_dom</code> 获取可见元素清单,严禁盲猜选择器。<br>
390
389
  • 表单填写使用 <code>browser_fill_text</code>,按键使用 <code>browser_press_key</code>。
@@ -742,18 +741,6 @@ export function createWebServer({ accountStore, authManager }) {
742
741
  new URL("../../bin/mcp-server.mjs", import.meta.url),
743
742
  ).replaceAll("\\", "/");
744
743
 
745
- const globalConfigStr = JSON.stringify(
746
- {
747
- mcpServers: {
748
- "dd-playwright": {
749
- command: "dd-playwright-mcp",
750
- },
751
- },
752
- },
753
- null,
754
- 2,
755
- );
756
-
757
744
  const localConfigStr = JSON.stringify(
758
745
  {
759
746
  mcpServers: {
@@ -8,9 +8,18 @@
8
8
  * - 仅监听 127.0.0.1,不对外暴露
9
9
  * - 域名后缀限制可声明规则;启用后浏览器流量均经代理转发
10
10
  */
11
- import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
11
+ import {
12
+ mkdtempSync,
13
+ writeFileSync,
14
+ mkdirSync,
15
+ rmSync,
16
+ existsSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ statSync,
20
+ } from "node:fs";
12
21
  import { tmpdir } from "node:os";
13
- import { join } from "node:path";
22
+ import { join, extname } from "node:path";
14
23
  import { createRequire } from "node:module";
15
24
 
16
25
  let startWhistleFn = null;
@@ -169,6 +178,103 @@ export class WhistleProxyManager {
169
178
  const url = protocol === "https" ? `https://${domain}/` : `http://${domain}/`;
170
179
  return { url, rule: ruleText };
171
180
  }
181
+ /**
182
+ * 从指定目录加载 Whistle 规则文件并全量激活。
183
+ * @param {string} rulesDir 规则文件所在的本地绝对路径
184
+ * @param {{clearExisting?: boolean}} [options] 是否清空已有的旧规则(默认 false,增量覆盖)
185
+ * @returns {Promise<{success: boolean, loadedFiles: string[], totalRulesCount: number, rulesSummary: string}>}
186
+ */
187
+ async loadRulesFromDirectory(rulesDir, { clearExisting = false } = {}) {
188
+ if (!this.started) {
189
+ throw new Error("Whistle proxy not started. Call start() first.");
190
+ }
191
+ if (!rulesDir || typeof rulesDir !== "string") {
192
+ throw new Error("必须提供有效的 rulesDir 路径");
193
+ }
194
+ if (!existsSync(rulesDir) || !statSync(rulesDir).isDirectory()) {
195
+ throw new Error(`目标路径不存在或不是有效目录: ${rulesDir}`);
196
+ }
197
+
198
+ const entries = readdirSync(rulesDir);
199
+ const loadedFiles = [];
200
+ const collectedRuleLines = [];
201
+
202
+ for (const file of entries) {
203
+ const fullPath = join(rulesDir, file);
204
+ try {
205
+ const stat = statSync(fullPath);
206
+ if (stat.isDirectory()) continue;
207
+
208
+ // 读取文本文件内容
209
+ const content = readFileSync(fullPath, "utf8").trim();
210
+ if (!content) continue;
211
+
212
+ loadedFiles.push(file);
213
+ collectedRuleLines.push(`# === From File: ${file} ===`);
214
+ collectedRuleLines.push(content);
215
+ collectedRuleLines.push("");
216
+ } catch {}
217
+ }
218
+
219
+ if (loadedFiles.length === 0) {
220
+ return {
221
+ success: true,
222
+ loadedFiles: [],
223
+ totalRulesCount: 0,
224
+ rulesSummary: "目录中未发现可读的规则文件",
225
+ };
226
+ }
227
+
228
+ let finalRulesText = collectedRuleLines.join("\n");
229
+
230
+ // 如果不需要清空已有规则,读取当前 default 规则合并
231
+ if (!clearExisting) {
232
+ try {
233
+ const listRes = await fetch(
234
+ `http://127.0.0.1:${this.port}/cgi-bin/rules/list`,
235
+ );
236
+ const listData = await listRes.json();
237
+ const currentDefault = listData.list?.find((r) => r.name === "default")?.data || "";
238
+ if (currentDefault.trim()) {
239
+ finalRulesText = `${currentDefault.trim()}\n\n${finalRulesText}`;
240
+ }
241
+ } catch {}
242
+ }
243
+
244
+ // 通过 WebUI CGI 写入 default 规则并激活
245
+ const res = await fetch(
246
+ `http://127.0.0.1:${this.port}/cgi-bin/rules/select`,
247
+ {
248
+ method: "POST",
249
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
250
+ body: new URLSearchParams({
251
+ name: "default",
252
+ value: finalRulesText,
253
+ }).toString(),
254
+ },
255
+ );
256
+
257
+ if (!res.ok) {
258
+ throw new Error(`Failed to set directory whistle rules: HTTP ${res.status}`);
259
+ }
260
+ const result = await res.json();
261
+ if (result.ec !== 0) {
262
+ throw new Error(`Failed to set whistle rules: ${JSON.stringify(result)}`);
263
+ }
264
+
265
+ const nonCommentRules = finalRulesText
266
+ .split("\n")
267
+ .map((l) => l.trim())
268
+ .filter((l) => l && !l.startsWith("#"));
269
+
270
+ return {
271
+ success: true,
272
+ loadedFiles,
273
+ totalRulesCount: nonCommentRules.length,
274
+ rulesSummary: finalRulesText,
275
+ };
276
+ }
277
+
172
278
 
173
279
  /**
174
280
  * 浏览器应使用的 Chromium 启动参数。