android-midscene-automation 0.1.37 → 0.1.39
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 +13 -0
- package/README.md +13 -0
- package/package.json +12 -7
- package/server/appium-recorder/ai-recognition.ts +4 -2
- package/server/appium-recorder/appium-runner.ts +90 -37
- package/server/appium-recorder/condition-timeout.ts +4 -0
- package/server/appium-recorder/image-check.ts +16 -2
- package/server/appium-recorder/replay-mp4.ts +72 -0
- package/server/appium-recorder/replay-video.ts +99 -0
- package/server/appium-recorder/report.ts +60 -6
- package/server/appium-recorder/repository.ts +1 -0
- package/server/appium-recorder/routes.ts +13 -17
- package/server/appium-recorder/run-history.ts +7 -1
- package/server/appium-recorder/scrcpy-recording-source.ts +80 -0
- package/server/appium-recorder/video-response.ts +36 -0
- package/src/appium-recorder/AppiumPage.vue +42 -98
- package/src/appium-recorder/RunHistoryDialog.vue +7 -2
- package/src/appium-recorder/action-descriptions.ts +1 -1
- package/src/appium-recorder/api.ts +3 -10
- package/src/appium-recorder/components/BranchTimeoutSettings.vue +23 -0
- package/src/appium-recorder/components/FlowNodeCard.vue +2 -3
- package/src/appium-recorder/components/FlowStepEditor.vue +6 -21
- package/src/appium-recorder/components/ImageCheckDialog.vue +38 -9
- package/src/appium-recorder/components/NestedConditionBranches.vue +0 -1
- package/src/appium-recorder/components/RecordedSteps.vue +0 -1
- package/src/appium-recorder/flow-graph.ts +2 -2
- package/src/appium-recorder/flow-labels.ts +2 -2
- package/src/appium-recorder/image-check.ts +6 -2
- package/src/appium-recorder/node-timeout.ts +1 -0
- package/src/appium-recorder/run-history.ts +1 -0
- package/src/appium-recorder/types.ts +1 -0
- package/src/components/config/AppiumConfigPanel.vue +0 -35
- package/src/appium-recorder/components/AiRecognitionTestDialog.vue +0 -71
|
@@ -47,7 +47,7 @@ export function flowTypeLabel(step: AppiumRecordedStep) {
|
|
|
47
47
|
checkboxState: 'Checkbox 状态',
|
|
48
48
|
checkedState: '判断勾选',
|
|
49
49
|
radioButtonState: 'RadioButton 状态',
|
|
50
|
-
aiRecognition: 'AI
|
|
50
|
+
aiRecognition: 'AI 识别(已移除)',
|
|
51
51
|
imageCheck: '图像判断',
|
|
52
52
|
textClick: '文字点击',
|
|
53
53
|
assertText: '断言文本',
|
|
@@ -81,7 +81,7 @@ export function flowStepMeta(step: AppiumRecordedStep) {
|
|
|
81
81
|
if (step.type === 'loop') return `最多 ${step.loop?.maxIterations ?? '?'} 次 · ${step.loop?.exitWhen === 'exists' ? '元素出现时退出' : step.loop?.exitWhen === 'notExists' ? '元素消失时退出' : '固定次数'}${step.loop?.exitWhen !== 'never' ? ` ${step.selector?.value || ''}` : ''}`;
|
|
82
82
|
if (step.type === 'breakLoop') return step.breakLoopTargetId ? '退出指定循环,继续循环结束后的流程' : '退出当前循环,继续循环结束后的流程';
|
|
83
83
|
if (step.type === 'log') return `${step.logPrefix ?? DEFAULT_LOG_PREFIX}:${step.value || ''}`;
|
|
84
|
-
if (step.type === 'aiRecognition') return
|
|
84
|
+
if (step.type === 'aiRecognition') return '此操作已移除,请替换为图像判断或原生组件判断';
|
|
85
85
|
if (step.type === 'longPress') {
|
|
86
86
|
const target = longPressMode(step) === 'element'
|
|
87
87
|
? `元素 ${step.selector?.strategy || ''} ${step.selector?.value || ''}`
|
|
@@ -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,8 @@ export type ImageCheckConfig = {
|
|
|
20
21
|
consecutive: number;
|
|
21
22
|
};
|
|
22
23
|
export type ImageCheckResult = {
|
|
24
|
+
templateMatch?: { matched: boolean; expected: 'present' | 'absent'; score: number; threshold: number };
|
|
25
|
+
timedOut?: boolean;
|
|
23
26
|
result: boolean | null;
|
|
24
27
|
mode: ImageCheckConfig['mode'];
|
|
25
28
|
region: AppiumVisualChangeRegion;
|
|
@@ -33,7 +36,7 @@ export type ImageCheckResult = {
|
|
|
33
36
|
export function createImageCheckConfig(): ImageCheckConfig {
|
|
34
37
|
return { mode: 'template', target: 'region', region: { x: 0, y: 0, width: 100, height: 100 },
|
|
35
38
|
screenWidth: 0, screenHeight: 0, expectation: 'present', threshold: 0.9, minScoreGap: 0.1,
|
|
36
|
-
color: '#000000', tolerance: 30, ratio: 95, durationMs:
|
|
39
|
+
color: '#000000', tolerance: 30, ratio: 95, durationMs: DEFAULT_NODE_TIMEOUT_MS, intervalMs: 1000, consecutive: 1 };
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// 只读取 PNG 头部尺寸,避免为了表单校验解码整张模板。
|
|
@@ -113,5 +116,6 @@ export function validateImageCheck(config: ImageCheckConfig | undefined) {
|
|
|
113
116
|
|
|
114
117
|
export function imageCheckSummary(config?: ImageCheckConfig) {
|
|
115
118
|
if (!config) return '未配置图像判断';
|
|
116
|
-
|
|
119
|
+
const expectation = config.mode === 'template' ? ` · ${config.expectation === 'present' ? '匹配到模板' : '未匹配到模板'}` : '';
|
|
120
|
+
return `${IMAGE_CHECK_MODES[config.mode]}${expectation} · ${config.target === 'element' ? '组件区域' : '框选区域'} · ${config.durationMs}ms`;
|
|
117
121
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_NODE_TIMEOUT_MS = 3000;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ConfigForm } from '../../types';
|
|
3
3
|
import FlowAppearanceSettings from './FlowAppearanceSettings.vue';
|
|
4
|
-
import { DEFAULT_FLOW_BACKGROUND, isHexColor } from '../../appium-recorder/flow-appearance';
|
|
5
4
|
|
|
6
5
|
defineProps<{
|
|
7
6
|
configForm: ConfigForm;
|
|
@@ -13,39 +12,5 @@ defineEmits<{ testModel: []; saveModelConfig: [] }>();
|
|
|
13
12
|
</script>
|
|
14
13
|
|
|
15
14
|
<template>
|
|
16
|
-
<el-card shadow="never" class="config-module-card config-appium-card">
|
|
17
|
-
<template #header>
|
|
18
|
-
<div class="panel-header">
|
|
19
|
-
<span>节点模型配置</span>
|
|
20
|
-
<el-button type="primary" :loading="isSavingModelConfig" :disabled="!isHexColor(configForm.appium.flowBackgroundColor ?? DEFAULT_FLOW_BACKGROUND)" @click="$emit('saveModelConfig')">
|
|
21
|
-
保存模型配置
|
|
22
|
-
</el-button>
|
|
23
|
-
</div>
|
|
24
|
-
</template>
|
|
25
|
-
<section class="appium-model-form">
|
|
26
|
-
<div class="panel-header panel-header--sub">
|
|
27
|
-
<span>AI 识别模型</span>
|
|
28
|
-
<el-button :loading="testingModelKey === 'appium'" @click="$emit('testModel')">测试模型</el-button>
|
|
29
|
-
</div>
|
|
30
|
-
<el-form label-position="top">
|
|
31
|
-
<el-form-item label="Base URL">
|
|
32
|
-
<el-input v-model="configForm.appium.model.baseUrl" />
|
|
33
|
-
</el-form-item>
|
|
34
|
-
<el-form-item label="API Key">
|
|
35
|
-
<el-input v-model="configForm.appium.model.apiKey" show-password />
|
|
36
|
-
</el-form-item>
|
|
37
|
-
<el-form-item label="Model Name">
|
|
38
|
-
<el-input v-model="configForm.appium.model.name" placeholder="支持图片输入的模型" />
|
|
39
|
-
</el-form-item>
|
|
40
|
-
<el-form-item v-if="testStatus" label="测试结果">
|
|
41
|
-
<el-input :model-value="testStatus" readonly />
|
|
42
|
-
</el-form-item>
|
|
43
|
-
</el-form>
|
|
44
|
-
</section>
|
|
45
|
-
</el-card>
|
|
46
15
|
<FlowAppearanceSettings v-model="configForm.appium.flowBackgroundColor" :saving="isSavingModelConfig" @save="$emit('saveModelConfig')" />
|
|
47
16
|
</template>
|
|
48
|
-
|
|
49
|
-
<style scoped>
|
|
50
|
-
.appium-model-form { max-width: 640px; }
|
|
51
|
-
</style>
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
|
|
3
|
-
import { Refresh } from '@element-plus/icons-vue';
|
|
4
|
-
import { testAiRecognition } from '../api';
|
|
5
|
-
import { AI_MODEL_CONFIG_HINT, validateAiRecognitionPrompt } from '../ai-recognition';
|
|
6
|
-
import type { AppiumRecordedStep } from '../types';
|
|
7
|
-
|
|
8
|
-
const props = defineProps<{ step: AppiumRecordedStep; deviceId: string; modelConfigured: boolean }>();
|
|
9
|
-
defineEmits<{ close: [] }>();
|
|
10
|
-
const loading = shallowRef(false);
|
|
11
|
-
const error = shallowRef('');
|
|
12
|
-
const result = shallowRef<Awaited<ReturnType<typeof testAiRecognition>> | null>(null);
|
|
13
|
-
let controller: AbortController | undefined;
|
|
14
|
-
|
|
15
|
-
async function runTest() {
|
|
16
|
-
if (loading.value) return;
|
|
17
|
-
error.value = '';
|
|
18
|
-
result.value = null;
|
|
19
|
-
controller = new AbortController();
|
|
20
|
-
loading.value = true;
|
|
21
|
-
try {
|
|
22
|
-
if (!props.modelConfigured) throw new Error(AI_MODEL_CONFIG_HINT);
|
|
23
|
-
if (!props.deviceId) throw new Error('请选择设备');
|
|
24
|
-
const prompt = validateAiRecognitionPrompt(props.step.value);
|
|
25
|
-
result.value = await testAiRecognition({ deviceId: props.deviceId, prompt, timeoutMs: props.step.timeoutMs }, controller.signal);
|
|
26
|
-
} catch (cause) {
|
|
27
|
-
if (!controller.signal.aborted) error.value = cause instanceof Error ? cause.message : 'AI 识别测试失败';
|
|
28
|
-
} finally {
|
|
29
|
-
loading.value = false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
onMounted(runTest);
|
|
34
|
-
onBeforeUnmount(() => controller?.abort());
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
|
-
<template>
|
|
38
|
-
<el-dialog :model-value="true" title="测试 AI 识别" width="min(560px, calc(100vw - 32px))" append-to-body @close="$emit('close')">
|
|
39
|
-
<div class="ai-recognition-test" aria-live="polite">
|
|
40
|
-
<p class="ai-recognition-test__prompt">{{ step.value }}</p>
|
|
41
|
-
<el-alert v-if="error" type="error" :title="error" :closable="false" show-icon />
|
|
42
|
-
<p v-if="loading">识别中…</p>
|
|
43
|
-
<template v-if="result">
|
|
44
|
-
<div class="ai-recognition-test__result">
|
|
45
|
-
<strong>{{ String(result.result) }}</strong>
|
|
46
|
-
<span>{{ result.durationMs }}ms</span>
|
|
47
|
-
</div>
|
|
48
|
-
<p>{{ result.reason }}</p>
|
|
49
|
-
<el-image
|
|
50
|
-
v-if="result.imageBase64"
|
|
51
|
-
class="ai-recognition-test__image" fit="contain"
|
|
52
|
-
:src="`data:image/png;base64,${result.imageBase64}`"
|
|
53
|
-
:preview-src-list="[`data:image/png;base64,${result.imageBase64}`]"
|
|
54
|
-
preview-teleported alt="AI 识别使用的设备截图"
|
|
55
|
-
/>
|
|
56
|
-
</template>
|
|
57
|
-
</div>
|
|
58
|
-
<template #footer>
|
|
59
|
-
<el-button @click="$emit('close')">关闭</el-button>
|
|
60
|
-
<el-button type="primary" :icon="Refresh" :loading="loading" :disabled="!modelConfigured || !deviceId" @click="runTest">重新测试</el-button>
|
|
61
|
-
</template>
|
|
62
|
-
</el-dialog>
|
|
63
|
-
</template>
|
|
64
|
-
|
|
65
|
-
<style scoped>
|
|
66
|
-
.ai-recognition-test { overflow-wrap: anywhere; }
|
|
67
|
-
.ai-recognition-test__prompt { white-space: pre-wrap; margin-top: 0; }
|
|
68
|
-
.ai-recognition-test__result { display: flex; align-items: center; gap: 16px; }
|
|
69
|
-
.ai-recognition-test__result strong { font-size: 20px; }
|
|
70
|
-
.ai-recognition-test__image { display: block; width: 100%; height: 280px; }
|
|
71
|
-
</style>
|