weixin-devtools-mcp 0.0.1 → 0.1.0

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -114
  3. package/build/MiniProgramContext.d.ts +307 -0
  4. package/build/MiniProgramContext.d.ts.map +1 -0
  5. package/build/MiniProgramContext.js +650 -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 +198 -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 +96 -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/formatters/consoleFormatter.d.ts +50 -0
  56. package/build/formatters/consoleFormatter.d.ts.map +1 -0
  57. package/build/formatters/consoleFormatter.js +116 -0
  58. package/build/formatters/consoleFormatter.js.map +1 -0
  59. package/build/formatters/snapshotFormatter.d.ts +41 -0
  60. package/build/formatters/snapshotFormatter.d.ts.map +1 -0
  61. package/build/formatters/snapshotFormatter.js +156 -0
  62. package/build/formatters/snapshotFormatter.js.map +1 -0
  63. package/build/index.d.ts +11 -0
  64. package/build/index.d.ts.map +1 -0
  65. package/build/index.js +45 -9
  66. package/build/index.js.map +1 -0
  67. package/build/server.d.ts +7 -0
  68. package/build/server.d.ts.map +1 -0
  69. package/build/server.js +70 -30
  70. package/build/server.js.map +1 -0
  71. package/build/tools/ToolDefinition.d.ts +215 -0
  72. package/build/tools/ToolDefinition.d.ts.map +1 -0
  73. package/build/tools/ToolDefinition.js +9 -7
  74. package/build/tools/ToolDefinition.js.map +1 -0
  75. package/build/tools/assert.d.ts +17 -0
  76. package/build/tools/assert.d.ts.map +1 -0
  77. package/build/tools/assert.js +8 -88
  78. package/build/tools/assert.js.map +1 -0
  79. package/build/tools/connection.d.ts +13 -0
  80. package/build/tools/connection.d.ts.map +1 -0
  81. package/build/tools/connection.js +332 -615
  82. package/build/tools/connection.js.map +1 -0
  83. package/build/tools/console.d.ts +20 -0
  84. package/build/tools/console.d.ts.map +1 -0
  85. package/build/tools/console.js +162 -152
  86. package/build/tools/console.js.map +1 -0
  87. package/build/tools/diagnose.d.ts +22 -0
  88. package/build/tools/diagnose.d.ts.map +1 -0
  89. package/build/tools/diagnose.js +400 -13
  90. package/build/tools/diagnose.js.map +1 -0
  91. package/build/tools/index.d.ts +6 -0
  92. package/build/tools/index.d.ts.map +1 -0
  93. package/build/tools/index.js +3 -77
  94. package/build/tools/index.js.map +1 -0
  95. package/build/tools/input.d.ts +21 -0
  96. package/build/tools/input.d.ts.map +1 -0
  97. package/build/tools/input.js +73 -139
  98. package/build/tools/input.js.map +1 -0
  99. package/build/tools/navigate.d.ts +21 -0
  100. package/build/tools/navigate.d.ts.map +1 -0
  101. package/build/tools/navigate.js +63 -126
  102. package/build/tools/navigate.js.map +1 -0
  103. package/build/tools/network.d.ts +17 -0
  104. package/build/tools/network.d.ts.map +1 -0
  105. package/build/tools/network.js +75 -887
  106. package/build/tools/network.js.map +1 -0
  107. package/build/tools/page.d.ts +13 -0
  108. package/build/tools/page.d.ts.map +1 -0
  109. package/build/tools/page.js +4 -1
  110. package/build/tools/page.js.map +1 -0
  111. package/build/tools/screenshot.d.ts +9 -0
  112. package/build/tools/screenshot.d.ts.map +1 -0
  113. package/build/tools/screenshot.js +3 -1
  114. package/build/tools/screenshot.js.map +1 -0
  115. package/build/tools/script.d.ts +6 -0
  116. package/build/tools/script.d.ts.map +1 -0
  117. package/build/tools/script.js +92 -0
  118. package/build/tools/script.js.map +1 -0
  119. package/build/tools/snapshot.d.ts +9 -0
  120. package/build/tools/snapshot.d.ts.map +1 -0
  121. package/build/tools/snapshot.js +78 -12
  122. package/build/tools/snapshot.js.map +1 -0
  123. package/build/tools/tools.d.ts +15 -0
  124. package/build/tools/tools.d.ts.map +1 -0
  125. package/build/tools/tools.js +62 -0
  126. package/build/tools/tools.js.map +1 -0
  127. package/build/tools.d.ts +431 -0
  128. package/build/tools.d.ts.map +1 -0
  129. package/build/tools.js +235 -117
  130. package/build/tools.js.map +1 -0
  131. package/build/types/errors.d.ts +189 -0
  132. package/build/types/errors.d.ts.map +1 -0
  133. package/build/types/errors.js +257 -0
  134. package/build/types/errors.js.map +1 -0
  135. package/build/utils/idGenerator.d.ts +21 -0
  136. package/build/utils/idGenerator.d.ts.map +1 -0
  137. package/build/utils/idGenerator.js +23 -0
  138. package/build/utils/idGenerator.js.map +1 -0
  139. package/package.json +34 -10
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);
@@ -572,14 +601,14 @@ async function executeCliCommand(command) {
572
601
  process.stdout.on('data', (data) => {
573
602
  const text = data.toString();
574
603
  output += text;
575
- console.log('[CLI stdout]:', text.trim());
604
+ console.error('[CLI stdout]:', text.trim());
576
605
  });
577
606
  }
