@szc-ft/mcp-szcd-client 0.27.1 → 0.27.3

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.
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * 共享依赖加载器
3
3
  *
4
- * 浏览器自动化依赖(puppeteer-core, pixelmatch, pngjs, sharp)
5
- * 安装到 ~/.szcd-mcp/deps/node_modules/,所有项目共享复用。
6
- * 首次使用时自动安装,后续直接加载。
4
+ * 浏览器自动化依赖安装到 ~/.szcd-mcp/deps/node_modules/,所有项目共享复用。
5
+ * 主链路 observe/act/assert 只安装 puppeteer-core;视觉对比按需安装 pixelmatch/pngjs/sharp。
7
6
  */
8
7
 
9
8
  import { createRequire } from "node:module";
@@ -14,11 +13,14 @@ import fs from "node:fs";
14
13
  /** 共享缓存目录 */
15
14
  const SHARED_DEPS_DIR = path.join(homedir(), ".szcd-mcp", "deps");
16
15
 
17
- /** 需要共享安装的包列表 */
18
- const PACKAGES = ["puppeteer-core", "pixelmatch", "pngjs", "sharp"];
16
+ /** 核心浏览器自动化依赖 */
17
+ const CORE_PACKAGES = ["puppeteer-core"];
19
18
 
20
- /** 标记文件,存在表示依赖已安装 */
21
- const INSTALLED_MARKER = path.join(SHARED_DEPS_DIR, ".installed");
19
+ /** 视觉对比依赖 */
20
+ const VISUAL_COMPARE_PACKAGES = ["pixelmatch", "pngjs", "sharp"];
21
+
22
+ /** 兼容旧导出 */
23
+ const PACKAGES = [...CORE_PACKAGES, ...VISUAL_COMPARE_PACKAGES];
22
24
 
