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.
- package/LICENSE +21 -0
- package/README.md +101 -114
- package/build/MiniProgramContext.d.ts +307 -0
- package/build/MiniProgramContext.d.ts.map +1 -0
- package/build/MiniProgramContext.js +650 -0
- package/build/MiniProgramContext.js.map +1 -0
- package/build/collectors/Collector.d.ts +127 -0
- package/build/collectors/Collector.d.ts.map +1 -0
- package/build/collectors/Collector.js +252 -0
- package/build/collectors/Collector.js.map +1 -0
- package/build/collectors/ConsoleCollector.d.ts +104 -0
- package/build/collectors/ConsoleCollector.d.ts.map +1 -0
- package/build/collectors/ConsoleCollector.js +157 -0
- package/build/collectors/ConsoleCollector.js.map +1 -0
- package/build/collectors/NetworkCollector.d.ts +167 -0
- package/build/collectors/NetworkCollector.d.ts.map +1 -0
- package/build/collectors/NetworkCollector.js +265 -0
- package/build/collectors/NetworkCollector.js.map +1 -0
- package/build/collectors/index.d.ts +13 -0
- package/build/collectors/index.d.ts.map +1 -0
- package/build/collectors/index.js +17 -0
- package/build/collectors/index.js.map +1 -0
- package/build/config/tool-profile.d.ts +30 -0
- package/build/config/tool-profile.d.ts.map +1 -0
- package/build/config/tool-profile.js +138 -0
- package/build/config/tool-profile.js.map +1 -0
- package/build/connection/adapters.d.ts +3 -0
- package/build/connection/adapters.d.ts.map +1 -0
- package/build/connection/adapters.js +134 -0
- package/build/connection/adapters.js.map +1 -0
- package/build/connection/errors.d.ts +34 -0
- package/build/connection/errors.d.ts.map +1 -0
- package/build/connection/errors.js +101 -0
- package/build/connection/errors.js.map +1 -0
- package/build/connection/health-probe.d.ts +4 -0
- package/build/connection/health-probe.d.ts.map +1 -0
- package/build/connection/health-probe.js +60 -0
- package/build/connection/health-probe.js.map +1 -0
- package/build/connection/index.d.ts +6 -0
- package/build/connection/index.d.ts.map +1 -0
- package/build/connection/index.js +6 -0
- package/build/connection/index.js.map +1 -0
- package/build/connection/manager.d.ts +19 -0
- package/build/connection/manager.d.ts.map +1 -0
- package/build/connection/manager.js +198 -0
- package/build/connection/manager.js.map +1 -0
- package/build/connection/resolver.d.ts +3 -0
- package/build/connection/resolver.d.ts.map +1 -0
- package/build/connection/resolver.js +96 -0
- package/build/connection/resolver.js.map +1 -0
- package/build/connection/types.d.ts +95 -0
- package/build/connection/types.d.ts.map +1 -0
- package/build/connection/types.js +16 -0
- package/build/connection/types.js.map +1 -0
- package/build/formatters/consoleFormatter.d.ts +50 -0
- package/build/formatters/consoleFormatter.d.ts.map +1 -0
- package/build/formatters/consoleFormatter.js +116 -0
- package/build/formatters/consoleFormatter.js.map +1 -0
- package/build/formatters/snapshotFormatter.d.ts +41 -0
- package/build/formatters/snapshotFormatter.d.ts.map +1 -0
- package/build/formatters/snapshotFormatter.js +156 -0
- package/build/formatters/snapshotFormatter.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +45 -9
- package/build/index.js.map +1 -0
- package/build/server.d.ts +7 -0
- package/build/server.d.ts.map +1 -0
- package/build/server.js +70 -30
- package/build/server.js.map +1 -0
- package/build/tools/ToolDefinition.d.ts +215 -0
- package/build/tools/ToolDefinition.d.ts.map +1 -0
- package/build/tools/ToolDefinition.js +9 -7
- package/build/tools/ToolDefinition.js.map +1 -0
- package/build/tools/assert.d.ts +17 -0
- package/build/tools/assert.d.ts.map +1 -0
- package/build/tools/assert.js +8 -88
- package/build/tools/assert.js.map +1 -0
- package/build/tools/connection.d.ts +13 -0
- package/build/tools/connection.d.ts.map +1 -0
- package/build/tools/connection.js +332 -615
- package/build/tools/connection.js.map +1 -0
- package/build/tools/console.d.ts +20 -0
- package/build/tools/console.d.ts.map +1 -0
- package/build/tools/console.js +162 -152
- package/build/tools/console.js.map +1 -0
- package/build/tools/diagnose.d.ts +22 -0
- package/build/tools/diagnose.d.ts.map +1 -0
- package/build/tools/diagnose.js +400 -13
- package/build/tools/diagnose.js.map +1 -0
- package/build/tools/index.d.ts +6 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +3 -77
- package/build/tools/index.js.map +1 -0
- package/build/tools/input.d.ts +21 -0
- package/build/tools/input.d.ts.map +1 -0
- package/build/tools/input.js +73 -139
- package/build/tools/input.js.map +1 -0
- package/build/tools/navigate.d.ts +21 -0
- package/build/tools/navigate.d.ts.map +1 -0
- package/build/tools/navigate.js +63 -126
- package/build/tools/navigate.js.map +1 -0
- package/build/tools/network.d.ts +17 -0
- package/build/tools/network.d.ts.map +1 -0
- package/build/tools/network.js +75 -887
- package/build/tools/network.js.map +1 -0
- package/build/tools/page.d.ts +13 -0
- package/build/tools/page.d.ts.map +1 -0
- package/build/tools/page.js +4 -1
- package/build/tools/page.js.map +1 -0
- package/build/tools/screenshot.d.ts +9 -0
- package/build/tools/screenshot.d.ts.map +1 -0
- package/build/tools/screenshot.js +3 -1
- package/build/tools/screenshot.js.map +1 -0
- package/build/tools/script.d.ts +6 -0
- package/build/tools/script.d.ts.map +1 -0
- package/build/tools/script.js +92 -0
- package/build/tools/script.js.map +1 -0
- package/build/tools/snapshot.d.ts +9 -0
- package/build/tools/snapshot.d.ts.map +1 -0
- package/build/tools/snapshot.js +78 -12
- package/build/tools/snapshot.js.map +1 -0
- package/build/tools/tools.d.ts +15 -0
- package/build/tools/tools.d.ts.map +1 -0
- package/build/tools/tools.js +62 -0
- package/build/tools/tools.js.map +1 -0
- package/build/tools.d.ts +431 -0
- package/build/tools.d.ts.map +1 -0
- package/build/tools.js +235 -117
- package/build/tools.js.map +1 -0
- package/build/types/errors.d.ts +189 -0
- package/build/types/errors.d.ts.map +1 -0
- package/build/types/errors.js +257 -0
- package/build/types/errors.js.map +1 -0
- package/build/utils/idGenerator.d.ts +21 -0
- package/build/utils/idGenerator.d.ts.map +1 -0
- package/build/utils/idGenerator.js +23 -0
- package/build/utils/idGenerator.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
249
|
+
console.error('[MCP-DEBUG] Mpx检测:', debugInfo);
|
|
222
250
|
// 强制安装 Mpx 拦截器(不检查标志,每次都重新安装以覆盖旧的)
|
|
223
251
|
// 这样可以解决小程序未重新加载导致标志残留的问题
|
|
224
252
|
// @ts-ignore
|
|
225
253
|
if (hasMpxFetch) {
|
|
226
|
-
console.
|
|
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.
|
|
313
|
+
console.error('[MCP] Mpx $xfetch 拦截器安装完成');
|
|
286
314
|
}
|
|
287
315
|
// @ts-ignore
|
|
288
316
|
wx.__networkInterceptorsInstalled = true;
|
|
289
317
|
});
|
|
290
|
-
console.
|
|
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',
|
|
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.
|
|
334
|
-
console.
|
|
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.
|
|
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.
|
|
420
|
+
console.error('⚠️ launchMode 失败,分析错误类型...');
|
|
393
421
|
}
|
|
394
422
|
// 仅在特定可恢复错误时回退到 connectMode
|
|
395
423
|
if (options.fallbackMode && isSessionConflictError(error)) {
|
|
396
424
|
if (options.verbose) {
|
|
397
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
768
|
+
console.error('🔍 检测微信开发者工具运行端口...');
|
|
741
769
|
}
|
|
742
770
|
// 策略1: 尝试常用端口
|
|
743
771
|
for (const port of commonPorts) {
|
|
744
772
|
if (verbose) {
|
|
745
|
-
console.
|
|
773
|
+
console.error(` 检测端口 ${port}...`);
|
|
746
774
|
}
|
|
747
775
|
if (await checkDevToolsRunning(port)) {
|
|
748
776
|
if (verbose) {
|
|
749
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
809
|
+
console.error(' lsof检测失败');
|
|
782
810
|
}
|
|
783
811
|
}
|
|
784
812
|
}
|
|
785
813
|
if (verbose) {
|
|
786
|
-
console.
|
|
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
|
-
|
|
848
|
-
const id = await
|
|
849
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
927
|
+
console.error(`[generateElementUid] Generated UID: ${selector}`);
|
|
861
928
|
return selector;
|
|
862
929
|
}
|
|
863
930
|
catch (error) {
|
|
864
|
-
console.
|
|
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
|
-
|
|
952
|
+
let usedStrategy = 'unknown';
|
|
953
|
+
// 策略1: 优先使用通配符(最快,一次API调用)
|
|
886
954
|
try {
|
|
887
955
|
childElements = await page.$$('*');
|
|
888
|
-
|
|
956
|
+
if (childElements.length > 0) {
|
|
957
|
+
usedStrategy = 'wildcard(*)';
|
|
958
|
+
console.error(`✅ 策略1成功: 通配符查询获取到 ${childElements.length} 个元素`);
|
|
959
|
+
}
|
|
889
960
|
}
|
|
890
961
|
catch (error) {
|
|
891
|
-
console.
|
|
962
|
+
console.warn('⚠️ 策略1失败 (*)', error);
|
|
892
963
|
}
|
|
893
|
-
// 策略2:
|
|
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
|
-
|
|
976
|
+
if (elements.length > 0) {
|
|
977
|
+
console.error(` - ${selector}: ${elements.length} 个元素`);
|
|
978
|
+
}
|
|
905
979
|
}
|
|
906
980
|
catch (error) {
|
|
907
|
-
|
|
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
|
-
|
|
995
|
+
if (childElements.length > 0) {
|
|
996
|
+
usedStrategy = 'hierarchical(page>*)';
|
|
997
|
+
console.error(`✅ 策略3成功: 获取到 ${childElements.length} 个元素`);
|
|
998
|
+
}
|
|
917
999
|
}
|
|
918
1000
|
catch (error) {
|
|
919
|
-
console.
|
|
1001
|
+
console.warn('⚠️ 策略3失败 (page > *)', error);
|
|
920
1002
|
}
|
|
921
1003
|
}
|
|
922
|
-
|
|
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
|
-
|
|
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
|
|
1070
|
+
tagName,
|
|
932
1071
|
};
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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.
|
|
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.
|
|
1167
|
+
console.error(`[Click] 点击前页面: ${beforePath}`);
|
|
1057
1168
|
// 执行点击操作
|
|
1058
1169
|
await element.tap();
|
|
1059
|
-
console.
|
|
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.
|
|
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.
|
|
1182
|
+
console.error(`[Click] 点击后页面: ${afterPath}`);
|
|
1072
1183
|
if (beforePath !== afterPath) {
|
|
1073
|
-
console.
|
|
1184
|
+
console.error(`[Click] ✅ 页面已切换: ${beforePath} → ${afterPath}`);
|
|
1074
1185
|
}
|
|
1075
1186
|
else {
|
|
1076
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1241
|
+
console.error(`截图保存成功: ${path}`);
|
|
1126
1242
|
break;
|
|
1127
1243
|
}
|
|
1128
1244
|
else {
|
|
1129
1245
|
// 返回base64数据
|
|
1130
1246
|
const base64Data = await miniProgram.screenshot();
|
|
1131
|
-
console.
|
|
1247
|
+
console.error('截图API调用完成,检查返回数据...');
|
|
1132
1248
|
if (base64Data && typeof base64Data === 'string' && base64Data.length > 0) {
|
|
1249
|
+
screenshotSucceeded = true;
|
|
1133
1250
|
result = base64Data;
|
|
1134
|
-
console.
|
|
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.
|
|
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 (!
|
|
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
|