weixin-devtools-mcp 0.0.1 → 0.1.1

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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -128
  3. package/build/MiniProgramContext.d.ts +317 -0
  4. package/build/MiniProgramContext.d.ts.map +1 -0
  5. package/build/MiniProgramContext.js +696 -0
  6. package/build/MiniProgramContext.js.map +1 -0
  7. package/build/collectors/Collector.d.ts +127 -0
  8. package/build/collectors/Collector.d.ts.map +1 -0
  9. package/build/collectors/Collector.js +252 -0
  10. package/build/collectors/Collector.js.map +1 -0
  11. package/build/collectors/ConsoleCollector.d.ts +104 -0
  12. package/build/collectors/ConsoleCollector.d.ts.map +1 -0
  13. package/build/collectors/ConsoleCollector.js +157 -0
  14. package/build/collectors/ConsoleCollector.js.map +1 -0
  15. package/build/collectors/NetworkCollector.d.ts +167 -0
  16. package/build/collectors/NetworkCollector.d.ts.map +1 -0
  17. package/build/collectors/NetworkCollector.js +265 -0
  18. package/build/collectors/NetworkCollector.js.map +1 -0
  19. package/build/collectors/index.d.ts +13 -0
  20. package/build/collectors/index.d.ts.map +1 -0
  21. package/build/collectors/index.js +17 -0
  22. package/build/collectors/index.js.map +1 -0
  23. package/build/config/tool-profile.d.ts +30 -0
  24. package/build/config/tool-profile.d.ts.map +1 -0
  25. package/build/config/tool-profile.js +138 -0
  26. package/build/config/tool-profile.js.map +1 -0
  27. package/build/connection/adapters.d.ts +3 -0
  28. package/build/connection/adapters.d.ts.map +1 -0
  29. package/build/connection/adapters.js +134 -0
  30. package/build/connection/adapters.js.map +1 -0
  31. package/build/connection/errors.d.ts +34 -0
  32. package/build/connection/errors.d.ts.map +1 -0
  33. package/build/connection/errors.js +101 -0
  34. package/build/connection/errors.js.map +1 -0
  35. package/build/connection/health-probe.d.ts +4 -0
  36. package/build/connection/health-probe.d.ts.map +1 -0
  37. package/build/connection/health-probe.js +60 -0
  38. package/build/connection/health-probe.js.map +1 -0
  39. package/build/connection/index.d.ts +6 -0
  40. package/build/connection/index.d.ts.map +1 -0
  41. package/build/connection/index.js +6 -0
  42. package/build/connection/index.js.map +1 -0
  43. package/build/connection/manager.d.ts +19 -0
  44. package/build/connection/manager.d.ts.map +1 -0
  45. package/build/connection/manager.js +227 -0
  46. package/build/connection/manager.js.map +1 -0
  47. package/build/connection/resolver.d.ts +3 -0
  48. package/build/connection/resolver.d.ts.map +1 -0
  49. package/build/connection/resolver.js +99 -0
  50. package/build/connection/resolver.js.map +1 -0
  51. package/build/connection/types.d.ts +95 -0
  52. package/build/connection/types.d.ts.map +1 -0
  53. package/build/connection/types.js +16 -0
  54. package/build/connection/types.js.map +1 -0
  55. package/build/core/assertion.d.ts +22 -0
  56. package/build/core/assertion.d.ts.map +1 -0
  57. package/build/core/assertion.js +318 -0
  58. package/build/core/assertion.js.map +1 -0
  59. package/build/core/connection.d.ts +37 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +745 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/index.d.ts +13 -0
  64. package/build/core/index.d.ts.map +1 -0
  65. package/build/core/index.js +19 -0
  66. package/build/core/index.js.map +1 -0
  67. package/build/core/interaction.d.ts +22 -0
  68. package/build/core/interaction.d.ts.map +1 -0
  69. package/build/core/interaction.js +185 -0
  70. package/build/core/interaction.js.map +1 -0
  71. package/build/core/navigation.d.ts +26 -0
  72. package/build/core/navigation.d.ts.map +1 -0
  73. package/build/core/navigation.js +210 -0
  74. package/build/core/navigation.js.map +1 -0
  75. package/build/core/query.d.ts +14 -0
  76. package/build/core/query.d.ts.map +1 -0
  77. package/build/core/query.js +191 -0
  78. package/build/core/query.js.map +1 -0
  79. package/build/core/screenshot.d.ts +10 -0
  80. package/build/core/screenshot.d.ts.map +1 -0
  81. package/build/core/screenshot.js +93 -0
  82. package/build/core/screenshot.js.map +1 -0
  83. package/build/core/snapshot.d.ts +17 -0
  84. package/build/core/snapshot.d.ts.map +1 -0
  85. package/build/core/snapshot.js +225 -0
  86. package/build/core/snapshot.js.map +1 -0
  87. package/build/core/types.d.ts +250 -0
  88. package/build/core/types.d.ts.map +1 -0
  89. package/build/core/types.js +6 -0
  90. package/build/core/types.js.map +1 -0
  91. package/build/formatters/consoleFormatter.d.ts +50 -0
  92. package/build/formatters/consoleFormatter.d.ts.map +1 -0
  93. package/build/formatters/consoleFormatter.js +116 -0
  94. package/build/formatters/consoleFormatter.js.map +1 -0
  95. package/build/formatters/snapshotFormatter.d.ts +41 -0
  96. package/build/formatters/snapshotFormatter.d.ts.map +1 -0
  97. package/build/formatters/snapshotFormatter.js +156 -0
  98. package/build/formatters/snapshotFormatter.js.map +1 -0
  99. package/build/index.d.ts +11 -0
  100. package/build/index.d.ts.map +1 -0
  101. package/build/index.js +45 -9
  102. package/build/index.js.map +1 -0
  103. package/build/server.d.ts +7 -0
  104. package/build/server.d.ts.map +1 -0
  105. package/build/server.js +88 -32
  106. package/build/server.js.map +1 -0
  107. package/build/tools/ToolDefinition.d.ts +265 -0
  108. package/build/tools/ToolDefinition.d.ts.map +1 -0
  109. package/build/tools/ToolDefinition.js +16 -7
  110. package/build/tools/ToolDefinition.js.map +1 -0
  111. package/build/tools/assert.d.ts +17 -0
  112. package/build/tools/assert.d.ts.map +1 -0
  113. package/build/tools/assert.js +63 -103
  114. package/build/tools/assert.js.map +1 -0
  115. package/build/tools/connection.d.ts +13 -0
  116. package/build/tools/connection.d.ts.map +1 -0
  117. package/build/tools/connection.js +338 -611
  118. package/build/tools/connection.js.map +1 -0
  119. package/build/tools/console.d.ts +20 -0
  120. package/build/tools/console.d.ts.map +1 -0
  121. package/build/tools/console.js +162 -152
  122. package/build/tools/console.js.map +1 -0
  123. package/build/tools/diagnose.d.ts +22 -0
  124. package/build/tools/diagnose.d.ts.map +1 -0
  125. package/build/tools/diagnose.js +406 -13
  126. package/build/tools/diagnose.js.map +1 -0
  127. package/build/tools/index.d.ts +6 -0
  128. package/build/tools/index.d.ts.map +1 -0
  129. package/build/tools/index.js +3 -77
  130. package/build/tools/index.js.map +1 -0
  131. package/build/tools/input.d.ts +21 -0
  132. package/build/tools/input.d.ts.map +1 -0
  133. package/build/tools/input.js +73 -139
  134. package/build/tools/input.js.map +1 -0
  135. package/build/tools/navigate.d.ts +21 -0
  136. package/build/tools/navigate.d.ts.map +1 -0
  137. package/build/tools/navigate.js +63 -126
  138. package/build/tools/navigate.js.map +1 -0
  139. package/build/tools/network.d.ts +21 -0
  140. package/build/tools/network.d.ts.map +1 -0
  141. package/build/tools/network.js +214 -1044
  142. package/build/tools/network.js.map +1 -0
  143. package/build/tools/page.d.ts +13 -0
  144. package/build/tools/page.d.ts.map +1 -0
  145. package/build/tools/page.js +6 -3
  146. package/build/tools/page.js.map +1 -0
  147. package/build/tools/screenshot.d.ts +9 -0
  148. package/build/tools/screenshot.d.ts.map +1 -0
  149. package/build/tools/screenshot.js +3 -1
  150. package/build/tools/screenshot.js.map +1 -0
  151. package/build/tools/script.d.ts +6 -0
  152. package/build/tools/script.d.ts.map +1 -0
  153. package/build/tools/script.js +92 -0
  154. package/build/tools/script.js.map +1 -0
  155. package/build/tools/snapshot.d.ts +9 -0
  156. package/build/tools/snapshot.d.ts.map +1 -0
  157. package/build/tools/snapshot.js +78 -12
  158. package/build/tools/snapshot.js.map +1 -0
  159. package/build/tools/tools.d.ts +15 -0
  160. package/build/tools/tools.d.ts.map +1 -0
  161. package/build/tools/tools.js +63 -0
  162. package/build/tools/tools.js.map +1 -0
  163. package/build/tools.d.ts +431 -0
  164. package/build/tools.d.ts.map +1 -0
  165. package/build/tools.js +258 -118
  166. package/build/tools.js.map +1 -0
  167. package/build/types/errors.d.ts +189 -0
  168. package/build/types/errors.d.ts.map +1 -0
  169. package/build/types/errors.js +257 -0
  170. package/build/types/errors.js.map +1 -0
  171. package/build/utils/error.d.ts +6 -0
  172. package/build/utils/error.d.ts.map +1 -0
  173. package/build/utils/error.js +11 -0
  174. package/build/utils/error.js.map +1 -0
  175. package/build/utils/idGenerator.d.ts +21 -0
  176. package/build/utils/idGenerator.d.ts.map +1 -0
  177. package/build/utils/idGenerator.js +23 -0
  178. package/build/utils/idGenerator.js.map +1 -0
  179. package/build/version.d.ts +7 -0
  180. package/build/version.d.ts.map +1 -0
  181. package/build/version.js +10 -0
  182. package/build/version.js.map +1 -0
  183. package/package.json +31 -9