578
607
  if (process.stderr) {
579
608
  process.stderr.on('data', (data) => {
580
609
  const text = data.toString();
581
610
  errorOutput += text;
582
- console.log('[CLI stderr]:', text.trim());
611
+ console.error('[CLI stderr]:', text.trim());
583
612
  // 检测端口冲突错误
584
613
  if (text.includes('must be restarted on port')) {
585
614
  const match = text.match(/started on .+:(\d+) and must be restarted on port (\d+)/);
@@ -619,7 +648,7 @@ async function executeCliCommand(command) {
619
648
  // 检测 CLI 命令失败(通用)
620
649
  if (text.includes('error') || text.includes('failed') || text.includes('失败')) {
621
650
  if (!resolved && text.length > 10) { // 确保不是误报
622
- console.log('[CLI 警告] 检测到潜在错误:', text.trim());
651
+ console.error('[CLI 警告] 检测到潜在错误:', text.trim());
623
652
  }
624
653
  }
625
654
  });
@@ -661,22 +690,21 @@ async function executeCliCommand(command) {
661
690
  export async function waitForWebSocketReady(port, timeout, verbose = false) {
662
691
  const startTime = Date.now();
663
692
  let attempt = 0;
664
- const maxAttempts = Math.ceil(timeout / 1000); // 每秒检查一次
665
693
  if (verbose) {
666
- console.log(`等待WebSocket服务启动,端口: ${port},超时: ${timeout}ms`);
694
+ console.error(`等待WebSocket服务启动,端口: ${port},超时: ${timeout}ms`);
667
695
  }
668
696
  while (Date.now() - startTime < timeout) {
669
697
  attempt++;
670
698
  if (verbose && attempt % 5 === 0) { // 每5秒显示一次进度
671
699
  const elapsed = Date.now() - startTime;
672
- console.log(`WebSocket检测进度: ${Math.round(elapsed / 1000)}s / ${Math.round(timeout / 1000)}s`);
700
+ console.error(`WebSocket检测进度: ${Math.round(elapsed / 1000)}s / ${Math.round(timeout / 1000)}s`);
673
701
  }
674
702
  // 尝试多种检测方式
675
703
  const isReady = await checkDevToolsRunning(port) || await checkWebSocketDirectly(port);
676
704
  if (isReady) {
677
705
  if (verbose) {
678
706
  const elapsed = Date.now() - startTime;
679
- console.log(`WebSocket服务已启动,耗时: ${elapsed}ms`);
707
+ console.error(`WebSocket服务已启动,耗时: ${elapsed}ms`);
680
708
  }
681
709
  return;
682
710
  }
@@ -737,16 +765,16 @@ export async function detectIDEPort(verbose = false) {
737
765
  // 常用端口列表
738
766
  const commonPorts = [9420, 9440, 9430, 9450, 9460];
739
767
  if (verbose) {
740
- console.log('🔍 检测微信开发者工具运行端口...');
768
+ console.error('🔍 检测微信开发者工具运行端口...');
741
769
  }
742
770
  // 策略1: 尝试常用端口
743
771
  for (const port of commonPorts) {
744
772
  if (verbose) {
745
- console.log(` 检测端口 ${port}...`);
773
+ console.error(` 检测端口 ${port}...`);
746
774
  }
747
775
  if (await checkDevToolsRunning(port)) {
748
776
  if (verbose) {
749
- console.log(`✅ 检测到IDE运行在端口 ${port}`);
777
+ console.error(`✅ 检测到IDE运行在端口 ${port}`);
750
778
  }
751
779
  return port;
752
780
  }
@@ -760,14 +788,14 @@ export async function detectIDEPort(verbose = false) {
760
788
  if (output) {
761
789
  const ports = output.split('\n').map((p) => parseInt(p, 10)).filter((p) => !isNaN(p));
762
790
  if (verbose && ports.length > 0) {
763
- console.log(` lsof检测到端口: ${ports.join(', ')}`);
791
+ console.error(` lsof检测到端口: ${ports.join(', ')}`);
764
792
  }
765
793
  // 遍历检测到的端口,验证是否为有效的自动化端口
766
794
  for (const port of ports) {
767
795
  if (port >= 9400 && port <= 9500) {
768
796
  if (await checkDevToolsRunning(port)) {
769
797
  if (verbose) {
770
- console.log(`✅ 通过lsof检测到IDE运行在端口 ${port}`);
798
+ console.error(`✅ 通过lsof检测到IDE运行在端口 ${port}`);
771
799
  }
772
800
  return port;
773
801
  }
@@ -778,12 +806,12 @@ export async function detectIDEPort(verbose = false) {
778
806
  catch (error) {
779
807
  // lsof 失败,继续
780
808
  if (verbose) {
781
- console.log(' lsof检测失败');
809
+ console.error(' lsof检测失败');
782
810
  }
783
811
  }
784
812
  }
785
813
  if (verbose) {
786
- console.log('❌ 未检测到IDE运行端口');
814
+ console.error('❌ 未检测到IDE运行端口');
787
815
  }
788
816
  return null;
789
817
  }
@@ -838,30 +866,69 @@ async function executeWithDetailedError(operation, phase) {
838
866
  throw new DevToolsConnectionError(originalError.message, phase, originalError, { timestamp: new Date().toISOString() });
839
867
  }
840
868
  }
869
+ /**
870
+ * 生成简单的文本哈希(用于增强 UID 唯一性)
871
+ */
872
+ function simpleTextHash(text) {
873
+ if (!text || text.length === 0)
874
+ return '';
875
+ // 取文本的前 8 个字符,过滤特殊字符
876
+ const sanitized = text.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '').slice(0, 8);
877
+ if (sanitized.length === 0)
878
+ return '';
879
+ return `_${sanitized}`;
880
+ }
841
881
  /**
842
882
  * 生成元素的唯一标识符 (uid)
883
+ *
884
+ * 优先级顺序(稳定性从高到低):
885
+ * 1. data-testid(专门用于测试,最稳定)
886
+ * 2. id 属性
887
+ * 3. data-id(自定义数据属性)
888
+ * 4. class + 文本哈希(中等稳定性)
889
+ * 5. class:eq(index)
890
+ * 6. nth-child(兜底)
843
891
  */
844
892
  export async function generateElementUid(element, index) {
845
893
  try {
846
894
  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}`);
895
+ // 并行获取所有可能的标识属性
896
+ const [className, id, testId, dataId, text] = await Promise.all([
897
+ element.attribute('class').catch(() => ''),
898
+ element.attribute('id').catch(() => ''),
899
+ element.attribute('data-testid').catch(() => ''),
900
+ element.attribute('data-id').catch(() => ''),
901
+ element.text().catch(() => '')
902
+ ]);
903
+ console.error(`[generateElementUid] tagName=${tagName}, id="${id}", testId="${testId}", dataId="${dataId}", className="${className}", index=${index}`);
850
904
  let selector = tagName;
851
- if (id) {
905
+ // 优先级1: data-testid(最稳定)
906
+ if (testId) {
907
+ selector += `[data-testid="${testId}"]`;
908
+ }
909
+ // 优先级2: id 属性
910
+ else if (id) {
852
911
  selector += `#${id}`;
853
912
  }
913
+ // 优先级3: data-id
914
+ else if (dataId) {
915
+ selector += `[data-id="${dataId}"]`;
916
+ }
917
+ // 优先级4: class + 文本哈希
854
918
  else if (className) {
855
- selector += `.${className.split(' ')[0]}`;
919
+ const firstClass = className.split(' ')[0];
920
+ const textHash = simpleTextHash(text);
921
+ selector += `.${firstClass}${textHash}`;
856
922
  }
923
+ // 优先级5: nth-child(兜底)
857
924
  else {
858
925
  selector += `:nth-child(${index + 1})`;
859
926
  }
860
- console.log(`[generateElementUid] Generated UID: ${selector}`);
927
+ console.error(`[generateElementUid] Generated UID: ${selector}`);
861
928
  return selector;
862
929
  }
863
930
  catch (error) {
864
- console.log(`[generateElementUid] Error:`, error);
931
+ console.error(`[generateElementUid] Error:`, error);
865
932
  return `${element.tagName || 'unknown'}:nth-child(${index + 1})`;
866
933
  }
867
934
  }
@@ -882,16 +949,21 @@ export async function getPageSnapshot(page) {
882
949
  await new Promise(resolve => setTimeout(resolve, 1000));
883
950
  // 尝试多种选择器策略获取元素
884
951
  let childElements = [];
885
- // 策略1: 尝试获取所有元素
952
+ let usedStrategy = 'unknown';
953
+ // 策略1: 优先使用通配符(最快,一次API调用)
886
954
  try {
887
955
  childElements = await page.$$('*');
888
- console.log(`策略1 (*) 获取到 ${childElements.length} 个元素`);
956
+ if (childElements.length > 0) {
957
+ usedStrategy = 'wildcard(*)';
958
+ console.error(`✅ 策略1成功: 通配符查询获取到 ${childElements.length} 个元素`);
959
+ }
889
960
  }
890
961
  catch (error) {
891
- console.log('策略1 (*) 失败:', error);
962
+ console.warn('⚠️ 策略1失败 (*)', error);
892
963
  }
893
- // 策略2: 如果策略1失败,尝试小程序常用组件
964
+ // 策略2: 降级到常用组件选择器(仅当策略1失败时)
894
965
  if (childElements.length === 0) {
966
+ console.error('🔄 策略1无结果,降级到策略2(常用组件选择器)');
895
967
  const commonSelectors = [
896
968
  'view', 'text', 'button', 'image', 'input', 'textarea', 'picker', 'switch',
897
969
  'slider', 'scroll-view', 'swiper', 'icon', 'rich-text', 'progress',
@@ -901,51 +973,108 @@ export async function getPageSnapshot(page) {
901
973
  try {
902
974
  const elements = await page.$$(selector);
903
975
  childElements.push(...elements);
904
- console.log(`策略2 (${selector}) 获取到 ${elements.length} 个元素`);
976
+ if (elements.length > 0) {
977
+ console.error(` - ${selector}: ${elements.length} 个元素`);
978
+ }
905
979
  }
906
980
  catch (error) {
907
- console.log(`策略2 (${selector}) 失败:`, error);
981
+ // 忽略单个选择器失败
908
982
  }
909
983
  }
984
+ if (childElements.length > 0) {
985
+ usedStrategy = 'common-selectors';
986
+ console.error(`✅ 策略2成功: 获取到 ${childElements.length} 个元素`);
987
+ }
910
988
  }
911
- // 策略3: 如果还是没有元素,尝试根据层级查找
989
+ // 策略3: 最后尝试层级选择器
912
990
  if (childElements.length === 0) {
991
+ console.error('🔄 策略2无结果,降级到策略3(层级选择器)');
913
992
  try {
914
993
  const rootElements = await page.$$('page > *');
915
994
  childElements = rootElements;
916
- console.log(`策略3 (page > *) 获取到 ${childElements.length} 个元素`);
995
+ if (childElements.length > 0) {
996
+ usedStrategy = 'hierarchical(page>*)';
997
+ console.error(`✅ 策略3成功: 获取到 ${childElements.length} 个元素`);
998
+ }
917
999
  }
918
1000
  catch (error) {
919
- console.log('策略3 (page > *) 失败:', error);
1001
+ console.warn('⚠️ 策略3失败 (page > *)', error);
920
1002
  }
921
1003
  }
922
- console.log(`最终获取到 ${childElements.length} 个元素`);
1004
+ if (childElements.length === 0) {
1005
+ console.warn('❌ 所有策略均未获取到元素');
1006
+ return {
1007
+ snapshot: { path: await page.path, elements: [] },
1008
+ elementMap: new Map()
1009
+ };
1010
+ }
1011
+ console.error(`📊 最终获取到 ${childElements.length} 个元素(策略:${usedStrategy})`);
923
1012
  // 用于跟踪每个基础选择器的元素计数
924
1013
  const selectorIndexMap = new Map();
1014
+ // 优化:批量并行处理元素属性
1015
+ const startTime = Date.now();
925
1016
  for (let i = 0; i < childElements.length; i++) {
926
1017
  const element = childElements[i];
927
1018
  try {
928
- const uid = await generateElementUid(element, i);
1019
+ // 🚀 优化点1: 使用 Promise.allSettled 并行获取所有元素属性
1020
+ // 减少API调用往返次数:从 6次串行 → 1次并行
1021
+ // 新增 data-testid 和 data-id 属性用于增强 UID 稳定性
1022
+ const [tagNameResult, textResult, classResult, idResult, testIdResult, dataIdResult, sizeResult, offsetResult] = await Promise.allSettled([
1023
+ Promise.resolve(element.tagName || 'unknown'),
1024
+ element.text().catch(() => ''),
1025
+ element.attribute('class').catch(() => ''),
1026
+ element.attribute('id').catch(() => ''),
1027
+ element.attribute('data-testid').catch(() => ''),
1028
+ element.attribute('data-id').catch(() => ''),
1029
+ element.size().catch(() => null),
1030
+ element.offset().catch(() => null)
1031
+ ]);
1032
+ // 提取结果
1033
+ const tagName = tagNameResult.status === 'fulfilled' ? tagNameResult.value : 'unknown';
1034
+ const text = textResult.status === 'fulfilled' ? textResult.value : '';
1035
+ const className = classResult.status === 'fulfilled' ? classResult.value : '';
1036
+ const id = idResult.status === 'fulfilled' ? idResult.value : '';
1037
+ const testId = testIdResult.status === 'fulfilled' ? testIdResult.value : '';
1038
+ const dataId = dataIdResult.status === 'fulfilled' ? dataIdResult.value : '';
1039
+ const size = sizeResult.status === 'fulfilled' ? sizeResult.value : null;
1040
+ const offset = offsetResult.status === 'fulfilled' ? offsetResult.value : null;
1041
+ // 生成 UID(增强版优先级顺序)
1042
+ // 优先级:data-testid > id > data-id > class+文本哈希 > nth-child
1043
+ let selector = tagName;
1044
+ if (testId) {
1045
+ // 优先级1: data-testid(专门用于测试,最稳定)
1046
+ selector += `[data-testid="${testId}"]`;
1047
+ }
1048
+ else if (id) {
1049
+ // 优先级2: id 属性
1050
+ selector += `#${id}`;
1051
+ }
1052
+ else if (dataId) {
1053
+ // 优先级3: data-id
1054
+ selector += `[data-id="${dataId}"]`;
1055
+ }
1056
+ else if (className) {
1057
+ // 优先级4: class + 文本哈希(中等稳定性)
1058
+ const firstClass = className.split(' ')[0];
1059
+ const textHash = simpleTextHash(text);
1060
+ selector += `.${firstClass}${textHash}`;
1061
+ }
1062
+ else {
1063
+ // 优先级5: nth-child(兜底)
1064
+ selector += `:nth-child(${i + 1})`;
1065
+ }
1066
+ const uid = selector;
1067
+ // 构建快照
929
1068
  const snapshot = {
930
1069
  uid,
931
- tagName: element.tagName || 'unknown',
1070
+ tagName,
932
1071
  };
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
- // 忽略无法获取文本的元素
1072
+ // 添加文本内容
1073
+ if (text && text.trim()) {
1074
+ snapshot.text = text.trim();
942
1075
  }
943
- // 获取元素位置信息
944
- try {
945
- const [size, offset] = await Promise.all([
946
- element.size(),
947
- element.offset()
948
- ]);
1076
+ // 添加位置信息
1077
+ if (size && offset) {
949
1078
  snapshot.position = {
950
1079
  left: offset.left,
951
1080
  top: offset.top,
@@ -953,56 +1082,38 @@ export async function getPageSnapshot(page) {
953
1082
  height: size.height
954
1083
  };
955
1084
  }
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
- }
1085
+ // 添加属性信息(可选,目前不收集)
1086
+ // 如果需要属性,可以在上面的 Promise.allSettled 中添加更多属性查询
981
1087
  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(() => '');
1088
+ // 生成可查询的基础选择器(与 UID 优先级一致)
986
1089
  let baseSelector = tagName;
987
- if (id) {
1090
+ if (testId) {
1091
+ baseSelector = `${tagName}[data-testid="${testId}"]`;
1092
+ }
1093
+ else if (id) {
988
1094
  baseSelector = `${tagName}#${id}`;
989
1095
  }
1096
+ else if (dataId) {
1097
+ baseSelector = `${tagName}[data-id="${dataId}"]`;
1098
+ }
990
1099
  else if (className) {
991
1100
  baseSelector = `${tagName}.${className.split(' ')[0]}`;
992
1101
  }
993
1102
  // 计算该选择器的元素索引(递增计数)
994
1103
  const currentIndex = selectorIndexMap.get(baseSelector) || 0;
995
1104
  selectorIndexMap.set(baseSelector, currentIndex + 1);
996
- // 存储 ElementMapInfo,使用可查询的基础选择器和索引
1105
+ // 存储 ElementMapInfo
997
1106
  elementMap.set(uid, {
998
- selector: baseSelector, // 使用可查询的基础选择器
999
- index: currentIndex // 使用该选择器的当前计数
1107
+ selector: baseSelector,
1108
+ index: currentIndex
1000
1109
  });
1001
1110
  }
1002
1111
  catch (error) {
1003
- console.warn(`Error processing element ${i}:`, error);
1112
+ console.warn(`⚠️ 处理元素 ${i} 时出错:`, error);
1004
1113
  }
1005
1114
  }
1115
+ const processingTime = Date.now() - startTime;
1116
+ console.error(`⏱️ 元素处理耗时: ${processingTime}ms (平均 ${(processingTime / childElements.length).toFixed(2)}ms/元素)`);
1006
1117
  const pagePath = await page.path;
1007
1118
  const snapshot = {
1008
1119
  path: pagePath,
@@ -1036,7 +1147,7 @@ export async function clickElement(page, elementMap, options) {
1036
1147
  if (!mapInfo) {
1037
1148
  throw new Error(`找不到uid为 ${uid} 的元素,请先获取页面快照`);
1038
1149
  }
1039
- console.log(`[Click] 准备点击元素 - UID: ${uid}, Selector: ${mapInfo.selector}, Index: ${mapInfo.index}`);
1150
+ console.error(`[Click] 准备点击元素 - UID: ${uid}, Selector: ${mapInfo.selector}, Index: ${mapInfo.index}`);
1040
1151
  // 使用选择器获取所有匹配元素
1041
1152
  const elements = await page.$$(mapInfo.selector);
1042
1153
  if (!elements || elements.length === 0) {
@@ -1053,27 +1164,27 @@ export async function clickElement(page, elementMap, options) {
1053
1164
  }
1054
1165
  // 记录点击前的页面路径
1055
1166
  const beforePath = await page.path;
1056
- console.log(`[Click] 点击前页面: ${beforePath}`);
1167
+ console.error(`[Click] 点击前页面: ${beforePath}`);
1057
1168
  // 执行点击操作
1058
1169
  await element.tap();
1059
- console.log(`[Click] 已执行 tap() 操作`);
1170
+ console.error(`[Click] 已执行 tap() 操作`);
1060
1171
  // 如果是双击,再点击一次
1061
1172
  if (dblClick) {
1062
1173
  await new Promise(resolve => setTimeout(resolve, 100)); // 短暂延迟
1063
1174
  await element.tap();
1064
- console.log(`[Click] 已执行第二次 tap() (双击)`);
1175
+ console.error(`[Click] 已执行第二次 tap() (双击)`);
1065
1176
  }
1066
1177
  // 等待一小段时间,让页面有机会响应
1067
1178
  await new Promise(resolve => setTimeout(resolve, 300));
1068
1179
  // 记录点击后的页面路径
1069
1180
  try {
1070
1181
  const afterPath = await page.path;
1071
- console.log(`[Click] 点击后页面: ${afterPath}`);
1182
+ console.error(`[Click] 点击后页面: ${afterPath}`);
1072
1183
  if (beforePath !== afterPath) {
1073
- console.log(`[Click] ✅ 页面已切换: ${beforePath} → ${afterPath}`);
1184
+ console.error(`[Click] ✅ 页面已切换: ${beforePath} → ${afterPath}`);
1074
1185
  }
1075
1186
  else {
1076
- console.log(`[Click] ⚠️ 页面未切换,可能是同页面操作或导航延迟`);
1187
+ console.error(`[Click] ⚠️ 页面未切换,可能是同页面操作或导航延迟`);
1077
1188
  }
1078
1189
  }
1079
1190
  catch (error) {
@@ -1101,12 +1212,12 @@ export async function takeScreenshot(miniProgram, options = {}) {
1101
1212
  const { path } = options;
1102
1213
  // 确保页面完全加载和稳定
1103
1214
  try {
1104
- console.log('获取当前页面并等待稳定...');
1215
+ console.error('获取当前页面并等待稳定...');
1105
1216
  const currentPage = await miniProgram.currentPage();
1106
1217
  if (currentPage && typeof currentPage.waitFor === 'function') {
1107
1218
  // 等待页面稳定,增加等待时间
1108
1219
  await currentPage.waitFor(1000);
1109
- console.log('页面等待完成');
1220
+ console.error('页面等待完成');
1110
1221
  }
1111
1222
  }
1112
1223
  catch (waitError) {
@@ -1114,24 +1225,30 @@ export async function takeScreenshot(miniProgram, options = {}) {
1114
1225
  }
1115
1226
  // 重试机制执行截图
1116
1227
  let result;
1228
+ let screenshotSucceeded = false;
1117
1229
  let lastError;
1118
1230
  for (let attempt = 1; attempt <= 3; attempt++) {
1119
1231
  try {
1120
- console.log(`截图尝试 ${attempt}/3`);
1232
+ console.error(`截图尝试 ${attempt}/3`);
1121
1233
  if (path) {
1122
1234
  // 保存到指定路径
1123
1235
  await miniProgram.screenshot({ path });
1236
+ if (!fs.existsSync(path)) {
1237
+ throw new Error(`截图命令返回成功,但目标文件不存在: ${path}`);
1238
+ }
1239
+ screenshotSucceeded = true;
1124
1240
  result = undefined;
1125
- console.log(`截图保存成功: ${path}`);
1241
+ console.error(`截图保存成功: ${path}`);
1126
1242
  break;
1127
1243
  }
1128
1244
  else {
1129
1245
  // 返回base64数据
1130
1246
  const base64Data = await miniProgram.screenshot();
1131
- console.log('截图API调用完成,检查返回数据...');
1247
+ console.error('截图API调用完成,检查返回数据...');
1132
1248
  if (base64Data && typeof base64Data === 'string' && base64Data.length > 0) {
1249
+ screenshotSucceeded = true;
1133
1250
  result = base64Data;
1134
- console.log(`截图成功,数据长度: ${base64Data.length}`);
1251
+ console.error(`截图成功,数据长度: ${base64Data.length}`);
1135
1252
  break;
1136
1253
  }
1137
1254
  else {
@@ -1144,12 +1261,12 @@ export async function takeScreenshot(miniProgram, options = {}) {
1144
1261
  console.warn(`截图尝试 ${attempt} 失败:`, lastError.message);
1145
1262
  if (attempt < 3) {
1146
1263
  // 重试前等待更长时间,让页面稳定
1147
- console.log(`等待 ${1000 + attempt * 500}ms 后重试...`);
1264
+ console.error(`等待 ${1000 + attempt * 500}ms 后重试...`);
1148
1265
  await new Promise(resolve => setTimeout(resolve, 1000 + attempt * 500));
1149
1266
  }
1150
1267
  }
1151
1268
  }
1152
- if (!result && !path) {
1269
+ if (!screenshotSucceeded) {
1153
1270
  const troubleshootingTips = `
1154
1271
 
1155
1272
  ⚠️ 截图功能故障排除建议:
@@ -2129,3 +2246,4 @@ export async function reLaunch(miniProgram, options) {
2129
2246
  throw new Error(`重新启动失败: ${errorMessage}`);
2130
2247
  }
2131
2248
  }
2249
+ //# sourceMappingURL=tools.js.map