android-midscene-automation 0.1.37 → 0.1.38
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/CHANGELOG.md +5 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/server/appium-recorder/ai-recognition.ts +4 -2
- package/server/appium-recorder/appium-runner.ts +40 -21
- package/server/appium-recorder/condition-timeout.ts +4 -0
- package/server/appium-recorder/image-check.ts +6 -2
- package/server/appium-recorder/report.ts +1 -1
- package/server/appium-recorder/repository.ts +1 -0
- package/src/appium-recorder/AppiumPage.vue +19 -58
- package/src/appium-recorder/components/BranchTimeoutSettings.vue +23 -0
- package/src/appium-recorder/components/FlowStepEditor.vue +6 -15
- package/src/appium-recorder/components/ImageCheckDialog.vue +11 -5
- package/src/appium-recorder/image-check.ts +3 -1
- package/src/appium-recorder/node-timeout.ts +1 -0
- package/src/appium-recorder/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
# 更新记录
|
|
2
|
+
## v0.1.38
|
|
3
|
+
- 移除节点类型切换;新节点及未配置的判断等待超时统一为 3000ms,已有自定义时间和延时、长按等操作时长保持不变。
|
|
4
|
+
- 取消 Activity 不匹配时的脚本编辑锁定,不在脚本绑定页面也可编辑节点;页面差异仅作提示,保留录制操作及回放期间的编辑保护。
|
|
5
|
+
- 所有分支节点新增“超时后处理”,可预设终止流程、进入左侧或右侧分支,默认终止;覆盖元素等待、文字匹配、勾选控件等待、AI 识别及图像观察超时,日志和 HTML 报告标明超时分支。正常 false、循环次数上限与超时区分处理,设备断开及 Appium 无响应仍终止。
|
|
6
|
+
|
|
2
7
|
## v0.1.37
|
|
3
8
|
- 修复元素查找超时未覆盖 Appium 请求的问题,驱动无响应时及时中止并明确报错,不再误判为找不到元素;唯一原生定位优先使用 ID 或 Accessibility ID。
|
|
4
9
|
- 设备预览新增设备信息,展示设备名称、品牌、型号、处理器、Android 版本、物理分辨率及当前分辨率,支持远程设备;问号位于标题右侧,样式与全局变量帮助图标一致。
|
package/README.md
CHANGED
|
@@ -1229,6 +1229,11 @@ Linux: ~/.local/share/android-midscene-automation
|
|
|
1229
1229
|
|
|
1230
1230
|
# 项目更新记录
|
|
1231
1231
|
|
|
1232
|
+
## v0.1.38
|
|
1233
|
+
- 移除节点类型切换;新节点及未配置的判断等待超时统一为 3000ms,已有自定义时间和延时、长按等操作时长保持不变。
|
|
1234
|
+
- 取消 Activity 不匹配时的脚本编辑锁定,不在脚本绑定页面也可编辑节点;页面差异仅作提示,保留录制操作及回放期间的编辑保护。
|
|
1235
|
+
- 所有分支节点新增“超时后处理”,可预设终止流程、进入左侧或右侧分支,默认终止;覆盖元素等待、文字匹配、勾选控件等待、AI 识别及图像观察超时,日志和 HTML 报告标明超时分支。正常 false、循环次数上限与超时区分处理,设备断开及 Appium 无响应仍终止。
|
|
1236
|
+
|
|
1232
1237
|
## v0.1.37
|
|
1233
1238
|
- 修复元素查找超时未覆盖 Appium 请求的问题,驱动无响应时及时中止并明确报错,不再误判为找不到元素;唯一原生定位优先使用 ID 或 Accessibility ID。
|
|
1234
1239
|
- 设备预览新增设备信息,展示设备名称、品牌、型号、处理器、Android 版本、物理分辨率及当前分辨率,支持远程设备;问号位于标题右侧,样式与全局变量帮助图标一致。
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
+
import { DEFAULT_NODE_TIMEOUT_MS } from '../../src/appium-recorder/node-timeout';
|
|
3
|
+
import { ConditionTimeoutError } from './condition-timeout';
|
|
2
4
|
import { loadConfig } from '../config';
|
|
3
5
|
import { adbScreenshotBase64 } from './screenshot';
|
|
4
6
|
import {
|
|
@@ -35,7 +37,7 @@ export async function recognizeDeviceScreen(input: {
|
|
|
35
37
|
if (!['http:', 'https:'].includes(baseURL.protocol)) throw new Error('AI 识别模型 Base URL 必须为 HTTP(S) 地址');
|
|
36
38
|
const startedAt = Date.now();
|
|
37
39
|
const timeoutMs = Number.isFinite(input.timeoutMs) && input.timeoutMs! > 0
|
|
38
|
-
? Math.min(300000, Math.max(1000, input.timeoutMs!)) :
|
|
40
|
+
? Math.min(300000, Math.max(1000, input.timeoutMs!)) : DEFAULT_NODE_TIMEOUT_MS;
|
|
39
41
|
const deadline = AbortSignal.timeout(timeoutMs);
|
|
40
42
|
const signal = input.signal ? AbortSignal.any([input.signal, deadline]) : deadline;
|
|
41
43
|
signal.throwIfAborted();
|
|
@@ -59,7 +61,7 @@ export async function recognizeDeviceScreen(input: {
|
|
|
59
61
|
return { ...parseAiRecognitionResult(choice.message.content), durationMs: Date.now() - startedAt, imageBase64 };
|
|
60
62
|
} catch (error) {
|
|
61
63
|
if (input.signal?.aborted) throw input.signal.reason;
|
|
62
|
-
if (deadline.aborted) throw new
|
|
64
|
+
if (deadline.aborted) throw new ConditionTimeoutError(`AI 识别超时(${timeoutMs}ms)`);
|
|
63
65
|
// 不透传模型服务的原始响应,避免 API Key 或敏感请求信息进入报告。
|
|
64
66
|
if (error instanceof OpenAI.APIError) throw new Error(`AI 识别模型请求失败(HTTP ${error.status || '连接异常'}),请检查模型配置及图片输入支持`);
|
|
65
67
|
throw error;
|
|
@@ -12,6 +12,8 @@ import { IMAGE_CHECK_MODES, type ImageCheckResult } from '../../src/appium-recor
|
|
|
12
12
|
import { formatStageLog } from '../../src/appium-recorder/stage-log';
|
|
13
13
|
import { openGalleryOnDevice } from './open-gallery';
|
|
14
14
|
import { stopAppOnDevice } from './stop-app';
|
|
15
|
+
import { DEFAULT_NODE_TIMEOUT_MS } from '../../src/appium-recorder/node-timeout';
|
|
16
|
+
import { ConditionTimeoutError, AppiumServiceError } from './condition-timeout';
|
|
15
17
|
import { AppiumRequestTimeoutError, timedAppiumFetch, withElementDeadline } from './request-timeout';
|
|
16
18
|
import { textClickSelector } from '../../src/appium-recorder/text-click';
|
|
17
19
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -86,7 +88,7 @@ function isReplayStopped(error?: unknown) {
|
|
|
86
88
|
function throwIfReplayStopped(error?: unknown) {
|
|
87
89
|
if (isReplayStopped(error)) throw new ReplayStoppedError();
|
|
88
90
|
// 驱动失去响应不是“元素不存在”,不能被备用定位或可选步骤吞掉。
|
|
89
|
-
if (error instanceof AppiumRequestTimeoutError) throw error;
|
|
91
|
+
if (error instanceof AppiumRequestTimeoutError || error instanceof AppiumServiceError) throw error;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
function errorDetail(error: unknown) {
|
|
@@ -209,16 +211,17 @@ function adbSwipe(deviceId: string, step: Required<AppiumRecordedStepRecord>['sw
|
|
|
209
211
|
});
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
function adbText(deviceId: string, args: string[]) {
|
|
213
|
-
return new Promise<string>((resolve) => {
|
|
214
|
-
execFile(getAdbCommand(), ['-s', deviceId, ...args], { maxBuffer: 4 * 1024 * 1024 }, (
|
|
214
|
+
function adbText(deviceId: string, args: string[], strict = false) {
|
|
215
|
+
return new Promise<string>((resolve, reject) => {
|
|
216
|
+
execFile(getAdbCommand(), ['-s', deviceId, ...args], { maxBuffer: 4 * 1024 * 1024, timeout: 10000, signal: replayContext.getStore()?.signal }, (error, stdout, stderr) => {
|
|
217
|
+
if (strict && error) { reject(new AppiumServiceError(`读取设备状态失败:${stderr || error.message}`)); return; }
|
|
215
218
|
resolve((stdout || stderr || '').trim());
|
|
216
219
|
});
|
|
217
220
|
});
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
async function getCurrentActivity(deviceId: string) {
|
|
221
|
-
const output = await adbText(deviceId, ['shell', 'dumpsys', 'activity', 'activities']);
|
|
224
|
+
const output = await adbText(deviceId, ['shell', 'dumpsys', 'activity', 'activities'], true);
|
|
222
225
|
const resumedLine = output
|
|
223
226
|
.split(/\r?\n/)
|
|
224
227
|
.find((line) => /(?:topResumedActivity|ResumedActivity|mResumedActivity)/.test(line));
|
|
@@ -235,7 +238,7 @@ async function waitForActivity(deviceId: string, step: AppiumRecordedStepRecord)
|
|
|
235
238
|
const expectedActivity = step.value || '';
|
|
236
239
|
if (!expectedActivity) throw new Error(`${step.label} 缺少目标 Activity`);
|
|
237
240
|
const startedAt = Date.now();
|
|
238
|
-
const timeoutMs = step.timeoutMs
|
|
241
|
+
const timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
239
242
|
let currentActivity = '';
|
|
240
243
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
241
244
|
throwIfReplayStopped();
|
|
@@ -243,7 +246,7 @@ async function waitForActivity(deviceId: string, step: AppiumRecordedStepRecord)
|
|
|
243
246
|
if (currentActivity === expectedActivity) return;
|
|
244
247
|
await wait(500);
|
|
245
248
|
}
|
|
246
|
-
throw new
|
|
249
|
+
throw new ConditionTimeoutError(`${step.label} 等待超时,当前 Activity:${currentActivity || '-'}`);
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
async function appendSettingsDiagnostics(lines: string[], deviceId: string) {
|
|
@@ -300,9 +303,9 @@ async function appiumRequest<T>(
|
|
|
300
303
|
context?.appiumLog(`[Appium] 异常:${method} ${path}(${errorDetail(error)})`);
|
|
301
304
|
if (error instanceof AppiumRequestTimeoutError) throw error;
|
|
302
305
|
if (isTimeoutError(error)) {
|
|
303
|
-
throw new
|
|
306
|
+
throw new AppiumServiceError(`Appium 请求超时 ${requestUrl},${errorDetail(error)}`);
|
|
304
307
|
}
|
|
305
|
-
throw new
|
|
308
|
+
throw new AppiumServiceError(`无法连接 Appium 服务 ${requestUrl},${errorDetail(error)}`);
|
|
306
309
|
}
|
|
307
310
|
|
|
308
311
|
context?.appiumLog(`[Appium] 响应:HTTP ${response.status} ${method} ${path}(${Date.now() - startedAt}ms)`);
|
|
@@ -325,7 +328,8 @@ async function appiumRequest<T>(
|
|
|
325
328
|
payload.value?.stacktrace || payload.stacktrace ? `堆栈信息:\n${payload.value?.stacktrace || payload.stacktrace}` : '',
|
|
326
329
|
responseText ? `原始响应:\n${responseText}` : '',
|
|
327
330
|
].filter(Boolean).join('\n');
|
|
328
|
-
|
|
331
|
+
const ErrorType = ['no such element', 'stale element reference'].includes(payload.value?.error || payload.error || '') ? Error : AppiumServiceError;
|
|
332
|
+
throw new ErrorType(`Appium ${method} ${path} 失败(HTTP ${response.status}):\n${detail || '未返回错误详情'}`);
|
|
329
333
|
}
|
|
330
334
|
return payload;
|
|
331
335
|
}
|
|
@@ -444,7 +448,7 @@ async function findElementBySelector(
|
|
|
444
448
|
return elementId;
|
|
445
449
|
}
|
|
446
450
|
|
|
447
|
-
async function findElement(sessionId: string, step: AppiumRecordedStepRecord, timeoutMs = step.timeoutMs ??
|
|
451
|
+
async function findElement(sessionId: string, step: AppiumRecordedStepRecord, timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS) {
|
|
448
452
|
return withElementDeadline(timeoutMs, () => findElementWithinDeadline(sessionId, step));
|
|
449
453
|
}
|
|
450
454
|
|
|
@@ -513,7 +517,7 @@ async function tapFallback(deviceId: string, step: AppiumRecordedStepRecord) {
|
|
|
513
517
|
|
|
514
518
|
async function waitForElement(sessionId: string, step: AppiumRecordedStepRecord) {
|
|
515
519
|
const startedAt = Date.now();
|
|
516
|
-
const timeoutMs = step.timeoutMs
|
|
520
|
+
const timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
517
521
|
let lastError: unknown;
|
|
518
522
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
519
523
|
try {
|
|
@@ -524,12 +528,12 @@ async function waitForElement(sessionId: string, step: AppiumRecordedStepRecord)
|
|
|
524
528
|
await wait(500);
|
|
525
529
|
}
|
|
526
530
|
}
|
|
527
|
-
throw
|
|
531
|
+
throw new ConditionTimeoutError(`${step.label} 等待元素超时(${timeoutMs}ms):${errorDetail(lastError)}`);
|
|
528
532
|
}
|
|
529
533
|
|
|
530
534
|
async function findOptionalElement(sessionId: string, step: AppiumRecordedStepRecord) {
|
|
531
535
|
const startedAt = Date.now();
|
|
532
|
-
const timeoutMs = step.timeoutMs ??
|
|
536
|
+
const timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
533
537
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
534
538
|
try {
|
|
535
539
|
return await findElement(sessionId, step, Math.max(1, timeoutMs - (Date.now() - startedAt)));
|
|
@@ -543,7 +547,7 @@ async function findOptionalElement(sessionId: string, step: AppiumRecordedStepRe
|
|
|
543
547
|
|
|
544
548
|
async function waitForElementGone(sessionId: string, step: AppiumRecordedStepRecord) {
|
|
545
549
|
const startedAt = Date.now();
|
|
546
|
-
const timeoutMs = step.timeoutMs
|
|
550
|
+
const timeoutMs = step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS;
|
|
547
551
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
548
552
|
try {
|
|
549
553
|
await findElement(sessionId, step, Math.max(1, timeoutMs - (Date.now() - startedAt)));
|
|
@@ -553,7 +557,7 @@ async function waitForElementGone(sessionId: string, step: AppiumRecordedStepRec
|
|
|
553
557
|
}
|
|
554
558
|
await wait(500);
|
|
555
559
|
}
|
|
556
|
-
throw new
|
|
560
|
+
throw new ConditionTimeoutError(`${step.label} 等待消失超时`);
|
|
557
561
|
}
|
|
558
562
|
|
|
559
563
|
async function saveScreenshot(deviceId: string) {
|
|
@@ -998,7 +1002,7 @@ async function evaluateCondition(sessionId: string, deviceId: string, step: Appi
|
|
|
998
1002
|
}
|
|
999
1003
|
if (step.type === 'textClick') {
|
|
1000
1004
|
const selector = textClickSelector(step);
|
|
1001
|
-
const deadline = Date.now() + Math.max(0, step.timeoutMs ??
|
|
1005
|
+
const deadline = Date.now() + Math.max(0, step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS);
|
|
1002
1006
|
// 查询空结果才是未匹配;通信错误、重复匹配和点击失败走现有异常处理。
|
|
1003
1007
|
do {
|
|
1004
1008
|
const payload = await appiumRequest<AppiumValueResponse<Record<string, string>[]>>(`/session/${sessionId}/elements`, {
|
|
@@ -1013,7 +1017,10 @@ async function evaluateCondition(sessionId: string, deviceId: string, step: Appi
|
|
|
1013
1017
|
await appiumRequest(`/session/${sessionId}/element/${id}/click`, { method: 'POST', body: '{}' });
|
|
1014
1018
|
return true;
|
|
1015
1019
|
}
|
|
1016
|
-
if (Date.now() >= deadline)
|
|
1020
|
+
if (Date.now() >= deadline) {
|
|
1021
|
+
if (step.timeoutMs === 0) return false;
|
|
1022
|
+
throw new ConditionTimeoutError(`文字点击等待匹配超时(${step.timeoutMs ?? DEFAULT_NODE_TIMEOUT_MS}ms)`);
|
|
1023
|
+
}
|
|
1017
1024
|
await wait(Math.max(0, Math.min(500, deadline - Date.now())));
|
|
1018
1025
|
} while (true);
|
|
1019
1026
|
}
|
|
@@ -1029,7 +1036,7 @@ async function evaluateCondition(sessionId: string, deviceId: string, step: Appi
|
|
|
1029
1036
|
return true;
|
|
1030
1037
|
}
|
|
1031
1038
|
if (step.type === 'assertText' || (step.type === 'assertExists' && step.value)) {
|
|
1032
|
-
const elementId = await
|
|
1039
|
+
const elementId = await waitForElement(sessionId, step);
|
|
1033
1040
|
const payload = await appiumRequest<AppiumValueResponse<string>>(`/session/${sessionId}/element/${elementId}/text`);
|
|
1034
1041
|
const actualText = payload.value || '';
|
|
1035
1042
|
const expectedText = step.value || '';
|
|
@@ -1041,7 +1048,7 @@ async function evaluateCondition(sessionId: string, deviceId: string, step: Appi
|
|
|
1041
1048
|
return true;
|
|
1042
1049
|
} catch (error) {
|
|
1043
1050
|
throwIfReplayStopped(error);
|
|
1044
|
-
|
|
1051
|
+
throw error;
|
|
1045
1052
|
}
|
|
1046
1053
|
}
|
|
1047
1054
|
|
|
@@ -1117,7 +1124,7 @@ async function replayLinkedScript(
|
|
|
1117
1124
|
type: 'waitActivity',
|
|
1118
1125
|
label: step.label,
|
|
1119
1126
|
value: linkedScript.appActivity,
|
|
1120
|
-
timeoutMs:
|
|
1127
|
+
timeoutMs: DEFAULT_NODE_TIMEOUT_MS,
|
|
1121
1128
|
});
|
|
1122
1129
|
} catch (error) {
|
|
1123
1130
|
throwIfReplayStopped(error);
|
|
@@ -1270,6 +1277,7 @@ async function replayFlowSteps(
|
|
|
1270
1277
|
replayContext.getStore()?.imageChecks.push({ ...imageResult, nodeId: step.id, nodeNumber: index + 1, nodeLabel: frameStep.label, scriptName: options.scriptName || '', settings });
|
|
1271
1278
|
lines.push(`[节点 ${index + 1}] 图像判断:${imageResult.result === null ? '无法判定' : imageResult.result},${imageResult.message};${settings};采样 ${imageResult.sampleCount} 帧,耗时 ${imageResult.durationMs}ms;指标 ${JSON.stringify(imageResult.metrics)}`);
|
|
1272
1279
|
if (imageResult.result === null) throw new Error(imageResult.message);
|
|
1280
|
+
if (imageResult.timedOut) throw new ConditionTimeoutError(imageResult.message);
|
|
1273
1281
|
}
|
|
1274
1282
|
const matched = imageResult ? imageResult.result! : recognition ? recognition.result : await evaluateCondition(sessionId, deviceId, step);
|
|
1275
1283
|
if (recognition) lines.push(`[节点 ${index + 1}] AI 识别:${recognition.result},耗时 ${recognition.durationMs}ms${recognition.reason ? `,依据:${recognition.reason}` : ''}`);
|
|
@@ -1294,6 +1302,17 @@ async function replayFlowSteps(
|
|
|
1294
1302
|
await captureReplayFrame(sessionId, step, index + 1, options.scriptName || '', 'stopped', '已终止');
|
|
1295
1303
|
throw new ReplayStoppedError();
|
|
1296
1304
|
}
|
|
1305
|
+
if (error instanceof ConditionTimeoutError && (step.timeoutBranch === 'yes' || step.timeoutBranch === 'no')) {
|
|
1306
|
+
const branch = step.timeoutBranch;
|
|
1307
|
+
const status = `判断超时,按配置进入${branch === 'yes' ? '左' : '右'}侧分支(${flowBranchLabel(step, branch)})`;
|
|
1308
|
+
lines.push(`[节点 ${index + 1}] ${status};${error.message}`);
|
|
1309
|
+
await captureReplayFrame(sessionId, frameStep, index + 1, options.scriptName || '', 'after', status);
|
|
1310
|
+
// 循环体必须经过 enter,才能正确记轮次及受最大次数保护。
|
|
1311
|
+
index = step.type === 'loop'
|
|
1312
|
+
? branch === 'yes' ? traversal.enter(step).next : traversal.exit(step)
|
|
1313
|
+
: traversal.branch(step, branch === 'yes');
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1297
1316
|
lines.push(`[节点 ${index + 1}] 失败:${errorDetail(error)}`);
|
|
1298
1317
|
await captureReplayFrame(sessionId, step, index + 1, options.scriptName || '', 'error', '失败');
|
|
1299
1318
|
await appendStepDiagnostics(lines, deviceId, step);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { PNG } from 'pngjs';
|
|
2
|
+
import { AppiumRequestTimeoutError } from './request-timeout';
|
|
3
|
+
import { AppiumServiceError } from './condition-timeout';
|
|
2
4
|
import { validateImageCheck, type ImageCheckConfig, type ImageCheckResult } from '../../src/appium-recorder/image-check';
|
|
3
5
|
import type { AppiumVisualChangeRegion } from '../../src/appium-recorder/types';
|
|
4
6
|
|
|
@@ -138,20 +140,22 @@ export async function checkImage(input: {
|
|
|
138
140
|
const changed = maximum >= config.ratio;
|
|
139
141
|
if (changed || (result.sampleCount >= 2 && now() - observedAt >= config.durationMs)) {
|
|
140
142
|
result.result = config.expectation === 'present' ? changed : !changed;
|
|
143
|
+
result.timedOut = !changed && config.expectation === 'present' && config.durationMs > 0;
|
|
141
144
|
break;
|
|
142
145
|
}
|
|
143
146
|
} else {
|
|
144
147
|
consecutive = measurement.matched ? consecutive + 1 : 0;
|
|
145
148
|
result.metrics['连续满足帧数'] = consecutive;
|
|
146
149
|
if (consecutive >= config.consecutive) { result.result = true; break; }
|
|
147
|
-
if (now() - observedAt >= config.durationMs) { result.result = false; break; }
|
|
150
|
+
if (now() - observedAt >= config.durationMs) { result.result = false; result.timedOut = config.durationMs > 0; break; }
|
|
148
151
|
}
|
|
149
152
|
await input.wait(Math.min(config.intervalMs, Math.max(1, config.durationMs - (now() - observedAt))));
|
|
150
153
|
}
|
|
151
154
|
if (last) keep('判定帧', last);
|
|
152
|
-
result.message = result.result ? '条件成立' : '条件不成立';
|
|
155
|
+
result.message = result.timedOut ? '观察超时,未在规定时间内满足条件' : result.result ? '条件成立' : '条件不成立';
|
|
153
156
|
} catch (error) {
|
|
154
157
|
if (input.signal?.aborted) throw error;
|
|
158
|
+
if (error instanceof AppiumRequestTimeoutError || error instanceof AppiumServiceError) throw error;
|
|
155
159
|
result.message = `无法判定:${error instanceof Error ? error.message : '图像检测异常'}`;
|
|
156
160
|
result.result = null;
|
|
157
161
|
}
|
|
@@ -605,7 +605,7 @@ function createReplayHtml(input: {
|
|
|
605
605
|
article.append(title);
|
|
606
606
|
const detail = document.createElement('dl');
|
|
607
607
|
const region = check.region || {};
|
|
608
|
-
const entries = [['结果', check.result === null ? '无法判定' : String(check.result)], ['配置', check.settings],
|
|
608
|
+
const entries = [['结果', check.timedOut ? '观察超时' : check.result === null ? '无法判定' : String(check.result)], ['配置', check.settings],
|
|
609
609
|
['区域', [region.x, region.y, region.width, region.height].join(', ')],
|
|
610
610
|
['采样', check.sampleCount + ' 帧'], ['耗时', check.durationMs + 'ms'], ['说明', check.message], ...Object.entries(check.metrics)];
|
|
611
611
|
entries.forEach(([key, value]) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, h, onMounted, onUnmounted, provide, reactive, shallowRef, watch } from 'vue';
|
|
3
3
|
import { flowBackgroundKey, normalizeFlowBackground } from './flow-appearance';
|
|
4
|
+
import { DEFAULT_NODE_TIMEOUT_MS } from './node-timeout';
|
|
4
5
|
import { ElForm, ElFormItem, ElInputNumber, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus';
|
|
5
6
|
import { Check, CircleClose, CopyDocument, Delete, Document, Download, Edit, Plus, Refresh, Upload, VideoPlay, View, Clock } from '@element-plus/icons-vue';
|
|
6
7
|
import RunHistoryDialog from './RunHistoryDialog.vue';
|
|
@@ -95,47 +96,6 @@ const workspaceRef = shallowRef<InstanceType<typeof RecorderWorkspace>>();
|
|
|
95
96
|
const historyScript = shallowRef<{ id: string; name: string } | null>(null);
|
|
96
97
|
const AUTO_TREE_REFRESH_INTERVAL_MS = 1800;
|
|
97
98
|
|
|
98
|
-
const insertableRecorderActions: RecorderAction[] = [
|
|
99
|
-
'extractVariable',
|
|
100
|
-
'delay',
|
|
101
|
-
'tap',
|
|
102
|
-
'input',
|
|
103
|
-
'waitFor',
|
|
104
|
-
'tapIfExists',
|
|
105
|
-
'inputIfExists',
|
|
106
|
-
'clearIfExists',
|
|
107
|
-
'backIfExists',
|
|
108
|
-
'popupCondition',
|
|
109
|
-
'checkboxState',
|
|
110
|
-
'checkedState',
|
|
111
|
-
'radioButtonState',
|
|
112
|
-
'aiRecognition',
|
|
113
|
-
'imageCheck',
|
|
114
|
-
'textClick',
|
|
115
|
-
'runScript',
|
|
116
|
-
'keyBack',
|
|
117
|
-
'keyHome',
|
|
118
|
-
'keyRecent',
|
|
119
|
-
'keyPower',
|
|
120
|
-
'waitActivity',
|
|
121
|
-
'swipe',
|
|
122
|
-
'clearInput',
|
|
123
|
-
'coordinateTap',
|
|
124
|
-
'launchApp',
|
|
125
|
-
'stopApp',
|
|
126
|
-
'openGallery',
|
|
127
|
-
'endFlow',
|
|
128
|
-
'loop',
|
|
129
|
-
'breakLoop',
|
|
130
|
-
'clearAppData',
|
|
131
|
-
'waitDisappear',
|
|
132
|
-
'longPress',
|
|
133
|
-
'pinch',
|
|
134
|
-
'noop',
|
|
135
|
-
'log',
|
|
136
|
-
'visualChangeStart',
|
|
137
|
-
'visualChangeEnd',
|
|
138
|
-
];
|
|
139
99
|
const readonlyFlowActionGroups: FlowActionGroup[] = [];
|
|
140
100
|
const readonlyFlowSelectedIndexes: number[] = [];
|
|
141
101
|
|
|
@@ -294,7 +254,7 @@ const recordingBusy = computed(() => (
|
|
|
294
254
|
|| resolvingNavigation.value
|
|
295
255
|
|| Boolean(pendingNavigation.value)
|
|
296
256
|
));
|
|
297
|
-
const recordingLocked = computed(() => recordingBusy.value ||
|
|
257
|
+
const recordingLocked = computed(() => recordingBusy.value || replaying.value);
|
|
298
258
|
const newScriptDisabled = computed(() => saving.value || replaying.value || recordingBusy.value || launchingApp.value || importingScript.value);
|
|
299
259
|
const overlayBounds = computed(() => flattenNodes(tree.value).flatMap((node) => (
|
|
300
260
|
node.bounds ? [{ id: node.id, ...node.bounds }] : []
|
|
@@ -510,7 +470,7 @@ function createStep(type: NodeStepType, node: AppiumNode): AppiumRecordedStep {
|
|
|
510
470
|
? [{ strategy: 'xpath', value: node.xpath, unique: true, matchCount: 1 }]
|
|
511
471
|
: undefined,
|
|
512
472
|
fallback: node.bounds ? { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY } : undefined,
|
|
513
|
-
timeoutMs: type === 'waitFor' ?
|
|
473
|
+
timeoutMs: type === 'waitFor' ? DEFAULT_NODE_TIMEOUT_MS : undefined,
|
|
514
474
|
pageBefore: currentPageSnapshot(node),
|
|
515
475
|
snapshot: {
|
|
516
476
|
text: node.text,
|
|
@@ -614,7 +574,7 @@ function createWaitActivityStep(activity: string): AppiumRecordedStep {
|
|
|
614
574
|
type: 'waitActivity',
|
|
615
575
|
label: `等待 Activity ${activity}`,
|
|
616
576
|
value: activity,
|
|
617
|
-
timeoutMs:
|
|
577
|
+
timeoutMs: DEFAULT_NODE_TIMEOUT_MS,
|
|
618
578
|
};
|
|
619
579
|
}
|
|
620
580
|
|
|
@@ -976,7 +936,7 @@ async function executeFlowStep(index: number) {
|
|
|
976
936
|
} else if (scriptActivityMismatch.value) {
|
|
977
937
|
ElMessage.warning(`App 已启动,当前页面仍为 ${currentActivity.value || '-'}`);
|
|
978
938
|
} else {
|
|
979
|
-
ElMessage.success('
|
|
939
|
+
ElMessage.success('已进入脚本绑定页面');
|
|
980
940
|
}
|
|
981
941
|
} catch (error) {
|
|
982
942
|
ElMessage.error(error instanceof Error ? error.message : 'App 操作失败');
|
|
@@ -1443,13 +1403,13 @@ async function addAction(
|
|
|
1443
1403
|
if (!input) return;
|
|
1444
1404
|
return insertStep({
|
|
1445
1405
|
id: createStepId(), type: 'aiRecognition', label: 'AI 识别',
|
|
1446
|
-
value: validateAiRecognitionPrompt(input.value), timeoutMs:
|
|
1406
|
+
value: validateAiRecognitionPrompt(input.value), timeoutMs: DEFAULT_NODE_TIMEOUT_MS,
|
|
1447
1407
|
flow: { nodeKind: 'condition' },
|
|
1448
1408
|
}, index, branchTarget);
|
|
1449
1409
|
}
|
|
1450
1410
|
if (action === 'textClick') {
|
|
1451
1411
|
const step = reactive<AppiumRecordedStep>({
|
|
1452
|
-
id: createStepId(), type: 'textClick', label: '文字点击', value: '', timeoutMs:
|
|
1412
|
+
id: createStepId(), type: 'textClick', label: '文字点击', value: '', timeoutMs: DEFAULT_NODE_TIMEOUT_MS,
|
|
1453
1413
|
flow: { nodeKind: 'condition', textMatch: 'exact' },
|
|
1454
1414
|
});
|
|
1455
1415
|
const result = await ElMessageBox({
|
|
@@ -1617,7 +1577,7 @@ async function addAction(
|
|
|
1617
1577
|
ElMessage.warning(`请选择原生 ${controlName} 元素,当前选中:${node.className || '未知类型'}`);
|
|
1618
1578
|
return;
|
|
1619
1579
|
}
|
|
1620
|
-
const step = createNodeActionStep(action, action === 'checkedState' ? '判断勾选' : `判断 ${controlName} 状态`, node, { timeoutMs:
|
|
1580
|
+
const step = createNodeActionStep(action, action === 'checkedState' ? '判断勾选' : `判断 ${controlName} 状态`, node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS });
|
|
1621
1581
|
if ((!step.selector?.value || step.selector.strategy === 'bounds') && node.xpath) {
|
|
1622
1582
|
step.selector = { strategy: 'xpath', value: node.xpath };
|
|
1623
1583
|
step.contextSelector = undefined;
|
|
@@ -1633,13 +1593,13 @@ async function addAction(
|
|
|
1633
1593
|
return insertStep(step, index, branchTarget);
|
|
1634
1594
|
}
|
|
1635
1595
|
if (action === 'popupCondition') {
|
|
1636
|
-
const step = createNodeActionStep('assertExists', '判断存在', node, { timeoutMs:
|
|
1596
|
+
const step = createNodeActionStep('assertExists', '判断存在', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS });
|
|
1637
1597
|
const inserted = insertStep({ ...step, flow: { nodeKind: 'condition' } }, index, branchTarget);
|
|
1638
1598
|
ElMessage.success('已添加判断节点,请在节点面板配置是/否分支');
|
|
1639
1599
|
return inserted;
|
|
1640
1600
|
}
|
|
1641
1601
|
if (action === 'tapIfExists') {
|
|
1642
|
-
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs:
|
|
1602
|
+
return insertStep(createNodeActionStep('tapIfExists', '存在则点击', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS }), index, branchTarget);
|
|
1643
1603
|
}
|
|
1644
1604
|
if (action === 'inputIfExists') {
|
|
1645
1605
|
const input = await ElMessageBox.prompt('', '存在则输入', {
|
|
@@ -1650,16 +1610,16 @@ async function addAction(
|
|
|
1650
1610
|
}).catch(() => null);
|
|
1651
1611
|
if (!input) return;
|
|
1652
1612
|
return insertStep(
|
|
1653
|
-
createNodeActionStep('inputIfExists', '存在则输入', node, { timeoutMs:
|
|
1613
|
+
createNodeActionStep('inputIfExists', '存在则输入', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS, value: input.value }),
|
|
1654
1614
|
index,
|
|
1655
1615
|
branchTarget,
|
|
1656
1616
|
);
|
|
1657
1617
|
}
|
|
1658
1618
|
if (action === 'clearIfExists') {
|
|
1659
|
-
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs:
|
|
1619
|
+
return insertStep(createNodeActionStep('clearIfExists', '存在则清空', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS }), index, branchTarget);
|
|
1660
1620
|
}
|
|
1661
1621
|
if (action === 'backIfExists') {
|
|
1662
|
-
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs:
|
|
1622
|
+
return insertStep(createNodeActionStep('backIfExists', '存在则返回', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS }), index, branchTarget);
|
|
1663
1623
|
}
|
|
1664
1624
|
if (action === 'clearInput') {
|
|
1665
1625
|
return insertStep(createNodeActionStep('clearInput', '清空输入', node), index, branchTarget);
|
|
@@ -1706,7 +1666,7 @@ async function addAction(
|
|
|
1706
1666
|
return insertStep({ ...step }, index, branchTarget);
|
|
1707
1667
|
}
|
|
1708
1668
|
if (action === 'waitDisappear') {
|
|
1709
|
-
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs:
|
|
1669
|
+
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: DEFAULT_NODE_TIMEOUT_MS }), index, branchTarget);
|
|
1710
1670
|
}
|
|
1711
1671
|
}
|
|
1712
1672
|
|
|
@@ -2287,7 +2247,7 @@ watch(
|
|
|
2287
2247
|
v-if="scriptActivityMismatch"
|
|
2288
2248
|
class="appium-activity-summary"
|
|
2289
2249
|
>
|
|
2290
|
-
<summary
|
|
2250
|
+
<summary>当前页面与脚本绑定的 Activity 不一致</summary>
|
|
2291
2251
|
<div class="appium-activity-lock-alert__details">
|
|
2292
2252
|
<div>
|
|
2293
2253
|
<strong>脚本绑定</strong>
|
|
@@ -2297,7 +2257,7 @@ watch(
|
|
|
2297
2257
|
<strong>当前 Activity</strong>
|
|
2298
2258
|
<code>{{ currentActivity || '正在获取' }}</code>
|
|
2299
2259
|
</div>
|
|
2300
|
-
<p
|
|
2260
|
+
<p>不影响脚本编辑;如需录制当前页面的组件操作,请确认设备页面及所选组件。</p>
|
|
2301
2261
|
</div>
|
|
2302
2262
|
</details>
|
|
2303
2263
|
</section>
|
|
@@ -2309,9 +2269,8 @@ watch(
|
|
|
2309
2269
|
:steps="steps"
|
|
2310
2270
|
:clipboard-count="flowClipboardCount"
|
|
2311
2271
|
:disabled="recordingLocked"
|
|
2312
|
-
:remove-disabled="
|
|
2272
|
+
:remove-disabled="recordingLocked"
|
|
2313
2273
|
:merge-disabled="recordingBusy || replaying || saving"
|
|
2314
|
-
:allowed-locked-actions="scriptActivityMismatch && !recordingBusy ? insertableRecorderActions : []"
|
|
2315
2274
|
:launching-step-id="executingAppStepId"
|
|
2316
2275
|
@remove="removeStep"
|
|
2317
2276
|
@copy="copyFlowNodes"
|
|
@@ -2434,6 +2393,8 @@ watch(
|
|
|
2434
2393
|
|
|
2435
2394
|
<ImageCheckDialog
|
|
2436
2395
|
v-if="imageCheckDraft?.imageCheck"
|
|
2396
|
+
:timeout-branch="imageCheckDraft.timeoutBranch"
|
|
2397
|
+
@timeout-branch="imageCheckDraft = { ...imageCheckDraft!, timeoutBranch: $event }"
|
|
2437
2398
|
:config="imageCheckDraft.imageCheck" :device-id="selectedDeviceId"
|
|
2438
2399
|
:has-element="Boolean(selectedNode?.bounds)" :editing="imageCheckEditing" :picking="imageCheckPicking"
|
|
2439
2400
|
@update="updateImageCheck" @close="imageCheckDraft = null; imageCheckPicking = false"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AppiumRecordedStep } from '../types';
|
|
3
|
+
import { flowBranchLabel } from '../flow-labels';
|
|
4
|
+
defineProps<{ step: AppiumRecordedStep; disabled?: boolean }>();
|
|
5
|
+
defineEmits<{ update: [value: NonNullable<AppiumRecordedStep['timeoutBranch']>] }>();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<el-form-item label="超时后处理">
|
|
10
|
+
<el-select class="branch-timeout-select" size="small" popper-class="branch-timeout-popper" :model-value="step.timeoutBranch || 'stop'" :disabled="disabled" @update:model-value="$emit('update', $event)">
|
|
11
|
+
<el-option label="未配置分支(超时终止)" value="stop" />
|
|
12
|
+
<el-option :label="`进入左侧分支(${flowBranchLabel(step, 'yes')})`" value="yes" />
|
|
13
|
+
<el-option :label="`进入右侧分支(${flowBranchLabel(step, 'no')})`" value="no" />
|
|
14
|
+
</el-select>
|
|
15
|
+
</el-form-item>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
.el-select.branch-timeout-select { width: 240px; max-width: 100%; flex: none; }
|
|
20
|
+
.branch-timeout-popper .el-select-dropdown__list { padding: 4px 0; }
|
|
21
|
+
.branch-timeout-popper .el-select-dropdown__item { height: 28px; line-height: 28px; padding: 0 12px; font-size: 12px; }
|
|
22
|
+
.branch-timeout-popper .el-select-dropdown__item.is-selected { color: var(--el-color-primary); }
|
|
23
|
+
</style>
|
|
@@ -5,6 +5,8 @@ import { normalizeVisualChangeConfig } from '../visual-change';
|
|
|
5
5
|
import { longPressMode } from '../long-press';
|
|
6
6
|
import { defaultFlowKind, isBooleanCondition } from '../flow-labels';
|
|
7
7
|
import TextClickSettings from './TextClickSettings.vue';
|
|
8
|
+
import BranchTimeoutSettings from './BranchTimeoutSettings.vue';
|
|
9
|
+
import { DEFAULT_NODE_TIMEOUT_MS } from '../node-timeout';
|
|
8
10
|
import LongPressSettings from './LongPressSettings.vue';
|
|
9
11
|
import StageLogSettings from './StageLogSettings.vue';
|
|
10
12
|
import LoopSettings from './LoopSettings.vue';
|
|
@@ -116,8 +118,7 @@ function patchVisualRegion(key: keyof VisualChangeConfig['region'], value: unkno
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
function patchTimeout(value: unknown) {
|
|
119
|
-
|
|
120
|
-
patchStep({ timeoutMs: timeout || undefined });
|
|
121
|
+
patchStep({ timeoutMs: value == null || value === '' ? undefined : Math.max(0, toInteger(value, DEFAULT_NODE_TIMEOUT_MS)) });
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
const showSelector = computed(() => props.step.type !== 'loop' && props.step.selector && (
|
|
@@ -152,21 +153,10 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
152
153
|
@update:model-value="patchStep({ note: String($event) || undefined })"
|
|
153
154
|
/>
|
|
154
155
|
</el-form-item>
|
|
155
|
-
<div
|
|
156
|
-
<el-form-item label="节点类型">
|
|
157
|
-
<el-select
|
|
158
|
-
:model-value="defaultKind()"
|
|
159
|
-
:disabled="disabled || isBooleanCondition(step) || step.type === 'stopApp' || step.type === 'extractVariable' || step.type === 'log' || step.type === 'openGallery' || step.type === 'endFlow' || step.type === 'loop' || step.type === 'breakLoop'"
|
|
160
|
-
@update:model-value="patchFlow({ nodeKind: $event as FlowKind })"
|
|
161
|
-
>
|
|
162
|
-
<el-option label="操作" value="action" />
|
|
163
|
-
<el-option label="判断" value="condition" />
|
|
164
|
-
<el-option label="校验" value="assertion" />
|
|
165
|
-
</el-select>
|
|
166
|
-
</el-form-item>
|
|
156
|
+
<div>
|
|
167
157
|
<el-form-item v-if="!['longPress', 'stopApp', 'log', 'openGallery', 'endFlow', 'loop', 'breakLoop'].includes(step.type)" label="超时时间 ms">
|
|
168
158
|
<el-input-number
|
|
169
|
-
:model-value="step.timeoutMs
|
|
159
|
+
:model-value="step.timeoutMs ?? (step.type === 'delay' ? 1000 : DEFAULT_NODE_TIMEOUT_MS)"
|
|
170
160
|
:disabled="disabled"
|
|
171
161
|
:min="0"
|
|
172
162
|
:max="999999"
|
|
@@ -174,6 +164,7 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
174
164
|
@update:model-value="patchTimeout($event)"
|
|
175
165
|
/>
|
|
176
166
|
</el-form-item>
|
|
167
|
+
<BranchTimeoutSettings v-if="defaultKind() === 'condition'" :step="step" :disabled="disabled" @update="patchStep({ timeoutBranch: $event })" />
|
|
177
168
|
</div>
|
|
178
169
|
<LongPressSettings v-if="step.type === 'longPress'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
179
170
|
<el-form-item v-if="['waitActivity', 'launchApp', 'stopApp', 'clearAppData'].includes(step.type)" :label="step.type === 'waitActivity' ? '目标 Activity' : '目标 APP 包名'">
|
|
@@ -4,9 +4,11 @@ import { ElMessage } from 'element-plus';
|
|
|
4
4
|
import { Aim, Camera, Upload, QuestionFilled } from '@element-plus/icons-vue';
|
|
5
5
|
import { IMAGE_CHECK_MODES, validateImageCheck, imageTemplateSize, imageTemplateRegionIssue, expandedImageTemplateRegion, type ImageCheckConfig } from '../image-check';
|
|
6
6
|
import { captureImageCheckRegion } from '../api';
|
|
7
|
+
import BranchTimeoutSettings from './BranchTimeoutSettings.vue';
|
|
8
|
+
import type { AppiumRecordedStep } from '../types';
|
|
7
9
|
|
|
8
|
-
const props = defineProps<{ config: ImageCheckConfig; deviceId: string; hasElement: boolean; editing: boolean; picking: boolean }>();
|
|
9
|
-
const emit = defineEmits<{ update: [config: ImageCheckConfig]; close: []; confirm: []; pick: []; useElement: [] }>();
|
|
10
|
+
const props = defineProps<{ config: ImageCheckConfig; deviceId: string; hasElement: boolean; editing: boolean; picking: boolean; timeoutBranch?: AppiumRecordedStep['timeoutBranch'] }>();
|
|
11
|
+
const emit = defineEmits<{ update: [config: ImageCheckConfig]; timeoutBranch: [value: NonNullable<AppiumRecordedStep['timeoutBranch']>]; close: []; confirm: []; pick: []; useElement: [] }>();
|
|
10
12
|
const busy = ref(false);
|
|
11
13
|
const regionIssue = computed(() => imageTemplateRegionIssue(props.config));
|
|
12
14
|
const expandedRegion = computed(() => expandedImageTemplateRegion(props.config));
|
|
@@ -119,10 +121,14 @@ async function upload(event: Event) {
|
|
|
119
121
|
<el-input :model-value="config.color" maxlength="7" @update:model-value="patch({ color: $event })" />
|
|
120
122
|
</el-form-item>
|
|
121
123
|
<div class="image-check-grid">
|
|
122
|
-
<
|
|
123
|
-
<el-
|
|
124
|
-
|
|
124
|
+
<template v-for="field in numericFields" :key="field.key">
|
|
125
|
+
<el-form-item :label="field.label">
|
|
126
|
+
<el-input-number :model-value="config[field.key]" :min="field.min" :max="field.max" :step="field.step" controls-position="right" @update:model-value="$event !== undefined && patch({ [field.key]: $event })" />
|
|
127
|
+
</el-form-item>
|
|
128
|
+
<BranchTimeoutSettings v-if="field.key === 'durationMs'" style="grid-column: 1 / -1" :step="{ id: '', label: '', type: 'imageCheck', timeoutBranch }" :disabled="busy" @update="emit('timeoutBranch', $event)" />
|
|
129
|
+
</template>
|
|
125
130
|
</div>
|
|
131
|
+
<BranchTimeoutSettings v-if="config.mode === 'state'" :step="{ id: '', label: '', type: 'imageCheck', timeoutBranch }" :disabled="busy" @update="emit('timeoutBranch', $event)" />
|
|
126
132
|
</el-form>
|
|
127
133
|
<template #footer><el-button :disabled="busy" @click="emit('close')">取消</el-button><el-button type="primary" :disabled="busy" @click="confirm">{{ editing ? '保存' : '添加' }}</el-button></template>
|
|
128
134
|
</el-dialog>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppiumVisualChangeRegion } from './types';
|
|
2
|
+
import { DEFAULT_NODE_TIMEOUT_MS } from './node-timeout';
|
|
2
3
|
|
|
3
4
|
export const IMAGE_CHECK_MODES = { template: '模板匹配', state: '图片状态判断', black: '黑屏检测', color: '区域颜色判断', change: '多帧变化检测' } as const;
|
|
4
5
|
export type ImageCheckConfig = {
|
|
@@ -20,6 +21,7 @@ export type ImageCheckConfig = {
|
|
|
20
21
|
consecutive: number;
|
|
21
22
|
};
|
|
22
23
|
export type ImageCheckResult = {
|
|
24
|
+
timedOut?: boolean;
|
|
23
25
|
result: boolean | null;
|
|
24
26
|
mode: ImageCheckConfig['mode'];
|
|
25
27
|
region: AppiumVisualChangeRegion;
|
|
@@ -33,7 +35,7 @@ export type ImageCheckResult = {
|
|
|
33
35
|
export function createImageCheckConfig(): ImageCheckConfig {
|
|
34
36
|
return { mode: 'template', target: 'region', region: { x: 0, y: 0, width: 100, height: 100 },
|
|
35
37
|
screenWidth: 0, screenHeight: 0, expectation: 'present', threshold: 0.9, minScoreGap: 0.1,
|
|
36
|
-
color: '#000000', tolerance: 30, ratio: 95, durationMs:
|
|
38
|
+
color: '#000000', tolerance: 30, ratio: 95, durationMs: DEFAULT_NODE_TIMEOUT_MS, intervalMs: 1000, consecutive: 1 };
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// 只读取 PNG 头部尺寸,避免为了表单校验解码整张模板。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_NODE_TIMEOUT_MS = 3000;
|