23
25
  /**
24
26
  * 从共享缓存目录加载模块
@@ -30,27 +32,9 @@ export function loadFromShared(moduleName) {
30
32
  return require_(moduleName);
31
33
  }
32
34
 
33
- /**
34
- * 确保共享依赖已安装,未安装则自动安装
35
- * @returns {Promise<void>}
36
- */
37
- export async function ensureSharedDeps() {
38
- // 检查标记文件
39
- if (fs.existsSync(INSTALLED_MARKER)) {
40
- // 快速验证:puppeteer-core 目录是否存在
41
- const pcoreDir = path.join(SHARED_DEPS_DIR, "node_modules", "puppeteer-core");
42
- if (fs.existsSync(pcoreDir)) {
43
- return; // 已安装
44
- }
45
- }
46
-
47
- process.stderr.write("[shared-deps] 首次使用浏览器测试功能,正在安装共享依赖...\n");
48
- process.stderr.write(`[shared-deps] 安装目录: ${SHARED_DEPS_DIR}\n`);
49
-
50
- // 确保目录存在
35
+ function ensurePackageJson() {
51
36
  fs.mkdirSync(SHARED_DEPS_DIR, { recursive: true });
52
37
 
53
- // 初始化 package.json(如果不存在)
54
38
  const pkgPath = path.join(SHARED_DEPS_DIR, "package.json");
55
39
  if (!fs.existsSync(pkgPath)) {
56
40
  fs.writeFileSync(pkgPath, JSON.stringify({
@@ -59,39 +43,95 @@ export async function ensureSharedDeps() {
59
43
  private: true,
60
44
  }, null, 2));
61
45
  }
46
+ }
47
+
48
+ function isPackageInstalled(packageName) {
49
+ try {
50
+ const require_ = createRequire(path.join(SHARED_DEPS_DIR, "node_modules", "_"));
51
+ require_.resolve(packageName);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async function installPackages(packages, label) {
59
+ const missing = packages.filter((packageName) => !isPackageInstalled(packageName));
60
+ if (missing.length === 0) return;
61
+
62
+ process.stderr.write(`[shared-deps] 正在安装${label}: ${missing.join(" ")}\n`);
63
+ process.stderr.write(`[shared-deps] 安装目录: ${SHARED_DEPS_DIR}\n`);
64
+
65
+ ensurePackageJson();
62
66
 
63
- // 安装依赖
64
67
  const { execSync } = await import("node:child_process");
65
- const pkgList = PACKAGES.join(" ");
66
- const mirrorFlag = "--sharp_binary_host=https://npmmirror.com/mirrors/sharp";
67
- const registryFlag = "--registry=https://npmmirror.com";
68
+ const pkgList = missing.join(" ");
69
+ const registryFlags = [
70
+ "--registry=https://registry.npmmirror.com",
71
+ "--@img:registry=https://registry.npmmirror.com",
72
+ ];
73
+ const speedFlags = [
74
+ "--prefer-offline",
75
+ "--no-audit",
76
+ "--no-fund",
77
+ "--omit=dev",
78
+ "--package-lock=false",
79
+ "--loglevel=warn",
80
+ ];
81
+ const sharpFlags = missing.includes("sharp") ? [
82
+ "--sharp_binary_host=https://npmmirror.com/mirrors/sharp",
83
+ "--sharp_libvips_binary_host=https://npmmirror.com/mirrors/sharp-libvips",
84
+ ] : [];
85
+ const installFlags = [...registryFlags, ...speedFlags, ...sharpFlags].join(" ");
68
86
 
69
87
  try {
70
- execSync(`npm install ${registryFlag} ${mirrorFlag} ${pkgList}`, {
88
+ execSync(`npm install ${installFlags} ${pkgList}`.trim(), {
71
89
  cwd: SHARED_DEPS_DIR,
72
90
  stdio: "inherit",
73
91
  timeout: 180000,
74
92
  });
75
93
  } catch (err) {
76
94
  throw new Error(
77
- `浏览器测试依赖安装失败。请手动执行:\n` +
78
- ` cd ${SHARED_DEPS_DIR} && npm install ${pkgList}\n` +
95
+ `${label}安装失败。请手动执行:\n` +
96
+ ` cd ${SHARED_DEPS_DIR} && npm install ${installFlags} ${pkgList}\n` +
79
97
  `原始错误:${err.message}`
80
98
  );
81
99
  }
82
100
 
83
- // 写入标记文件
84
- fs.writeFileSync(INSTALLED_MARKER, new Date().toISOString());
85
- process.stderr.write("[shared-deps] 共享依赖安装完成!后续所有项目将复用此安装。\n");
101
+ process.stderr.write(`[shared-deps] ${label}安装完成。\n`);
102
+ }
103
+
104
+ export async function ensureCoreDeps() {
105
+ await installPackages(CORE_PACKAGES, "浏览器核心依赖");
106
+ }
107
+
108
+ export async function ensureVisualCompareDeps() {
109
+ await ensureCoreDeps();
110
+ await installPackages(VISUAL_COMPARE_PACKAGES, "视觉对比依赖");
111
+ }
112
+
113
+ /**
114
+ * 确保全部共享依赖已安装,保留给旧调用兼容。
115
+ * @returns {Promise<void>}
116
+ */
117
+ export async function ensureSharedDeps() {
118
+ await ensureVisualCompareDeps();
119
+ }
120
+
121
+ /**
122
+ * 检查核心依赖是否已安装
123
+ * @returns {boolean}
124
+ */
125
+ export function isCoreDepsInstalled() {
126
+ return CORE_PACKAGES.every(isPackageInstalled);
86
127
  }
87
128
 
88
129
  /**
89
- * 检查共享依赖是否已安装
130
+ * 检查全部共享依赖是否已安装
90
131
  * @returns {boolean}
91
132
  */
92
133
  export function isSharedDepsInstalled() {
93
- return fs.existsSync(INSTALLED_MARKER) &&
94
- fs.existsSync(path.join(SHARED_DEPS_DIR, "node_modules", "puppeteer-core"));
134
+ return PACKAGES.every(isPackageInstalled);
95
135
  }
96
136
 
97
- export { SHARED_DEPS_DIR, PACKAGES };
137
+ export { SHARED_DEPS_DIR, PACKAGES, CORE_PACKAGES, VISUAL_COMPARE_PACKAGES };
@@ -15,7 +15,7 @@
15
15
 
16
16
  import fs from "node:fs";
17
17
  import path from "node:path";
18
- import { ensureSharedDeps, loadFromShared } from "./shared-deps.js";
18
+ import { ensureVisualCompareDeps, loadFromShared } from "./shared-deps.js";
19
19
 
20
20
  /**
21
21
  * 全局视觉对比
@@ -27,8 +27,8 @@ import { ensureSharedDeps, loadFromShared } from "./shared-deps.js";
27
27
  * @returns {Promise<object>} 对比结果
28
28
  */
29
29
  export async function compareDesign(screenshotPath, designImagePath, options = {}) {
30
- // 确保共享依赖已安装,然后加载
31
- await ensureSharedDeps();
30
+ // 视觉对比才安装 pixelmatch/pngjs/sharp,避免 observe 主链路被重依赖阻塞
31
+ await ensureVisualCompareDeps();
32
32
  const sharp = loadFromShared("sharp");
33
33
  const pixelmatch = loadFromShared("pixelmatch");
34
34
  const { PNG } = loadFromShared("pngjs");
@@ -3,10 +3,16 @@
3
3
  * 本地浏览器测试执行器 - CLI 入口
4
4
  *
5
5
  * 用法:
6
+ * node local-browser-executor.js --action observe --output /tmp/observe.json
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
6
11
  * node local-browser-executor.js --plan '<JSON>' --output /tmp/result.json
7
12
  * node local-browser-executor.js --plan-file /path/to/plan.json --output /tmp/result.json
8
13
  *
9
14
  * 参数:
15
+ * --action 直接执行动作,当前支持 observe/act/assert/api-capture
10
16
  * --plan 测试计划 JSON 字符串
11
17
  * --plan-file 测试计划 JSON 文件路径(与 --plan 二选一)
12
18
  * --output 结果输出文件路径(默认 stdout)
@@ -36,6 +42,9 @@ function parseArgs() {
36
42
 
37
43
  for (let i = 0; i < args.length; i++) {
38
44
  switch (args[i]) {
45
+ case "--action":
46
+ parsed.action = args[++i];
47
+ break;
39
48
  case "--plan":
40
49
  parsed.planJson = args[++i];
41
50
  break;
@@ -51,9 +60,117 @@ function parseArgs() {
51
60
  case "--page-url":
52
61
  parsed.pageUrl = args[++i];
53
62
  break;
63
+ case "--url-contains":
64
+ parsed.urlContains = args[++i];
65
+ break;
66
+ case "--frame-url-contains":
67
+ parsed.frameUrlContains = args[++i];
68
+ break;
69
+ case "--frame-content-contains":
70
+ parsed.frameContentContains = args[++i];
71
+ break;
72
+ case "--frame-index":
73
+ parsed.frameIndex = args[++i];
74
+ break;
75
+ case "--selector":
76
+ parsed.selector = args[++i];
77
+ break;
78
+ case "--depth":
79
+ parsed.depth = args[++i];
80
+ break;
81
+ case "--max-interactive-elements":
82
+ parsed.maxInteractiveElements = args[++i];
83
+ break;
84
+ case "--index":
85
+ parsed.index = args[++i];
86
+ break;
87
+ case "--click":
88
+ parsed.click = args[++i];
89
+ break;
90
+ case "--type":
91
+ parsed.typeText = args[++i];
92
+ break;
93
+ case "--clear":
94
+ parsed.clear = true;
95
+ break;
96
+ case "--hover":
97
+ parsed.hover = args[++i];
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;
105
+ case "--observe-after":
106
+ parsed.observeAfter = true;
107
+ break;
108
+ case "--url-contains-assert":
109
+ parsed.urlContainsAssert = args[++i];
110
+ break;
111
+ case "--text-contains":
112
+ parsed.textContains = args[++i];
113
+ break;
114
+ case "--expect-visible":
115
+ parsed.expectVisible = true;
116
+ break;
117
+ case "--min-count":
118
+ parsed.minCount = args[++i];
119
+ break;
120
+ case "--has-text":
121
+ parsed.hasText = args[++i];
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;
54
168
  case "--base-url":
55
169
  parsed.baseUrl = args[++i];
56
170
  break;
171
+ case "--allow-launch":
172
+ parsed.allowLaunch = true;
173
+ break;
57
174
  case "--help":
58
175
  case "-h":
59
176
  printHelp();
@@ -68,17 +185,60 @@ function printHelp() {
68
185
  本地浏览器测试执行器
69
186
 
70
187
  用法:
188
+ node local-browser-executor.js --action observe [options]
189
+ node local-browser-executor.js --action act --index 1 --observe-after [options]
190
+ node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after [options]
191
+ node local-browser-executor.js --action assert --selector ".ant-table" --expect-visible [options]
192
+ node local-browser-executor.js --action api-capture --wait 15000 --include-response-body [options]
193
+ node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0 [options]
71
194
  node local-browser-executor.js --plan '<JSON>' [options]
72
195
  node local-browser-executor.js --plan-file <path> [options]
73
196
 
74
197
  选项:
75
- --plan <json> 测试计划 JSON 字符串
76
- --plan-file <path> 测试计划 JSON 文件路径
77
- --output <path> 结果输出文件路径(默认 stdout)
78
- --cdp-url <url> Chrome DevTools Protocol URL(默认 http://localhost:9222
79
- --page-url <url> 目标页面 URL(connect 模式时匹配标签页)
80
- --base-url <url> 基础 URL,替换计划中的 {{baseUrl}} 占位符
81
- -h, --help 显示帮助
198
+ --action <name> 直接执行动作,当前支持 observe/act/assert/api-capture
199
+ --plan <json> 测试计划 JSON 字符串
200
+ --plan-file <path> 测试计划 JSON 文件路径
201
+ --output <path> 结果输出文件路径(默认 stdout
202
+ --cdp-url <url> Chrome DevTools Protocol URL(默认 http://localhost:9222)
203
+ --page-url <url> 目标页面 URL(connect 模式时匹配标签页)
204
+ --url-contains <text> 按 URL 片段匹配已打开标签页
205
+ --frame-url-contains <text> 按 URL 片段匹配 iframe
206
+ --frame-content-contains <text> 按文本内容匹配 iframe
207
+ --frame-index <n> 按 frame 下标选择 iframe
208
+ --selector <css> observe/assert 根节点或目标元素(observe 默认 body)
209
+ --depth <n> observe 的 DOM 树深度(默认 5)
210
+ --max-interactive-elements <n> observe 的交互元素上限(默认 200)
211
+ --index <n> act 时按 observe 的 interactiveElements[index] 操作
212
+ --click <selector> act 时点击 selector/text=/role=/~ 目标
213
+ --type <text> act 时输入文本
214
+ --clear act 输入前清空目标
215
+ --hover <selector> act 时悬停 selector/text=/role=/~ 目标
216
+ --menu-path <a/b> act 时按菜单路径展开并点击,如 "目录管理/数据集目录"
217
+ --wait-after-each <ms> menu-path 每步点击后的等待时间(默认 300)
218
+ --observe-after act 后再次 observe,返回 before/act/after 闭环结果
219
+ --url-contains-assert <text> assert 当前 URL 包含文本
220
+ --text-contains <text> assert 页面文本包含内容
221
+ --expect-visible assert selector 可见
222
+ --min-count <n> assert selector 数量下限
223
+ --has-text <text> assert selector 文本包含内容
224
+ --wait <ms> api-capture 监听时长(默认 15000)
225
+ --method <GET|POST> api-capture 按请求方法过滤
226
+ --url-pattern <text> api-capture 按 URL 片段过滤
227
+ --target-url-contains <text> api-capture 按 target URL 过滤 iframe/page target
228
+ --include-request-body api-capture 包含请求体(默认包含)
229
+ --no-request-body api-capture 不包含请求体
230
+ --include-response-body api-capture 调用 Network.getResponseBody 获取响应体
231
+ --reload api-capture 开始监听后刷新页面触发请求
232
+ --include-all-targets api-capture 不限制到匹配 frame,监听所有 page/iframe targets
233
+ --capture-file <path> assert-api 读取 api-capture JSON 结果
234
+ --max-failed <n> assert-api 允许失败请求数上限
235
+ --forbid-status-gte <n> assert-api 禁止出现大于等于该状态码的响应
236
+ --required-url <text> assert-api 要求出现的 URL 片段,可重复传入
237
+ --required-method <GET|POST> assert-api 要求出现的请求方法,可重复传入
238
+ --min-total <n> assert-api 要求捕获请求总数下限
239
+ --allow-launch action 模式允许无 CDP 时启动 headless Chrome
240
+ --base-url <url> 基础 URL,替换计划中的 {{baseUrl}} 占位符
241
+ -h, --help 显示帮助
82
242
  `);
83
243
  }
84
244
 
@@ -183,9 +343,163 @@ async function executePlan(plan, options) {
183
343
 
184
344
  // ==================== 主流程 ====================
185
345
 
346
+ function executeAssertApiAction(args) {
347
+ if (!args.captureFile) {
348
+ return {
349
+ error: "必须提供 --capture-file 参数",
350
+ errorCode: "CAPTURE_FILE_REQUIRED",
351
+ };
352
+ }
353
+
354
+ const captureResult = JSON.parse(fs.readFileSync(args.captureFile, "utf8"));
355
+ const engine = new BrowserEngine();
356
+ return engine.assertApi(captureResult, {
357
+ maxFailed: args.maxFailed,
358
+ forbidStatusGte: args.forbidStatusGte,
359
+ requiredUrls: args.requiredUrls,
360
+ requiredMethods: args.requiredMethods,
361
+ minTotal: args.minTotal,
362
+ });
363
+ }
364
+
365
+ async function executeAction(args) {
366
+ const engine = new BrowserEngine();
367
+ const startedAt = new Date().toISOString();
368
+ try {
369
+ const cdpUrl = args.cdpUrl || "http://localhost:9222";
370
+ const debuggerAvailable = await engine.isDebuggerAvailable(cdpUrl);
371
+ if (!debuggerAvailable && !args.allowLaunch) {
372
+ return {
373
+ error: `无法连接到 Chrome 调试端口: ${cdpUrl}`,
374
+ errorCode: "CDP_UNAVAILABLE",
375
+ guide: "请先以 --remote-debugging-port=9222 启动用户真实浏览器;如确需启动 headless 回退,追加 --allow-launch。",
376
+ startTime: startedAt,
377
+ endTime: new Date().toISOString(),
378
+ };
379
+ }
380
+
381
+ const initResult = await engine.init({
382
+ cdpUrl,
383
+ pageUrl: args.pageUrl || args.urlContains,
384
+ });
385
+
386
+ switch (args.action) {
387
+ case "observe": {
388
+ const result = await engine.observe({
389
+ selector: args.selector,
390
+ depth: args.depth,
391
+ frameUrlContains: args.frameUrlContains,
392
+ frameContentContains: args.frameContentContains,
393
+ frameIndex: args.frameIndex,
394
+ maxInteractiveElements: args.maxInteractiveElements,
395
+ });
396
+ return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
397
+ }
398
+ case "act": {
399
+ const actResult = await engine.act({
400
+ index: args.index,
401
+ selector: args.selector,
402
+ click: args.click,
403
+ hover: args.hover,
404
+ menuPath: args.menuPath,
405
+ waitAfterEach: args.waitAfterEach,
406
+ typeText: args.typeText,
407
+ clear: args.clear,
408
+ frameUrlContains: args.frameUrlContains,
409
+ frameContentContains: args.frameContentContains,
410
+ frameIndex: args.frameIndex,
411
+ });
412
+ if (!args.observeAfter) {
413
+ return { type: "act", ...actResult, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
414
+ }
415
+ const after = await engine.observe({
416
+ depth: args.depth,
417
+ frameUrlContains: args.frameUrlContains,
418
+ frameContentContains: args.frameContentContains,
419
+ frameIndex: args.frameIndex,
420
+ maxInteractiveElements: args.maxInteractiveElements,
421
+ });
422
+ return { type: "actObserve", act: actResult, after, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
423
+ }
424
+ case "assert": {
425
+ const expect = {};
426
+ if (args.expectVisible) expect.visible = true;
427
+ if (args.minCount !== undefined) expect.minCount = Number.parseInt(args.minCount, 10);
428
+ if (args.hasText !== undefined) expect.hasText = args.hasText;
429
+ const result = await engine.assert({
430
+ selector: args.selector,
431
+ expect,
432
+ urlContains: args.urlContainsAssert,
433
+ textContains: args.textContains,
434
+ frameUrlContains: args.frameUrlContains,
435
+ frameContentContains: args.frameContentContains,
436
+ frameIndex: args.frameIndex,
437
+ });
438
+ return { type: "assert", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
439
+ }
440
+ case "api-capture":
441
+ case "apiCapture": {
442
+ const result = await engine.captureApi({
443
+ wait: args.wait,
444
+ method: args.method,
445
+ urlPattern: args.urlPattern,
446
+ targetUrlContains: args.targetUrlContains,
447
+ includeRequestBody: args.includeRequestBody,
448
+ includeResponseBody: args.includeResponseBody,
449
+ reload: args.reload,
450
+ includeAllTargets: args.includeAllTargets,
451
+ frameUrlContains: args.frameUrlContains,
452
+ frameContentContains: args.frameContentContains,
453
+ frameIndex: args.frameIndex,
454
+ });
455
+ return { ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
456
+ }
457
+ default:
458
+ return {
459
+ error: `未知 action: ${args.action}`,
460
+ errorCode: "UNKNOWN_ACTION",
461
+ supportedActions: ["observe", "act", "assert", "api-capture"],
462
+ startTime: startedAt,
463
+ endTime: new Date().toISOString(),
464
+ };
465
+ }
466
+ } catch (err) {
467
+ return {
468
+ error: err.message,
469
+ errorCode: err.code || "ACTION_FAILED",
470
+ installGuide: err.code === "CHROME_NOT_FOUND" || err.code === "DEPENDENCY_MISSING" ? err.message : undefined,
471
+ startTime: startedAt,
472
+ endTime: new Date().toISOString(),
473
+ };
474
+ } finally {
475
+ await engine.close();
476
+ }
477
+ }
478
+
186
479
  async function main() {
187
480
  const args = parseArgs();
188
481
 
482
+ if (args.action === "assert-api" || args.action === "assertApi") {
483
+ try {
484
+ const results = executeAssertApiAction(args);
485
+ outputResult(results, args.output);
486
+ process.exit(results.error ? 2 : results.passed === false ? 1 : 0);
487
+ } catch (err) {
488
+ const errorResult = {
489
+ error: err.message,
490
+ errorCode: "ASSERT_API_FAILED",
491
+ };
492
+ outputResult(errorResult, args.output);
493
+ process.exit(2);
494
+ }
495
+ }
496
+
497
+ if (args.action) {
498
+ const results = await executeAction(args);
499
+ outputResult(results, args.output);
500
+ process.exit(results.error ? 2 : 0);
501
+ }
502
+
189
503
  let plan;
190
504
  try {
191
505
  plan = loadPlan(args);
@@ -53,5 +53,5 @@ argument-hint: <目标页面URL> [可选:设计稿图片路径]
53
53
 
54
54
  **注意事项**:
55
55
  - 首次使用需确保 Chrome 以调试模式启动:`google-chrome --remote-debugging-port=9222`
56
- - 如执行器报错依赖缺失,引导用户安装:`npm install puppeteer-core pixelmatch pngjs sharp`
56
+ - 如执行器报错依赖缺失,优先按提示安装核心依赖;仅设计稿像素对比需要额外安装 `pixelmatch pngjs sharp`
57
57
  - connect 模式下天然继承用户浏览器的登录态和微前端环境