@szc-ft/mcp-szcd-client 0.27.2 → 0.27.4

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.
@@ -5,11 +5,14 @@
5
5
  * 用法:
6
6
  * node local-browser-executor.js --action observe --output /tmp/observe.json
7
7
  * node local-browser-executor.js --action act --index 1 --observe-after
8
+ * node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after
9
+ * node local-browser-executor.js --action api-capture --wait 15000 --include-response-body
10
+ * node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0
8
11
  * node local-browser-executor.js --plan '<JSON>' --output /tmp/result.json
9
12
  * node local-browser-executor.js --plan-file /path/to/plan.json --output /tmp/result.json
10
13
  *
11
14
  * 参数:
12
- * --action 直接执行动作,当前支持 observe/act/assert
15
+ * --action 直接执行动作,当前支持 observe/act/assert/api-capture
13
16
  * --plan 测试计划 JSON 字符串
14
17
  * --plan-file 测试计划 JSON 文件路径(与 --plan 二选一)
15
18
  * --output 结果输出文件路径(默认 stdout)
@@ -93,6 +96,12 @@ function parseArgs() {
93
96
  case "--hover":
94
97
  parsed.hover = args[++i];
95
98
  break;
99
+ case "--menu-path":
100
+ parsed.menuPath = args[++i];
101
+ break;
102
+ case "--wait-after-each":
103
+ parsed.waitAfterEach = args[++i];
104
+ break;
96
105
  case "--observe-after":
97
106
  parsed.observeAfter = true;
98
107
  break;
@@ -111,12 +120,102 @@ function parseArgs() {
111
120
  case "--has-text":
112
121
  parsed.hasText = args[++i];
113
122
  break;
123
+ case "--wait":
124
+ parsed.wait = args[++i];
125
+ break;
126
+ case "--method":
127
+ parsed.method = args[++i];
128
+ break;
129
+ case "--url-pattern":
130
+ parsed.urlPattern = args[++i];
131
+ break;
132
+ case "--target-url-contains":
133
+ parsed.targetUrlContains = args[++i];
134
+ break;
135
+ case "--include-request-body":
136
+ parsed.includeRequestBody = true;
137
+ break;
138
+ case "--no-request-body":
139
+ parsed.includeRequestBody = false;
140
+ break;
141
+ case "--include-response-body":
142
+ parsed.includeResponseBody = true;
143
+ break;
144
+ case "--reload":
145
+ parsed.reload = true;
146
+ break;
147
+ case "--include-all-targets":
148
+ parsed.includeAllTargets = true;
149
+ break;
150
+ case "--capture-file":
151
+ parsed.captureFile = args[++i];
152
+ break;
153
+ case "--max-failed":
154
+ parsed.maxFailed = args[++i];
155
+ break;
156
+ case "--forbid-status-gte":
157
+ parsed.forbidStatusGte = args[++i];
158
+ break;
159
+ case "--required-url":
160
+ parsed.requiredUrls = [...(parsed.requiredUrls || []), args[++i]];
161
+ break;
162
+ case "--required-method":
163
+ parsed.requiredMethods = [...(parsed.requiredMethods || []), args[++i]];
164
+ break;
165
+ case "--min-total":
166
+ parsed.minTotal = args[++i];
167
+ break;
114
168
  case "--base-url":
115
169
  parsed.baseUrl = args[++i];
116
170
  break;
117
171
  case "--allow-launch":
118
172
  parsed.allowLaunch = true;
119
173
  break;
174
+ case "--task":
175
+ parsed.task = args[++i];
176
+ break;
177
+ case "--menu-api-pattern":
178
+ parsed.menuApiPattern = args[++i];
179
+ break;
180
+ case "--menu-limit":
181
+ parsed.menuLimit = Number.parseInt(args[++i], 10);
182
+ break;
183
+ case "--menu-path-prefix":
184
+ parsed.menuPathPrefix = (parsed.menuPathPrefix || []).concat(args[++i]);
185
+ break;
186
+ case "--menu-filter":
187
+ parsed.menuFilter = (parsed.menuFilter || []).concat(args[++i]);
188
+ break;
189
+ case "--drawer-trigger-selector":
190
+ parsed.drawerTriggerSelector = args[++i];
191
+ break;
192
+ case "--drawer-open-selector":
193
+ parsed.drawerOpenSelector = args[++i];
194
+ break;
195
+ case "--menu-scroll-container":
196
+ parsed.menuScrollContainer = args[++i];
197
+ break;
198
+ case "--use-native-hover":
199
+ parsed.useNativeHover = args[++i] !== "false";
200
+ break;
201
+ case "--max-retry-per-step":
202
+ parsed.maxRetryPerStep = Number.parseInt(args[++i], 10);
203
+ break;
204
+ case "--snapshot-each":
205
+ parsed.snapshotEach = true;
206
+ break;
207
+ case "--screenshot-each":
208
+ parsed.screenshotEach = true;
209
+ break;
210
+ case "--continue-on-fail":
211
+ parsed.continueOnFail = args[++i] !== "false";
212
+ break;
213
+ case "--no-continue-on-fail":
214
+ parsed.continueOnFail = false;
215
+ break;
216
+ case "--capture-input-file":
217
+ parsed.captureInputFile = args[++i];
218
+ break;
120
219
  case "--help":
121
220
  case "-h":
122
221
  printHelp();
@@ -133,12 +232,16 @@ function printHelp() {
133
232
  用法:
134
233
  node local-browser-executor.js --action observe [options]
135
234
  node local-browser-executor.js --action act --index 1 --observe-after [options]
235
+ node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after [options]
136
236
  node local-browser-executor.js --action assert --selector ".ant-table" --expect-visible [options]
237
+ node local-browser-executor.js --action api-capture --wait 15000 --include-response-body [options]
238
+ node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0 [options]
239
+ node local-browser-executor.js --action run-task --task menu-api-scan --capture-input-file /tmp/menu-tree.json [options]
137
240
  node local-browser-executor.js --plan '<JSON>' [options]
138
241
  node local-browser-executor.js --plan-file <path> [options]
139
242
 
140
243
  选项:
141
- --action <name> 直接执行动作,当前支持 observe/act/assert
244
+ --action <name> 直接执行动作,当前支持 observe/act/assert/api-capture/assert-api/run-task
142
245
  --plan <json> 测试计划 JSON 字符串
143
246
  --plan-file <path> 测试计划 JSON 文件路径
144
247
  --output <path> 结果输出文件路径(默认 stdout)
@@ -156,14 +259,45 @@ function printHelp() {
156
259
  --type <text> act 时输入文本
157
260
  --clear act 输入前清空目标
158
261
  --hover <selector> act 时悬停 selector/text=/role=/~ 目标
262
+ --menu-path <a/b> act 时按菜单路径展开并点击,如 "目录管理/数据集目录"
263
+ --wait-after-each <ms> menu-path 每步点击后的等待时间(默认 300)
159
264
  --observe-after act 后再次 observe,返回 before/act/after 闭环结果
160
265
  --url-contains-assert <text> assert 当前 URL 包含文本
161
266
  --text-contains <text> assert 页面文本包含内容
162
267
  --expect-visible assert selector 可见
163
268
  --min-count <n> assert selector 数量下限
164
269
  --has-text <text> assert selector 文本包含内容
270
+ --wait <ms> api-capture 监听时长(默认 15000)
271
+ --method <GET|POST> api-capture 按请求方法过滤
272
+ --url-pattern <text> api-capture 按 URL 片段过滤
273
+ --target-url-contains <text> api-capture 按 target URL 过滤 iframe/page target
274
+ --include-request-body api-capture 包含请求体(默认包含)
275
+ --no-request-body api-capture 不包含请求体
276
+ --include-response-body api-capture 调用 Network.getResponseBody 获取响应体
277
+ --reload api-capture 开始监听后刷新页面触发请求
278
+ --include-all-targets api-capture 不限制到匹配 frame,监听所有 page/iframe targets
279
+ --capture-file <path> assert-api 读取 api-capture JSON 结果
280
+ --max-failed <n> assert-api 允许失败请求数上限
281
+ --forbid-status-gte <n> assert-api 禁止出现大于等于该状态码的响应
282
+ --required-url <text> assert-api 要求出现的 URL 片段,可重复传入
283
+ --required-method <GET|POST> assert-api 要求出现的请求方法,可重复传入
284
+ --min-total <n> assert-api 要求捕获请求总数下限
165
285
  --allow-launch action 模式允许无 CDP 时启动 headless Chrome
166
286
  --base-url <url> 基础 URL,替换计划中的 {{baseUrl}} 占位符
287
+ --task <name> run-task 任务名(当前仅支持 menu-api-scan)
288
+ --capture-input-file <path> run-task 读取已抓到的菜单接口 JSON 作为 lastApiCapture
289
+ --menu-api-pattern <regex> run-task 在 lastApiCapture 中筛选菜单接口的正则(默认 listUserViewMenus)
290
+ --menu-limit <n> run-task 仅探索前 N 条菜单
291
+ --menu-path-prefix <text> run-task 仅探索 path 以此开头的菜单(可重复)
292
+ --menu-filter <text> run-task 仅探索 name/path 包含此关键字的菜单(可重复)
293
+ --drawer-trigger-selector <css> run-task / act 抽屉式菜单的触发器选择器
294
+ --drawer-open-selector <css> run-task / act 抽屉打开态选择器(默认 .ant-drawer-open)
295
+ --menu-scroll-container <css> run-task / act 菜单容器需要滚动时的容器选择器
296
+ --use-native-hover <true|false> run-task / act 使用 puppeteer 原生 hover 触发子菜单(默认 true)
297
+ --max-retry-per-step <n> run-task / act 每级菜单查找重试次数(默认 2)
298
+ --snapshot-each run-task 每条菜单成功后做一次轻量 observe
299
+ --screenshot-each run-task 失败时截图(依赖 outputDir,默认关闭)
300
+ --no-continue-on-fail run-task 第一条失败即终止(默认遇错继续)
167
301
  -h, --help 显示帮助
168
302
  `);
169
303
  }
@@ -269,6 +403,25 @@ async function executePlan(plan, options) {
269
403
 
270
404
  // ==================== 主流程 ====================
271
405
 
406
+ function executeAssertApiAction(args) {
407
+ if (!args.captureFile) {
408
+ return {
409
+ error: "必须提供 --capture-file 参数",
410
+ errorCode: "CAPTURE_FILE_REQUIRED",
411
+ };
412
+ }
413
+
414
+ const captureResult = JSON.parse(fs.readFileSync(args.captureFile, "utf8"));
415
+ const engine = new BrowserEngine();
416
+ return engine.assertApi(captureResult, {
417
+ maxFailed: args.maxFailed,
418
+ forbidStatusGte: args.forbidStatusGte,
419
+ requiredUrls: args.requiredUrls,
420
+ requiredMethods: args.requiredMethods,
421
+ minTotal: args.minTotal,
422
+ });
423
+ }
424
+
272
425
  async function executeAction(args) {
273
426
  const engine = new BrowserEngine();
274
427
  const startedAt = new Date().toISOString();
@@ -308,6 +461,8 @@ async function executeAction(args) {
308
461
  selector: args.selector,
309
462
  click: args.click,
310
463
  hover: args.hover,
464
+ menuPath: args.menuPath,
465
+ waitAfterEach: args.waitAfterEach,
311
466
  typeText: args.typeText,
312
467
  clear: args.clear,
313
468
  frameUrlContains: args.frameUrlContains,
@@ -342,11 +497,69 @@ async function executeAction(args) {
342
497
  });
343
498
  return { type: "assert", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
344
499
  }
500
+ case "api-capture":
501
+ case "apiCapture": {
502
+ const result = await engine.captureApi({
503
+ wait: args.wait,
504
+ method: args.method,
505
+ urlPattern: args.urlPattern,
506
+ targetUrlContains: args.targetUrlContains,
507
+ includeRequestBody: args.includeRequestBody,
508
+ includeResponseBody: args.includeResponseBody,
509
+ reload: args.reload,
510
+ includeAllTargets: args.includeAllTargets,
511
+ frameUrlContains: args.frameUrlContains,
512
+ frameContentContains: args.frameContentContains,
513
+ frameIndex: args.frameIndex,
514
+ });
515
+ return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
516
+ }
517
+ case "run-task":
518
+ case "runTask": {
519
+ // 把外部 captureFile 当作 lastApiCapture 注入 context
520
+ const context = {};
521
+ if (args.captureInputFile) {
522
+ const fs = await import("node:fs");
523
+ try {
524
+ context.lastApiCapture = JSON.parse(fs.readFileSync(args.captureInputFile, "utf8"));
525
+ } catch (err) {
526
+ return {
527
+ error: `读取 capture 输入文件失败: ${err.message}`,
528
+ errorCode: "CAPTURE_INPUT_INVALID",
529
+ startTime: startedAt,
530
+ endTime: new Date().toISOString(),
531
+ };
532
+ }
533
+ }
534
+ const result = await engine.runTask({
535
+ task: args.task || "menu-api-scan",
536
+ menuApiPattern: args.menuApiPattern,
537
+ menuLimit: args.menuLimit,
538
+ menuPathPrefix: args.menuPathPrefix,
539
+ menuFilter: args.menuFilter,
540
+ drawerTriggerSelector: args.drawerTriggerSelector,
541
+ drawerOpenSelector: args.drawerOpenSelector,
542
+ menuScrollContainer: args.menuScrollContainer,
543
+ useNativeHover: args.useNativeHover,
544
+ maxRetryPerStep: args.maxRetryPerStep,
545
+ snapshotEach: args.snapshotEach,
546
+ screenshotEach: args.screenshotEach,
547
+ continueOnFail: args.continueOnFail,
548
+ apiCaptureOptions: {
549
+ wait: args.wait,
550
+ targetUrlContains: args.targetUrlContains,
551
+ includeAllTargets: args.includeAllTargets,
552
+ includeResponseBody: args.includeResponseBody !== false,
553
+ urlPattern: args.urlPattern,
554
+ },
555
+ }, context);
556
+ return { type: "runTask", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
557
+ }
345
558
  default:
346
559
  return {
347
560
  error: `未知 action: ${args.action}`,
348
561
  errorCode: "UNKNOWN_ACTION",
349
- supportedActions: ["observe", "act", "assert"],
562
+ supportedActions: ["observe", "act", "assert", "api-capture", "assert-api", "run-task"],
350
563
  startTime: startedAt,
351
564
  endTime: new Date().toISOString(),
352
565
  };
@@ -367,6 +580,21 @@ async function executeAction(args) {
367
580
  async function main() {
368
581
  const args = parseArgs();
369
582
 
583
+ if (args.action === "assert-api" || args.action === "assertApi") {
584
+ try {
585
+ const results = executeAssertApiAction(args);
586
+ outputResult(results, args.output);
587
+ process.exit(results.error ? 2 : results.passed === false ? 1 : 0);
588
+ } catch (err) {
589
+ const errorResult = {
590
+ error: err.message,
591
+ errorCode: "ASSERT_API_FAILED",
592
+ };
593
+ outputResult(errorResult, args.output);
594
+ process.exit(2);
595
+ }
596
+ }
597
+
370
598
  if (args.action) {
371
599
  const results = await executeAction(args);
372
600
  outputResult(results, args.output);
@@ -79,8 +79,10 @@ AI 直接构造测试计划 JSON:
79
79
  | type | 功能 | 参数 | 返回 |
80
80
  |------|------|------|------|
81
81
  | observe | 结构化观察 | `selector`, `depth`, `frameContentContains`, `maxInteractiveElements` | `{ frames, tree, interactiveElements[] }` |
82
- | act | 按 index/selector 交互 | `index` 或 `selector/click/hover`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element }` |
82
+ | act | 按 index/selector/menuPath 交互 | `index` 或 `selector/click/hover` 或 `menuPath`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element/steps }` |
83
83
  | assert | 轻量断言 | `selector`, `expect`, `urlContains`, `textContains` | `{ passed, checks }` |
84
+ | apiCapture | API 请求/响应捕获 | `wait`, `urlPattern`, `method`, `includeResponseBody`, `frameContentContains` | `{ targets, requests, summary }` |
85
+ | assertApi | API 捕获结果断言 | `maxFailed`, `requiredUrls`, `forbidStatusGte`, `minTotal` | `{ passed, checks, summary }` |
84
86
  | navigate | 导航到 URL | `url`, `waitUntil` | `{ url, title, loadTime }` |
85
87
  | screenshot | 页面截图 | `fullPage`, `clip`, `filename` | `{ filepath, width, height }` |
86
88
  | compare | 设计稿对比 | `designImagePath`, `threshold`, `regions[]` | `{ fidelity, diffPixels, diffImagePath, regions[] }` |
@@ -93,6 +95,7 @@ AI 直接构造测试计划 JSON:
93
95
  | findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
94
96
  | loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
95
97
  | aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
98
+ | runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
96
99
 
97
100
  ### 场景选择策略
98
101
 
@@ -236,6 +239,18 @@ node {client_path}/local-browser-executor.js \
236
239
  --output /tmp/browser-act-observe.json
237
240
  ```
238
241
 
242
+ ### 左侧菜单路径稳定点击
243
+ ```bash
244
+ node {client_path}/local-browser-executor.js \
245
+ --action act \
246
+ --url-contains "platform.aicityos.com" \
247
+ --menu-path "目录管理/数据集目录" \
248
+ --observe-after \
249
+ --output /tmp/browser-menu-act.json
250
+ ```
251
+
252
+ `--menu-path` 会按路径逐级匹配菜单项,非末级优先执行展开,末级执行点击;匹配优先考虑 `role=menuitem`、`.ant-menu-item`、`.ant-menu-submenu-title` 和文本精确/包含匹配。
253
+
239
254
  ### 轻量 DOM 断言
240
255
  ```bash
241
256
  node {client_path}/local-browser-executor.js \
@@ -247,6 +262,34 @@ node {client_path}/local-browser-executor.js \
247
262
  --output /tmp/browser-assert.json
248
263
  ```
249
264
 
265
+ ### API 请求/响应捕获(真实浏览器 + 微前端)
266
+ ```bash
267
+ node {client_path}/local-browser-executor.js \
268
+ --action api-capture \
269
+ --url-contains "platform.aicityos.com" \
270
+ --frame-content-contains "数据集目录" \
271
+ --url-pattern "/api/" \
272
+ --wait 15000 \
273
+ --include-response-body \
274
+ --reload \
275
+ --output /tmp/browser-api-capture.json
276
+ ```
277
+
278
+ `api-capture` 使用 CDP `Network.*` 事件监听主页面和可 attach 的 page/iframe target,不使用 `setRequestInterception`,避免阻断真实请求。wujie/qiankun 场景优先传 `--frame-content-contains` 锁定业务 iframe;如需监听全部 target,追加 `--include-all-targets`。
279
+
280
+ ### API 捕获结果断言
281
+ ```bash
282
+ node {client_path}/local-browser-executor.js \
283
+ --action assert-api \
284
+ --capture-file /tmp/browser-api-capture.json \
285
+ --max-failed 0 \
286
+ --forbid-status-gte 400 \
287
+ --required-url "/list" \
288
+ --output /tmp/browser-api-assert.json
289
+ ```
290
+
291
+ 计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
292
+
250
293
  ### 快速诊断(过渡兼容)
251
294
  ```bash
252
295
  NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
@@ -446,3 +489,71 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
446
489
  "outputDir": "/tmp/browser-test-xxx"
447
490
  }
448
491
  ```
492
+
493
+ ## 批量动态菜单扫描(runTask)
494
+
495
+ 适用场景:测试系统有 N 条动态路由(来自后端菜单接口),要逐条点击触发并验证接口是否返回 200。6/18 反馈中,AI 最终用 130 余条 `act --menu-path` + `api-capture` 手工组合走完 129 条菜单——`runTask` 把这套流水固化为单步骤。
496
+
497
+ 工作流:
498
+
499
+ 1. **第一步:先抓菜单接口**(一次即可,作为整张菜单树的来源)
500
+ ```bash
501
+ node local-browser-executor.js --action api-capture \
502
+ --url-pattern "listUserViewMenus" \
503
+ --include-response-body \
504
+ --include-all-targets \
505
+ --reload --wait 8000 \
506
+ --output /tmp/menu-tree.json
507
+ ```
508
+
509
+ 2. **第二步:把菜单树喂给 runTask**
510
+ ```bash
511
+ node local-browser-executor.js --action run-task \
512
+ --task menu-api-scan \
513
+ --capture-input-file /tmp/menu-tree.json \
514
+ --menu-path-prefix /catalog \
515
+ --menu-limit 20 \
516
+ --drawer-trigger-selector "[data-test=menu-toggle]" \
517
+ --use-native-hover true \
518
+ --max-retry-per-step 2 \
519
+ --wait 4000 \
520
+ --output /tmp/menu-scan-report.json
521
+ ```
522
+
523
+ 3. **结果解读**:`byOutcome` 给出四档统计:
524
+ - `apiOk`:菜单点开 + 接口 200 ✅
525
+ - `apiFailed`:菜单点开 + 接口 ≥400(看 `results[i].api.requests` 找具体 URL)
526
+ - `noApi`:菜单点开但没产生任何 XHR(可能是纯静态页或路由没触发请求)
527
+ - `clickFailed`:菜单点不开(看 `results[i].menuResult.failedAt` + `candidates`)
528
+
529
+ 4. **plan JSON 形式**(也可作为 `--plan-file` 一步:先 apiCapture 再 runTask):
530
+ ```json
531
+ {
532
+ "steps": [
533
+ { "type": "apiCapture", "urlPattern": "listUserViewMenus", "includeResponseBody": true, "reload": true, "wait": 8000, "includeAllTargets": true },
534
+ { "type": "runTask", "task": "menu-api-scan", "menuLimit": 30, "drawerTriggerSelector": "[data-test=menu-toggle]" }
535
+ ]
536
+ }
537
+ ```
538
+ `runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
539
+
540
+ ## 反模式备忘(来自 6/17 + 6/18 实战反馈)
541
+
542
+ | 错误做法 | 正确做法 | 原因 |
543
+ |---------|---------|------|
544
+ | `setRequestInterception(true)` 抓接口 | 仅 CDP `Network.responseReceived` + `getResponseBody` | request interception 会拦下 wujie iframe 的 RPC 帧造成挂起 |
545
+ | `page.click("text=保存")` 不加可见性过滤 | 默认走 `act` 的 `text=`,已自动只匹配 `visible+enabled` 的元素 | 隐藏在收起菜单/未渲染抽屉里的同名文本会被错误命中 |
546
+ | 多级菜单只用一次 `dispatchEvent("mouseover")` | `useNativeHover: true`(默认) + `drawerTriggerSelector` 每级前重开 | Vue 的 v-on:mouseenter 对 dispatchEvent 不触发;抽屉式菜单点完一次会自动收起 |
547
+ | `setTimeout(awaitPage.bringToFront(), 30000)` 之外不做兜底 | 调用前自查"目标页是否还活着"(`findPage` / `_findFrame`),失败立刻退而求其次操作主帧 | bringToFront 在 OS 抢焦点失败时会无限挂起,没超时 |
548
+ | iframe attach 失败就报错退出 | `attachRetries` 默认 3 次、间隔 1.5s 重抓 `browser.targets()` | wujie 的 blob iframe target 在主页面 ready 后才注册,时序敏感 |
549
+ | 大批量探索每条都 throw | `_actMenuPath` 失败时返回 `acted:false + candidates`,配合 `runTask --continue-on-fail` | 探索性扫描遇到一条点不开就全部中断,没法发现整体面 |
550
+
551
+ ## 非目标声明
552
+
553
+ `local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
554
+
555
+ - ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
556
+ - ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
557
+ - ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
558
+ - ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
559
+ - ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描
@@ -38,6 +38,21 @@
38
38
  * --max-rounds <n> 最大轮数(默认 60)
39
39
  */
40
40
 
41
+ // 共享缓存自愈:6/17 反馈中 NODE_PATH 未注入会导致 puppeteer-core MODULE_NOT_FOUND。
42
+ // 把 ~/.szcd-mcp/deps/node_modules 注入 module 解析路径,再 require puppeteer-core。
43
+ (function ensureSharedDeps() {
44
+ try {
45
+ const os = require('os');
46
+ const Module = require('module');
47
+ const sharedDeps = require('path').join(os.homedir(), '.szcd-mcp', 'deps', 'node_modules');
48
+ if (require('fs').existsSync(sharedDeps) && !Module.globalPaths.includes(sharedDeps)) {
49
+ Module.globalPaths.push(sharedDeps);
50
+ }
51
+ } catch {
52
+ // ignore — 没有共享缓存时退回项目本地 node_modules
53
+ }
54
+ })();
55
+
41
56
  const puppeteer = require('puppeteer-core');
42
57
  const fs = require('fs');
43
58
  const path = require('path');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@szc-ft/mcp-szcd-client",
3
- "version": "0.27.2",
3
+ "version": "0.27.4",
4
4
  "description": "MCP client for szcd component library - auto-configures AI coding tools with MCP server, skills, agents and commands",
5
5
  "keywords": [
6
6
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "szcd-component-helper",
3
- "version": "0.27.2",
3
+ "version": "0.27.4",
4
4
  "description": "szcd 组件库 MCP 助手 — 查询组件信息、匹配需求、生成代码",
5
5
  "mcpServers": {
6
6
  "szcd-component-helper": {
@@ -79,8 +79,10 @@ AI 直接构造测试计划 JSON:
79
79
  | type | 功能 | 参数 | 返回 |
80
80
  |------|------|------|------|
81
81
  | observe | 结构化观察 | `selector`, `depth`, `frameContentContains`, `maxInteractiveElements` | `{ frames, tree, interactiveElements[] }` |
82
- | act | 按 index/selector 交互 | `index` 或 `selector/click/hover`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element }` |
82
+ | act | 按 index/selector/menuPath 交互 | `index` 或 `selector/click/hover` 或 `menuPath`, `typeText`, `clear`, `frameContentContains` | `{ acted, action, element/steps }` |
83
83
  | assert | 轻量断言 | `selector`, `expect`, `urlContains`, `textContains` | `{ passed, checks }` |
84
+ | apiCapture | API 请求/响应捕获 | `wait`, `urlPattern`, `method`, `includeResponseBody`, `frameContentContains` | `{ targets, requests, summary }` |
85
+ | assertApi | API 捕获结果断言 | `maxFailed`, `requiredUrls`, `forbidStatusGte`, `minTotal` | `{ passed, checks, summary }` |
84
86
  | navigate | 导航到 URL | `url`, `waitUntil` | `{ url, title, loadTime }` |
85
87
  | screenshot | 页面截图 | `fullPage`, `clip`, `filename` | `{ filepath, width, height }` |
86
88
  | compare | 设计稿对比 | `designImagePath`, `threshold`, `regions[]` | `{ fidelity, diffPixels, diffImagePath, regions[] }` |
@@ -93,6 +95,7 @@ AI 直接构造测试计划 JSON:
93
95
  | findFrame | 查找微前端 iframe | `urlIncludes` 或 `urlRegex` 或 `titleIncludes` 或 `contentIncludes` 或 `frameIndex` | `{ found, frameUrl, matchedBy, totalFrames }` |
94
96
  | loginWait | 等待 SSO 登录完成 | `loginSelector`, `timeout`, `interval` | `{ loggedIn, elapsed, currentUrl }` |
95
97
  | aiAssert | 语义断言(截图+审查) | `assertion`, `filename` | `{ screenshotPath, assertion, needsVisualReview }` |
98
+ | runTask | 高层任务 runner(批量动态菜单扫描) | `task: "menu-api-scan"`, `menuApiPattern`, `menuLimit`, `menuPathPrefix[]`, `menuFilter[]`, `drawerTriggerSelector`, `useNativeHover`, `apiCaptureOptions`, `continueOnFail` | `{ task, discovered, attempted, byOutcome, results[] }` |
96
99
 
97
100
  ### 场景选择策略
98
101
 
@@ -236,6 +239,18 @@ node {client_path}/local-browser-executor.js \
236
239
  --output /tmp/browser-act-observe.json
237
240
  ```
238
241
 
242
+ ### 左侧菜单路径稳定点击
243
+ ```bash
244
+ node {client_path}/local-browser-executor.js \
245
+ --action act \
246
+ --url-contains "platform.aicityos.com" \
247
+ --menu-path "目录管理/数据集目录" \
248
+ --observe-after \
249
+ --output /tmp/browser-menu-act.json
250
+ ```
251
+
252
+ `--menu-path` 会按路径逐级匹配菜单项,非末级优先执行展开,末级执行点击;匹配优先考虑 `role=menuitem`、`.ant-menu-item`、`.ant-menu-submenu-title` 和文本精确/包含匹配。
253
+
239
254
  ### 轻量 DOM 断言
240
255
  ```bash
241
256
  node {client_path}/local-browser-executor.js \
@@ -247,6 +262,34 @@ node {client_path}/local-browser-executor.js \
247
262
  --output /tmp/browser-assert.json
248
263
  ```
249
264
 
265
+ ### API 请求/响应捕获(真实浏览器 + 微前端)
266
+ ```bash
267
+ node {client_path}/local-browser-executor.js \
268
+ --action api-capture \
269
+ --url-contains "platform.aicityos.com" \
270
+ --frame-content-contains "数据集目录" \
271
+ --url-pattern "/api/" \
272
+ --wait 15000 \
273
+ --include-response-body \
274
+ --reload \
275
+ --output /tmp/browser-api-capture.json
276
+ ```
277
+
278
+ `api-capture` 使用 CDP `Network.*` 事件监听主页面和可 attach 的 page/iframe target,不使用 `setRequestInterception`,避免阻断真实请求。wujie/qiankun 场景优先传 `--frame-content-contains` 锁定业务 iframe;如需监听全部 target,追加 `--include-all-targets`。
279
+
280
+ ### API 捕获结果断言
281
+ ```bash
282
+ node {client_path}/local-browser-executor.js \
283
+ --action assert-api \
284
+ --capture-file /tmp/browser-api-capture.json \
285
+ --max-failed 0 \
286
+ --forbid-status-gte 400 \
287
+ --required-url "/list" \
288
+ --output /tmp/browser-api-assert.json
289
+ ```
290
+
291
+ 计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
292
+
250
293
  ### 快速诊断(过渡兼容)
251
294
  ```bash
252
295
  NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
@@ -446,3 +489,71 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
446
489
  "outputDir": "/tmp/browser-test-xxx"
447
490
  }
448
491
  ```
492
+
493
+ ## 批量动态菜单扫描(runTask)
494
+
495
+ 适用场景:测试系统有 N 条动态路由(来自后端菜单接口),要逐条点击触发并验证接口是否返回 200。6/18 反馈中,AI 最终用 130 余条 `act --menu-path` + `api-capture` 手工组合走完 129 条菜单——`runTask` 把这套流水固化为单步骤。
496
+
497
+ 工作流:
498
+
499
+ 1. **第一步:先抓菜单接口**(一次即可,作为整张菜单树的来源)
500
+ ```bash
501
+ node local-browser-executor.js --action api-capture \
502
+ --url-pattern "listUserViewMenus" \
503
+ --include-response-body \
504
+ --include-all-targets \
505
+ --reload --wait 8000 \
506
+ --output /tmp/menu-tree.json
507
+ ```
508
+
509
+ 2. **第二步:把菜单树喂给 runTask**
510
+ ```bash
511
+ node local-browser-executor.js --action run-task \
512
+ --task menu-api-scan \
513
+ --capture-input-file /tmp/menu-tree.json \
514
+ --menu-path-prefix /catalog \
515
+ --menu-limit 20 \
516
+ --drawer-trigger-selector "[data-test=menu-toggle]" \
517
+ --use-native-hover true \
518
+ --max-retry-per-step 2 \
519
+ --wait 4000 \
520
+ --output /tmp/menu-scan-report.json
521
+ ```
522
+
523
+ 3. **结果解读**:`byOutcome` 给出四档统计:
524
+ - `apiOk`:菜单点开 + 接口 200 ✅
525
+ - `apiFailed`:菜单点开 + 接口 ≥400(看 `results[i].api.requests` 找具体 URL)
526
+ - `noApi`:菜单点开但没产生任何 XHR(可能是纯静态页或路由没触发请求)
527
+ - `clickFailed`:菜单点不开(看 `results[i].menuResult.failedAt` + `candidates`)
528
+
529
+ 4. **plan JSON 形式**(也可作为 `--plan-file` 一步:先 apiCapture 再 runTask):
530
+ ```json
531
+ {
532
+ "steps": [
533
+ { "type": "apiCapture", "urlPattern": "listUserViewMenus", "includeResponseBody": true, "reload": true, "wait": 8000, "includeAllTargets": true },
534
+ { "type": "runTask", "task": "menu-api-scan", "menuLimit": 30, "drawerTriggerSelector": "[data-test=menu-toggle]" }
535
+ ]
536
+ }
537
+ ```
538
+ `runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
539
+
540
+ ## 反模式备忘(来自 6/17 + 6/18 实战反馈)
541
+
542
+ | 错误做法 | 正确做法 | 原因 |
543
+ |---------|---------|------|
544
+ | `setRequestInterception(true)` 抓接口 | 仅 CDP `Network.responseReceived` + `getResponseBody` | request interception 会拦下 wujie iframe 的 RPC 帧造成挂起 |
545
+ | `page.click("text=保存")` 不加可见性过滤 | 默认走 `act` 的 `text=`,已自动只匹配 `visible+enabled` 的元素 | 隐藏在收起菜单/未渲染抽屉里的同名文本会被错误命中 |
546
+ | 多级菜单只用一次 `dispatchEvent("mouseover")` | `useNativeHover: true`(默认) + `drawerTriggerSelector` 每级前重开 | Vue 的 v-on:mouseenter 对 dispatchEvent 不触发;抽屉式菜单点完一次会自动收起 |
547
+ | `setTimeout(awaitPage.bringToFront(), 30000)` 之外不做兜底 | 调用前自查"目标页是否还活着"(`findPage` / `_findFrame`),失败立刻退而求其次操作主帧 | bringToFront 在 OS 抢焦点失败时会无限挂起,没超时 |
548
+ | iframe attach 失败就报错退出 | `attachRetries` 默认 3 次、间隔 1.5s 重抓 `browser.targets()` | wujie 的 blob iframe target 在主页面 ready 后才注册,时序敏感 |
549
+ | 大批量探索每条都 throw | `_actMenuPath` 失败时返回 `acted:false + candidates`,配合 `runTask --continue-on-fail` | 探索性扫描遇到一条点不开就全部中断,没法发现整体面 |
550
+
551
+ ## 非目标声明
552
+
553
+ `local-browser-test` **不**承担以下职责,遇到时显式拒绝并指给正确工具:
554
+
555
+ - ❌ 接口压力测试 / 并发性能验证 → 用 k6、wrk、jmeter
556
+ - ❌ 跨浏览器兼容性矩阵(Safari/Firefox 多版本)→ BrowserStack / SauceLabs
557
+ - ❌ 单元测试 / 组件级测试 → Vitest / Jest / RTL
558
+ - ❌ 视觉回归全量对比(无 baseline 设计稿)→ Percy / Chromatic
559
+ - ✅ 真实浏览器内的功能验证、API 时序捕获、设计稿单图对比、动态菜单批量扫描