package/build/tools.js CHANGED
@@ -2,21 +2,40 @@
2
2
  * 微信开发者工具 MCP 工具函数
3
3
  * 提供可测试的纯函数实现
4
4
  */
5
- import automator from "miniprogram-automator";
6
- import path from "path";
7
- import fs from "fs";
8
5
  import { spawn } from "child_process";
6
+ import fs from "fs";
7
+ import path from "path";
9
8
  import { promisify } from "util";
9
+ import automator from "miniprogram-automator";
10
10
  const sleep = promisify(setTimeout);
11
11
  /**
12
12
  * 开发者工具连接错误类
13
13
  */
14
- export class DevToolsConnectionError extends Error {
14
+ import { DevToolsError, ErrorCode, ErrorCategory } from './types/errors.js';
15
+ /**
16
+ * @deprecated 使用 DevToolsError 替代
17
+ * 保留此类以保持向后兼容性
18
+ */
19
+ export class DevToolsConnectionError extends DevToolsError {
15
20
  phase;
16
21
  originalError;
17
22
  details;
18
23
  constructor(message, phase, originalError, details) {
19
- super(message);
24
+ // 根据阶段选择错误代码
25
+ const code = phase === 'startup'
26
+ ? ErrorCode.CONNECTION_FAILED
27
+ : phase === 'connection'
28
+ ? ErrorCode.CONNECTION_FAILED
29
+ : ErrorCode.CONNECTION_TIMEOUT;
30
+ const context = {
31
+ operation: phase,
32
+ details,
33
+ cause: originalError,
34
+ };
35
+ super(message, code, {
36
+ category: ErrorCategory.CONNECTION,
37
+ context,
38
+ });
20
39
  this.phase = phase;
21
40
  this.originalError = originalError;
22
41
  this.details = details;
@@ -31,7 +50,7 @@ export class DevToolsConnectionError extends Error {
31
50
  * @throws 连接失败时抛出错误
32
51
  */
33
52
  export async function connectDevtools(options) {
34
- const { projectPath, cliPath, port } = options;
53
+ const { projectPath, cliPath, port, autoAudits } = options;
35
54
  if (!projectPath) {
36
55
  throw new Error("项目路径是必需的");
37
56
  }
@@ -57,6 +76,15 @@ export async function connectDevtools(options) {
57
76
  launchOptions.cliPath = cliPath;
58
77
  if (port)
59
78
  launchOptions.port = port;
79
+ if (typeof autoAudits === 'boolean') {
80
+ launchOptions.projectConfig = {
81
+ ...(launchOptions.projectConfig || {}),
82
+ setting: {
83
+ ...(launchOptions.projectConfig?.setting || {}),
84
+ autoAudits
85
+ }
86
+ };
87
+ }
60
88
  // 启动并连接微信开发者工具
61
89
  const miniProgram = await automator.launch(launchOptions);
62
90
  // 获取当前页面
@@ -218,12 +246,12 @@ export async function connectDevtools(options) {
218
246
  hasInterceptors: !!(app && app.$xfetch && app.$xfetch.interceptors),
219
247
  hasMpxFetch: hasMpxFetch
220
248
  };
221
- console.log('[MCP-DEBUG] Mpx检测:', debugInfo);
249
+ console.error('[MCP-DEBUG] Mpx检测:', debugInfo);
222
250
  // 强制安装 Mpx 拦截器(不检查标志,每次都重新安装以覆盖旧的)
223
251
  // 这样可以解决小程序未重新加载导致标志残留的问题
224
252
  // @ts-ignore
225
253
  if (hasMpxFetch) {
226
- console.log('[MCP] 正在安装 Mpx $xfetch 拦截器(强制覆盖)...');
254
+ console.error('[MCP] 正在安装 Mpx $xfetch 拦截器(强制覆盖)...');
227
255
  // 安装 Mpx 请求拦截器
228
256
  // @ts-ignore
229
257
  app.$xfetch.interceptors.request.use(function (config) {
@@ -282,12 +310,12 @@ export async function connectDevtools(options) {
282
310
  });
283
311
  throw error;
284
312
  });
285
- console.log('[MCP] Mpx $xfetch 拦截器安装完成');
313
+ console.error('[MCP] Mpx $xfetch 拦截器安装完成');
286
314
  }
287
315
  // @ts-ignore
288
316
  wx.__networkInterceptorsInstalled = true;
289
317
  });
290
- console.log('[connectDevtools] 网络监听已自动启动(包含 Mpx 框架支持)');
318
+ console.error('[connectDevtools] 网络监听已自动启动(包含 Mpx 框架支持)');
291
319
  }
292
320
  catch (err) {
293
321
  console.warn('[connectDevtools] 网络监听启动失败:', err);
@@ -311,7 +339,7 @@ export async function connectDevtools(options) {
311
339
  * @returns 详细连接结果
312
340
  */
313
341
  export async function connectDevtoolsEnhanced(options) {
314
- const { mode = 'auto', fallbackMode = true, healthCheck = true, verbose = false } = options;
342
+ const { mode = 'auto', verbose = false } = options;
315
343
  const startTime = Date.now();
316
344
  // 验证项目路径(在所有模式执行前统一验证)
317
345
  if (!options.projectPath) {
@@ -330,8 +358,8 @@ export async function connectDevtoolsEnhanced(options) {
330
358
  throw new Error(`Project path '${resolvedProjectPath}' doesn't exist`);
331
359
  }
332
360
  if (verbose) {
333
- console.log(`开始连接微信开发者工具,模式: ${mode}`);
334
- console.log(`项目路径: ${resolvedProjectPath}`);
361
+ console.error(`开始连接微信开发者工具,模式: ${mode}`);
362
+ console.error(`项目路径: ${resolvedProjectPath}`);
335
363
  }
336
364
  try {
337
365
  switch (mode) {
@@ -377,7 +405,7 @@ function isSessionConflictError(error) {
377
405
  */
378
406
  async function intelligentConnect(options, startTime) {
379
407
  if (options.verbose) {
380
- console.log('🎯 智能连接策略: 优先使用 launchMode(自动处理项目验证和会话复用)');
408
+ console.error('🎯 智能连接策略: 优先使用 launchMode(自动处理项目验证和会话复用)');
381
409
  }
382
410
  try {
383
411
  // 默认使用 launchMode
@@ -389,12 +417,12 @@ async function intelligentConnect(options, startTime) {
389
417
  }
390
418
  catch (error) {
391
419
  if (options.verbose) {
392
- console.log('⚠️ launchMode 失败,分析错误类型...');
420
+ console.error('⚠️ launchMode 失败,分析错误类型...');
393
421
  }
394
422
  // 仅在特定可恢复错误时回退到 connectMode
395
423
  if (options.fallbackMode && isSessionConflictError(error)) {
396
424
  if (options.verbose) {
397
- console.log('🔄 检测到会话冲突,尝试回退到 connectMode');
425
+ console.error('🔄 检测到会话冲突,尝试回退到 connectMode');
398
426
  }
399
427
  return await connectMode(options, startTime);
400
428
  }
@@ -430,7 +458,7 @@ async function connectMode(options, startTime) {
430
458
  error.phase === 'startup' &&
431
459
  error.details?.reason === 'session_conflict') {
432
460
  if (options.verbose) {
433
- console.log('🔄 检测到会话冲突,自动回退到传统连接模式(launch)...');
461
+ console.error('🔄 检测到会话冲突,自动回退到传统连接模式(launch)...');
434
462
  }
435
463
  // 如果允许回退,自动使用launch模式
436
464
  if (options.fallbackMode) {
@@ -448,7 +476,8 @@ async function launchMode(options, startTime) {
448
476
  const connectOptions = {
449
477
  projectPath: options.projectPath,
450
478
  cliPath: options.cliPath,
451
- port: options.autoPort || options.port
479
+ port: options.autoPort || options.port,
480
+ autoAudits: options.autoAudits
452
481
  };
453
482
  const result = await connectDevtools(connectOptions);
454
483
  // 健康检查
@@ -470,7 +499,7 @@ async function startupPhase(options) {
470
499
  const port = options.autoPort || 9420;
471
500
  const cliCommand = buildCliCommand(options);
472
501
  if (options.verbose) {
473
- console.log('执行CLI命令:', cliCommand.join(' '));
502
+ console.error('执行CLI命令:', cliCommand.join(' '));
474
503
  }
475
504
  // 执行CLI命令
476
505
  const process = await executeCliCommand(cliCommand);
@@ -490,7 +519,7 @@ async function startupPhase(options) {
490
519
  async function connectionPhase(options, startupResult) {
491
520
  const wsEndpoint = `ws://localhost:${startupResult.processInfo.port}`;
492
521
  if (options.verbose) {
493
- console.log('连接WebSocket端点:', wsEndpoint);
522
+ console.error('连接WebSocket端点:', wsEndpoint);
494
523
  }
495
524
  // 连接到WebSocket端点
496
525
  const miniProgram = await connectWithRetry(wsEndpoint, 3);
@@ -530,8 +559,19 @@ function buildCliCommand(options) {
530
559
  }
531
560
  /**
532
561
  * 查找默认CLI路径
562
+ * 优先级:环境变量 > 默认路径
533
563
  */
534
564
  function findDefaultCliPath() {
565
+ // 1. 优先使用环境变量
566
+ const envCliPath = process.env.WECHAT_DEVTOOLS_CLI;
567
+ if (envCliPath) {
568
+ if (envCliPath.startsWith('@playground/')) {
569
+ // @playground/ 格式需要转换为实际路径
570
+ const relativePath = envCliPath.replace('@playground/', 'playground/');
571
+ return path.resolve(process.cwd(), relativePath);
572
+ }
573
+ return path.resolve(process.cwd(), envCliPath);
574
+ }
535
575
  const platform = process.platform;
536
576
  if (platform === 'darwin') {
537
577
  return '/Applications/wechatwebdevtools.app/Contents/MacOS/cli';
@@ -540,7 +580,18 @@ function findDefaultCliPath() {
540
580
  return 'C:/Program Files (x86)/Tencent/微信web开发者工具/cli.bat';
541
581
  }
542
582
  else {
543
- throw new Error(`不支持的平台: ${platform}`);
583
+ // Linux: 尝试常见的 Linux 版开发者工具路径
584
+ const linuxPaths = [
585
+ '/opt/apps/io.github.msojocs.wechat-devtools-linux/files/bin/bin/wechat-devtools-cli',
586
+ '/usr/share/wechat-devtools/bin/cli',
587
+ '/usr/local/bin/wechat-devtools-cli',
588
+ ];
589
+ for (const p of linuxPaths) {
590
+ if (fs.existsSync(p)) {
591
+ return p;
592
+ }
593
+ }
594
+ throw new Error(`不支持的平台: ${platform}。可在 Linux 上设置环境变量 WECHAT_DEVTOOLS_CLI 指定 CLI 路径`);
544
595
  }
545
596
  }
546
597
  /**
@@ -572,14 +623,14 @@ async function executeCliCommand(command) {
572
623
  process.stdout.on('data', (data) => {
573
624
  const text = data.toString();
574
625
  output += text;
575
- console.log('[CLI stdout]:', text.trim());
626
+ console.error('[CLI stdout]:', text.trim());
576
627
  });
577
628
  }
578
629
  if (process.stderr) {
579
630
  process.stderr.on('data', (data) => {
580
631
  const text = data.toString();
581
632
  errorOutput += text;
582
- console.log('[CLI stderr]:', text.trim());
633
+ console.error('[CLI stderr]:', text.trim());
583
634
  // 检测端口冲突错误
584
635
  if (text.includes('must be restarted on port')) {
585
636
  const match = text.match(/started on .+:(\d+) and must be restarted on port (\d+)/);
@@ -619,7 +670,7 @@ async function executeCliCommand(command) {
619
670
  // 检测 CLI 命令失败(通用)
620
671
  if (text.includes('error') || text.includes('failed') || text.includes('失败')) {
621
672
  if (!resolved && text.length > 10) { // 确保不是误报
622
- console.log('[CLI 警告] 检测到潜在错误:', text.trim());
673
+ console.error('[CLI 警告] 检测到潜在错误:', text.trim());
623
674
  }
624
675
  }
625
676
  });
@@ -661,22 +712,21 @@ async function executeCliCommand(command) {
661
712
  export async function waitForWebSocketReady(port, timeout, verbose = false) {
662
713
  const startTime = Date.now();
663
714
  let attempt = 0;
664
- const maxAttempts = Math.ceil(timeout / 1000); // 每秒检查一次
665
715
  if (verbose) {
666
- console.log(`等待WebSocket服务启动,端口: ${port},超时: ${timeout}ms`);
716
+ console.error(`等待WebSocket服务启动,端口: ${port},超时: ${timeout}ms`);
667
717
  }
668
718
  while (Date.now() - startTime < timeout) {
669
719
  attempt++;
670
720
  if (verbose && attempt % 5 === 0) { // 每5秒显示一次进度
671
721
  const elapsed = Date.now() - startTime;
672
- console.log(`WebSocket检测进度: ${Math.round(elapsed / 1000)}s / ${Math.round(timeout / 1000)}s`);
722
+ console.error(`WebSocket检测进度: ${Math.round(elapsed / 1000)}s / ${Math.round(timeout / 1000)}s`);
673
723
  }
674
724
  // 尝试多种检测方式
675
725
  const isReady = await checkDevToolsRunning(port) || await checkWebSocketDirectly(port);
676
726
  if (isReady) {
677
727
  if (verbose) {
678
728
  const elapsed = Date.now() - startTime;
679
- console.log(`WebSocket服务已启动,耗时: ${elapsed}ms`);
729
+ console.error(`WebSocket服务已启动,耗时: ${elapsed}ms`);
680
730
  }
681
731
  return;
682
732
  }
@@ -737,16 +787,16 @@ export async function detectIDEPort(verbose = false) {
737
787
  // 常用端口列表
738
788
  const commonPorts = [9420, 9440, 9430, 9450, 9460];
739
789
  if (verbose) {
740
- console.log('🔍 检测微信开发者工具运行端口...');
790
+ console.error('🔍 检测微信开发者工具运行端口...');
741
791
  }
742
792
  // 策略1: 尝试常用端口
743
793
  for (const port of commonPorts) {
744
794
  if (verbose) {
745
- console.log(` 检测端口 ${port}...`);
795
+ console.error(` 检测端口 ${port}...`);
746
796
  }
747
797
  if (await checkDevToolsRunning(port)) {
748
798
  if (verbose) {
749
- console.log(`✅ 检测到IDE运行在端口 ${port}`);
799
+ console.error(`✅ 检测到IDE运行在端口 ${port}`);
750
800
  }
751
801
  return port;
752
802
  }
@@ -760,14 +810,14 @@ export async function detectIDEPort(verbose = false) {
760
810
  if (output) {
761
811
  const ports = output.split('\n').map((p) => parseInt(p, 10)).filter((p) => !isNaN(p));
762
812
  if (verbose && ports.length > 0) {
763
- console.log(` lsof检测到端口: ${ports.join(', ')}`);
813
+ console.error(` lsof检测到端口: ${ports.join(', ')}`);
764
814
  }
765
815
  // 遍历检测到的端口,验证是否为有效的自动化端口
766
816
  for (const port of ports) {
767
817
  if (port >= 9400 && port <= 9500) {
768
818
  if (await checkDevToolsRunning(port)) {
769
819
  if (verbose) {
770
- console.log(`✅ 通过lsof检测到IDE运行在端口 ${port}`);
820
+ console.error(`✅ 通过lsof检测到IDE运行在端口 ${port}`);
771
821
  }
772
822
  return port;
773
823
  }
@@ -778,12 +828,12 @@ export async function detectIDEPort(verbose = false) {
778
828
  catch (error) {
779
829
  // lsof 失败,继续
780
830
  if (verbose) {
781
- console.log(' lsof检测失败');
831
+ console.error(' lsof检测失败');
782
832
  }
783
833
  }
784
834
  }
785
835
  if (verbose) {
786
- console.log('❌ 未检测到IDE运行端口');
836
+ console.error('❌ 未检测到IDE运行端口');
787
837
  }
788
838
  return null;
789
839
  }
@@ -838,30 +888,69 @@ async function executeWithDetailedError(operation, phase) {
838
888
  throw new DevToolsConnectionError(originalError.message, phase, originalError, { timestamp: new Date().toISOString() });
839
889
  }
840
890
  }
891
+ /**
892
+ * 生成简单的文本哈希(用于增强 UID 唯一性)
893
+ */
894
+ function simpleTextHash(text) {
895
+ if (!text || text.length === 0)
896
+ return '';
897
+ // 取文本的前 8 个字符,过滤特殊字符
898
+ const sanitized = text.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '').slice(0, 8);
899
+ if (sanitized.length === 0)
900
+ return '';
901
+ return `_${sanitized}`;
902
+ }
841
903
  /**
842
904
  * 生成元素的唯一标识符 (uid)
905
+ *
906
+ * 优先级顺序(稳定性从高到低):
907
+ * 1. data-testid(专门用于测试,最稳定)
908
+ * 2. id 属性
909
+ * 3. data-id(自定义数据属性)
910
+ * 4. class + 文本哈希(中等稳定性)
911
+ * 5. class:eq(index)
912
+ * 6. nth-child(兜底)
843
913
  */
844
914
  export async function generateElementUid(element, index) {
845
915
  try {
846
916
  const tagName = element.tagName;
847
- const className = await element.attribute('class').catch(() => '');
848
- const id = await element.attribute('id').catch(() => '');
849
- console.log(`[generateElementUid] tagName=${tagName}, className="${className}", id="${id}", index=${index}`);
917
+ // 并行获取所有可能的标识属性
918
+ const [className, id, testId, dataId, text] = await Promise.all([
919
+ element.attribute('class').catch(() => ''),
920
+ element.attribute('id').catch(() => ''),
921
+ element.attribute('data-testid').catch(() => ''),
922
+ element.attribute('data-id').catch(() => ''),
923
+ element.text().catch(() => '')
924
+ ]);
925
+ console.error(`[generateElementUid] tagName=${tagName}, id="${id}", testId="${testId}", dataId="${dataId}", className="${className}", index=${index}`);
850
926
  let selector = tagName;
851
- if (id) {
927
+ // 优先级1: data-testid(最稳定)
928
+ if (testId) {
929
+ selector += `[data-testid="${testId}"]`;
930
+ }
931
+ // 优先级2: id 属性
932
+ else if (id) {
852
933
  selector += `#${id}`;
853
934
  }
935
+ // 优先级3: data-id
936
+ else if (dataId) {
937
+ selector += `[data-id="${dataId}"]`;
938
+ }
939
+ // 优先级4: class + 文本哈希
854
940
  else if (className) {
855
- selector += `.${className.split(' ')[0]}`;
941
+ const firstClass = className.split(' ')[0];
942
+ const textHash = simpleTextHash(text);
943
+ selector += `.${firstClass}${textHash}`;
856
944
  }
945
+ // 优先级5: nth-child(兜底)
857
946
  else {
858
947
  selector += `:nth-child(${index + 1})`;
859
948
  }
860
- console.log(`[generateElementUid] Generated UID: ${selector}`);
949
+ console.error(`[generateElementUid] Generated UID: ${selector}`);
861
950
  return selector;
862
951
  }
863
952
  catch (error) {
864
- console.log(`[generateElementUid] Error:`, error);
953
+ console.error(`[generateElementUid] Error:`, error);
865
954
  return `${element.tagName || 'unknown'}:nth-child(${index + 1})`;
866
955
  }
867
956
  }
@@ -882,16 +971,21 @@ export async function getPageSnapshot(page) {
882
971
  await new Promise(resolve => setTimeout(resolve, 1000));
883
972
  // 尝试多种选择器策略获取元素
884
973
  let childElements = [];
885
- // 策略1: 尝试获取所有元素
974
+ let usedStrategy = 'unknown';
975
+ // 策略1: 优先使用通配符(最快,一次API调用)
886
976
  try {
887
977
  childElements = await page.$$('*');
888
- console.log(`策略1 (*) 获取到 ${childElements.length} 个元素`);
978
+ if (childElements.length > 0) {
979
+ usedStrategy = 'wildcard(*)';
980
+ console.error(`✅ 策略1成功: 通配符查询获取到 ${childElements.length} 个元素`);
981
+ }
889
982
  }
890
983
  catch (error) {
891
- console.log('策略1 (*) 失败:', error);
984
+ console.warn('⚠️ 策略1失败 (*)', error);
892
985
  }
893
- // 策略2: 如果策略1失败,尝试小程序常用组件
986
+ // 策略2: 降级到常用组件选择器(仅当策略1失败时)
894
987
  if (childElements.length === 0) {
988
+ console.error('🔄 策略1无结果,降级到策略2(常用组件选择器)');
895
989
  const commonSelectors = [
896
990
  'view', 'text', 'button', 'image', 'input', 'textarea', 'picker', 'switch',
897
991
  'slider', 'scroll-view', 'swiper', 'icon', 'rich-text', 'progress',
@@ -901,51 +995,108 @@ export async function getPageSnapshot(page) {
901
995
  try {
902
996
  const elements = await page.$$(selector);
903
997
  childElements.push(...elements);
904
- console.log(`策略2 (${selector}) 获取到 ${elements.length} 个元素`);
998
+ if (elements.length > 0) {
999
+ console.error(` - ${selector}: ${elements.length} 个元素`);
1000
+ }
905
1001
  }
906
1002
  catch (error) {
907
- console.log(`策略2 (${selector}) 失败:`, error);
1003
+ // 忽略单个选择器失败
908
1004
  }
909
1005
  }
1006
+ if (childElements.length > 0) {
1007
+ usedStrategy = 'common-selectors';
1008
+ console.error(`✅ 策略2成功: 获取到 ${childElements.length} 个元素`);
1009
+ }
910
1010
  }
911
- // 策略3: 如果还是没有元素,尝试根据层级查找
1011
+ // 策略3: 最后尝试层级选择器
912
1012
  if (childElements.length === 0) {
1013
+ console.error('🔄 策略2无结果,降级到策略3(层级选择器)');
913
1014
  try {
914
1015
  const rootElements = await page.$$('page > *');
915
1016
  childElements = rootElements;
916
- console.log(`策略3 (page > *) 获取到 ${childElements.length} 个元素`);
1017
+ if (childElements.length > 0) {
1018
+ usedStrategy = 'hierarchical(page>*)';
1019
+ console.error(`✅ 策略3成功: 获取到 ${childElements.length} 个元素`);
1020
+ }
917
1021
  }
918
1022
  catch (error) {
919
- console.log('策略3 (page > *) 失败:', error);
1023
+ console.warn('⚠️ 策略3失败 (page > *)', error);
920
1024
  }
921
1025
  }
922
- console.log(`最终获取到 ${childElements.length} 个元素`);
1026
+ if (childElements.length === 0) {
1027
+ console.warn('❌ 所有策略均未获取到元素');
1028
+ return {
1029
+ snapshot: { path: await page.path, elements: [] },
1030
+ elementMap: new Map()
1031
+ };
1032
+ }
1033
+ console.error(`📊 最终获取到 ${childElements.length} 个元素(策略:${usedStrategy})`);
923
1034
  // 用于跟踪每个基础选择器的元素计数
924
1035
  const selectorIndexMap = new Map();
1036
+ // 优化:批量并行处理元素属性
1037
+ const startTime = Date.now();
925
1038
  for (let i = 0; i < childElements.length; i++) {
926
1039
  const element = childElements[i];
927
1040
  try {
928
- const uid = await generateElementUid(element, i);
1041
+ // 🚀 优化点1: 使用 Promise.allSettled 并行获取所有元素属性
1042
+ // 减少API调用往返次数:从 6次串行 → 1次并行
1043
+ // 新增 data-testid 和 data-id 属性用于增强 UID 稳定性
1044
+ const [tagNameResult, textResult, classResult, idResult, testIdResult, dataIdResult, sizeResult, offsetResult] = await Promise.allSettled([
1045
+ Promise.resolve(element.tagName || 'unknown'),
1046
+ element.text().catch(() => ''),
1047
+ element.attribute('class').catch(() => ''),
1048
+ element.attribute('id').catch(() => ''),
1049
+ element.attribute('data-testid').catch(() => ''),
1050
+ element.attribute('data-id').catch(() => ''),
1051
+ element.size().catch(() => null),
1052
+ element.offset().catch(() => null)
1053
+ ]);
1054
+ // 提取结果
1055
+ const tagName = tagNameResult.status === 'fulfilled' ? tagNameResult.value : 'unknown';
1056
+ const text = textResult.status === 'fulfilled' ? textResult.value : '';
1057
+ const className = classResult.status === 'fulfilled' ? classResult.value : '';
1058
+ const id = idResult.status === 'fulfilled' ? idResult.value : '';
1059
+ const testId = testIdResult.status === 'fulfilled' ? testIdResult.value : '';
1060
+ const dataId = dataIdResult.status === 'fulfilled' ? dataIdResult.value : '';
1061
+ const size = sizeResult.status === 'fulfilled' ? sizeResult.value : null;
1062
+ const offset = offsetResult.status === 'fulfilled' ? offsetResult.value : null;
1063
+ // 生成 UID(增强版优先级顺序)
1064
+ // 优先级:data-testid > id > data-id > class+文本哈希 > nth-child
1065
+ let selector = tagName;
1066
+ if (testId) {
1067
+ // 优先级1: data-testid(专门用于测试,最稳定)
1068
+ selector += `[data-testid="${testId}"]`;
1069
+ }
1070
+ else if (id) {
1071
+ // 优先级2: id 属性
1072
+ selector += `#${id}`;
1073
+ }
1074
+ else if (dataId) {
1075
+ // 优先级3: data-id
1076
+ selector += `[data-id="${dataId}"]`;
1077
+ }
1078
+ else if (className) {
1079
+ // 优先级4: class + 文本哈希(中等稳定性)
1080
+ const firstClass = className.split(' ')[0];
1081
+ const textHash = simpleTextHash(text);
1082
+ selector += `.${firstClass}${textHash}`;
1083
+ }
1084
+ else {
1085
+ // 优先级5: nth-child(兜底)
1086
+ selector += `:nth-child(${i + 1})`;
1087
+ }
1088
+ const uid = selector;
1089
+ // 构建快照
929
1090
  const snapshot = {
930
1091
  uid,
931
- tagName: element.tagName || 'unknown',
1092
+ tagName,
932
1093
  };
933
- // 获取元素文本
934
- try {
935
- const text = await element.text();
936
- if (text && text.trim()) {
937
- snapshot.text = text.trim();
938
- }
939
- }
940
- catch (error) {
941
- // 忽略无法获取文本的元素
1094
+ // 添加文本内容
1095
+ if (text && text.trim()) {
1096
+ snapshot.text = text.trim();
942
1097
  }
943
- // 获取元素位置信息
944
- try {
945
- const [size, offset] = await Promise.all([
946
- element.size(),
947
- element.offset()
948
- ]);
1098
+ // 添加位置信息
1099
+ if (size && offset) {
949
1100
  snapshot.position = {
950
1101
  left: offset.left,
951
1102
  top: offset.top,
@@ -953,56 +1104,38 @@ export async function getPageSnapshot(page) {
953
1104
  height: size.height
954
1105
  };
955
1106
  }
956
- catch (error) {
957
- // 忽略无法获取位置的元素
958
- }
959
- // 获取常用属性
960
- try {
961
- const attributes = {};
962
- const commonAttrs = ['class', 'id', 'data-*'];
963
- for (const attr of commonAttrs) {
964
- try {
965
- const value = await element.attribute(attr);
966
- if (value) {
967
- attributes[attr] = value;
968
- }
969
- }
970
- catch (error) {
971
- // 忽略不存在的属性
972
- }
973
- }
974
- if (Object.keys(attributes).length > 0) {
975
- snapshot.attributes = attributes;
976
- }
977
- }
978
- catch (error) {
979
- // 忽略属性获取错误
980
- }
1107
+ // 添加属性信息(可选,目前不收集)
1108
+ // 如果需要属性,可以在上面的 Promise.allSettled 中添加更多属性查询
981
1109
  elements.push(snapshot);
982
- // 生成可查询的基础选择器(不包含伪类)
983
- const tagName = element.tagName;
984
- const className = await element.attribute('class').catch(() => '');
985
- const id = await element.attribute('id').catch(() => '');
1110
+ // 生成可查询的基础选择器(与 UID 优先级一致)
986
1111
  let baseSelector = tagName;
987
- if (id) {
1112
+ if (testId) {
1113
+ baseSelector = `${tagName}[data-testid="${testId}"]`;
1114
+ }
1115
+ else if (id) {
988
1116
  baseSelector = `${tagName}#${id}`;
989
1117
  }
1118
+ else if (dataId) {
1119
+ baseSelector = `${tagName}[data-id="${dataId}"]`;
1120
+ }
990
1121
  else if (className) {
991
1122
  baseSelector = `${tagName}.${className.split(' ')[0]}`;
992
1123
  }
993
1124
  // 计算该选择器的元素索引(递增计数)
994
1125
  const currentIndex = selectorIndexMap.get(baseSelector) || 0;
995
1126
  selectorIndexMap.set(baseSelector, currentIndex + 1);
996
- // 存储 ElementMapInfo,使用可查询的基础选择器和索引
1127
+ // 存储 ElementMapInfo
997
1128
  elementMap.set(uid, {
998
- selector: baseSelector, // 使用可查询的基础选择器
999
- index: currentIndex // 使用该选择器的当前计数
1129
+ selector: baseSelector,
1130
+ index: currentIndex
1000
1131
  });
1001
1132
  }
1002
1133
  catch (error) {
1003
- console.warn(`Error processing element ${i}:`, error);
1134
+ console.warn(`⚠️ 处理元素 ${i} 时出错:`, error);
1004
1135
  }
1005
1136
  }
1137
+ const processingTime = Date.now() - startTime;
1138
+ console.error(`⏱️ 元素处理耗时: ${processingTime}ms (平均 ${(processingTime / childElements.length).toFixed(2)}ms/元素)`);
1006
1139
  const pagePath = await page.path;
1007
1140
  const snapshot = {
1008
1141
  path: pagePath,
@@ -1036,7 +1169,7 @@ export async function clickElement(page, elementMap, options) {
1036
1169
  if (!mapInfo) {
1037
1170
  throw new Error(`找不到uid为 ${uid} 的元素,请先获取页面快照`);
1038
1171
  }
1039
- console.log(`[Click] 准备点击元素 - UID: ${uid}, Selector: ${mapInfo.selector}, Index: ${mapInfo.index}`);
1172
+ console.error(`[Click] 准备点击元素 - UID: ${uid}, Selector: ${mapInfo.selector}, Index: ${mapInfo.index}`);
1040
1173
  // 使用选择器获取所有匹配元素
1041
1174
  const elements = await page.$$(mapInfo.selector);
1042
1175
  if (!elements || elements.length === 0) {
@@ -1053,27 +1186,27 @@ export async function clickElement(page, elementMap, options) {
1053
1186
  }
1054
1187
  // 记录点击前的页面路径
1055
1188
  const beforePath = await page.path;
1056
- console.log(`[Click] 点击前页面: ${beforePath}`);
1189
+ console.error(`[Click] 点击前页面: ${beforePath}`);
1057
1190
  // 执行点击操作
1058
1191
  await element.tap();
1059
- console.log(`[Click] 已执行 tap() 操作`);
1192
+ console.error(`[Click] 已执行 tap() 操作`);
1060
1193
  // 如果是双击,再点击一次
1061
1194
  if (dblClick) {
1062
1195
  await new Promise(resolve => setTimeout(resolve, 100)); // 短暂延迟
1063
1196
  await element.tap();
1064
- console.log(`[Click] 已执行第二次 tap() (双击)`);
1197
+ console.error(`[Click] 已执行第二次 tap() (双击)`);
1065
1198
  }
1066
1199
  // 等待一小段时间,让页面有机会响应
1067
1200
  await new Promise(resolve => setTimeout(resolve, 300));
1068
1201
  // 记录点击后的页面路径
1069
1202
  try {
1070
1203
  const afterPath = await page.path;
1071
- console.log(`[Click] 点击后页面: ${afterPath}`);
1204
+ console.error(`[Click] 点击后页面: ${afterPath}`);
1072
1205
  if (beforePath !== afterPath) {
1073
- console.log(`[Click] ✅ 页面已切换: ${beforePath} → ${afterPath}`);
1206
+ console.error(`[Click] ✅ 页面已切换: ${beforePath} → ${afterPath}`);
1074
1207
  }
1075
1208
  else {
1076
- console.log(`[Click] ⚠️ 页面未切换,可能是同页面操作或导航延迟`);
1209
+ console.error(`[Click] ⚠️ 页面未切换,可能是同页面操作或导航延迟`);
1077
1210
  }
1078
1211
  }
1079
1212
  catch (error) {
@@ -1101,12 +1234,12 @@ export async function takeScreenshot(miniProgram, options = {}) {
1101
1234
  const { path } = options;
1102
1235
  // 确保页面完全加载和稳定
1103
1236
  try {
1104
- console.log('获取当前页面并等待稳定...');
1237
+ console.error('获取当前页面并等待稳定...');
1105
1238
  const currentPage = await miniProgram.currentPage();
1106
1239
  if (currentPage && typeof currentPage.waitFor === 'function') {
1107
1240
  // 等待页面稳定,增加等待时间
1108
1241
  await currentPage.waitFor(1000);
1109
- console.log('页面等待完成');
1242
+ console.error('页面等待完成');
1110
1243
  }
1111
1244
  }
1112
1245
  catch (waitError) {
@@ -1114,24 +1247,30 @@ export async function takeScreenshot(miniProgram, options = {}) {
1114
1247
  }
1115
1248
  // 重试机制执行截图
1116
1249
  let result;
1250
+ let screenshotSucceeded = false;
1117
1251
  let lastError;
1118
1252
  for (let attempt = 1; attempt <= 3; attempt++) {
1119
1253
  try {
1120
- console.log(`截图尝试 ${attempt}/3`);
1254
+ console.error(`截图尝试 ${attempt}/3`);
1121
1255
  if (path) {
1122
1256
  // 保存到指定路径
1123
1257
  await miniProgram.screenshot({ path });
1258
+ if (!fs.existsSync(path)) {
1259
+ throw new Error(`截图命令返回成功,但目标文件不存在: ${path}`);
1260
+ }
1261
+ screenshotSucceeded = true;
1124
1262
  result = undefined;
1125
- console.log(`截图保存成功: ${path}`);
1263
+ console.error(`截图保存成功: ${path}`);
1126
1264
  break;
1127
1265
  }
1128
1266
  else {
1129
1267
  // 返回base64数据
1130
1268
  const base64Data = await miniProgram.screenshot();
1131
- console.log('截图API调用完成,检查返回数据...');
1269
+ console.error('截图API调用完成,检查返回数据...');
1132
1270
  if (base64Data && typeof base64Data === 'string' && base64Data.length > 0) {
1271
+ screenshotSucceeded = true;
1133
1272
  result = base64Data;
1134
- console.log(`截图成功,数据长度: ${base64Data.length}`);
1273
+ console.error(`截图成功,数据长度: ${base64Data.length}`);
1135
1274
  break;
1136
1275
  }
1137
1276
  else {
@@ -1144,12 +1283,12 @@ export async function takeScreenshot(miniProgram, options = {}) {
1144
1283
  console.warn(`截图尝试 ${attempt} 失败:`, lastError.message);
1145
1284
  if (attempt < 3) {
1146
1285
  // 重试前等待更长时间,让页面稳定
1147
- console.log(`等待 ${1000 + attempt * 500}ms 后重试...`);
1286
+ console.error(`等待 ${1000 + attempt * 500}ms 后重试...`);
1148
1287
  await new Promise(resolve => setTimeout(resolve, 1000 + attempt * 500));
1149
1288
  }
1150
1289
  }
1151
1290
  }
1152
- if (!result && !path) {
1291
+ if (!screenshotSucceeded) {
1153
1292
  const troubleshootingTips = `
1154
1293
 
1155
1294
  ⚠️ 截图功能故障排除建议:
@@ -2129,3 +2268,4 @@ export async function reLaunch(miniProgram, options) {
2129
2268
  throw new Error(`重新启动失败: ${errorMessage}`);
2130
2269
  }
2131
2270
  }
2271
+ //# sourceMappingURL=tools.js.map