android-midscene-automation 0.1.34 → 0.1.35
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 +7 -0
- package/README.md +7 -0
- package/package.json +2 -1
- package/server/appium-recorder/appium-runner.ts +39 -1
- package/server/appium-recorder/image-check.ts +160 -0
- package/server/appium-recorder/report.ts +41 -0
- package/server/appium-recorder/repository.ts +5 -0
- package/server/appium-recorder/routes.ts +19 -0
- package/src/appium-recorder/AppiumPage.vue +86 -4
- package/src/appium-recorder/action-descriptions.ts +46 -0
- package/src/appium-recorder/api.ts +5 -0
- package/src/appium-recorder/components/ComponentInspector.vue +9 -2
- package/src/appium-recorder/components/FlowActionMenu.vue +26 -5
- package/src/appium-recorder/components/FlowCanvas.vue +5 -4
- package/src/appium-recorder/components/FlowNodeCard.vue +5 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +143 -0
- package/src/appium-recorder/components/NestedConditionBranches.vue +6 -2
- package/src/appium-recorder/components/RecordedSteps.vue +7 -6
- package/src/appium-recorder/flow-copy.ts +4 -0
- package/src/appium-recorder/flow-graph.ts +22 -10
- package/src/appium-recorder/flow-insert.ts +27 -0
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/appium-recorder/image-check.ts +117 -0
- package/src/appium-recorder/types.ts +3 -0
- package/src/appium-recorder/visual-change-pairs.ts +2 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { AppiumVisualChangeRegion } from './types';
|
|
2
|
+
|
|
3
|
+
export const IMAGE_CHECK_MODES = { template: '模板匹配', state: '图片状态判断', black: '黑屏检测', color: '区域颜色判断', change: '多帧变化检测' } as const;
|
|
4
|
+
export type ImageCheckConfig = {
|
|
5
|
+
mode: keyof typeof IMAGE_CHECK_MODES;
|
|
6
|
+
target: 'region' | 'element';
|
|
7
|
+
region: AppiumVisualChangeRegion;
|
|
8
|
+
screenWidth: number;
|
|
9
|
+
screenHeight: number;
|
|
10
|
+
template?: string;
|
|
11
|
+
negativeTemplate?: string;
|
|
12
|
+
expectation: 'present' | 'absent';
|
|
13
|
+
threshold: number;
|
|
14
|
+
minScoreGap: number;
|
|
15
|
+
color: string;
|
|
16
|
+
tolerance: number;
|
|
17
|
+
ratio: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
intervalMs: number;
|
|
20
|
+
consecutive: number;
|
|
21
|
+
};
|
|
22
|
+
export type ImageCheckResult = {
|
|
23
|
+
result: boolean | null;
|
|
24
|
+
mode: ImageCheckConfig['mode'];
|
|
25
|
+
region: AppiumVisualChangeRegion;
|
|
26
|
+
message: string;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
sampleCount: number;
|
|
29
|
+
metrics: Record<string, number>;
|
|
30
|
+
images: { label: string; base64: string }[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function createImageCheckConfig(): ImageCheckConfig {
|
|
34
|
+
return { mode: 'template', target: 'region', region: { x: 0, y: 0, width: 100, height: 100 },
|
|
35
|
+
screenWidth: 0, screenHeight: 0, expectation: 'present', threshold: 0.9, minScoreGap: 0.1,
|
|
36
|
+
color: '#000000', tolerance: 30, ratio: 95, durationMs: 5000, intervalMs: 1000, consecutive: 1 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 只读取 PNG 头部尺寸,避免为了表单校验解码整张模板。
|
|
40
|
+
export function imageTemplateSize(base64?: string) {
|
|
41
|
+
if (!base64) return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const header = atob(base64.slice(0, 32));
|
|
44
|
+
if (header.length < 24 || header.slice(0, 8) !== '\x89PNG\r\n\x1a\n') return undefined;
|
|
45
|
+
const uint = (offset: number) => (header.charCodeAt(offset) * 0x1000000
|
|
46
|
+
+ header.charCodeAt(offset + 1) * 0x10000 + header.charCodeAt(offset + 2) * 0x100 + header.charCodeAt(offset + 3));
|
|
47
|
+
const width = uint(16), height = uint(20);
|
|
48
|
+
return width && height ? { width, height } : undefined;
|
|
49
|
+
} catch { return undefined; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function imageTemplateRegionIssue(config: ImageCheckConfig) {
|
|
53
|
+
if (!['template', 'state'].includes(config.mode)) return '';
|
|
54
|
+
const templates = [{ value: config.template, label: config.mode === 'state' ? '选中模板' : '模板' },
|
|
55
|
+
...(config.mode === 'state' ? [{ value: config.negativeTemplate, label: '未选中模板' }] : [])];
|
|
56
|
+
for (const template of templates) {
|
|
57
|
+
const size = imageTemplateSize(template.value);
|
|
58
|
+
if (size && (size.width > config.region.width || size.height > config.region.height)) {
|
|
59
|
+
return `${template.label} ${size.width}×${size.height} 超过检测区域 ${config.region.width}×${config.region.height},${config.target === 'region' ? '请扩大区域或重新采集模板' : '请重新选择组件或采集对应模板'}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function expandedImageTemplateRegion(config: ImageCheckConfig) {
|
|
66
|
+
if (config.target !== 'region' || !['template', 'state'].includes(config.mode)) return undefined;
|
|
67
|
+
const sizes = [config.template, ...(config.mode === 'state' ? [config.negativeTemplate] : [])].map(imageTemplateSize);
|
|
68
|
+
const width = Math.max(config.region.width, ...sizes.map(size => size?.width || 0));
|
|
69
|
+
const height = Math.max(config.region.height, ...sizes.map(size => size?.height || 0));
|
|
70
|
+
if (width > config.screenWidth || height > config.screenHeight || !Number.isFinite(width + height)) return undefined;
|
|
71
|
+
// 用户明确选择扩区时以原区域中心扩展,并保证仍在屏幕内,不改变模板像素。
|
|
72
|
+
const x = Math.max(0, Math.min(config.screenWidth - width, Math.floor(config.region.x + (config.region.width - width) / 2)));
|
|
73
|
+
const y = Math.max(0, Math.min(config.screenHeight - height, Math.floor(config.region.y + (config.region.height - height) / 2)));
|
|
74
|
+
return { x, y, width, height };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function validateImageCheck(config: ImageCheckConfig | undefined) {
|
|
78
|
+
if (!config || !Object.prototype.hasOwnProperty.call(IMAGE_CHECK_MODES, config.mode)) throw new Error('请选择图像判断模式');
|
|
79
|
+
const range = (value: number, min: number, max: number, name: string, integer = false) => {
|
|
80
|
+
if (!Number.isFinite(value) || value < min || value > max || (integer && !Number.isInteger(value))) throw new Error(`${name}必须在 ${min} 至 ${max} 之间${integer ? ',且为整数' : ''}`);
|
|
81
|
+
};
|
|
82
|
+
if (!['region', 'element'].includes(config.target)) throw new Error('检测目标无效');
|
|
83
|
+
if (!['present', 'absent'].includes(config.expectation)) throw new Error('判断条件无效');
|
|
84
|
+
range(config.region?.x, 0, 16000, '区域 X', true);
|
|
85
|
+
range(config.region?.y, 0, 16000, '区域 Y', true);
|
|
86
|
+
range(config.region?.width, 1, 16000, '区域宽度', true);
|
|
87
|
+
range(config.region?.height, 1, 16000, '区域高度', true);
|
|
88
|
+
if (config.target === 'region') {
|
|
89
|
+
range(config.screenWidth, 1, 16000, '采集屏幕宽度', true);
|
|
90
|
+
range(config.screenHeight, 1, 16000, '采集屏幕高度', true);
|
|
91
|
+
}
|
|
92
|
+
range(config.threshold, 0.01, 1, '模板阈值');
|
|
93
|
+
range(config.minScoreGap, 0.001, 1, '最小得分差');
|
|
94
|
+
range(config.tolerance, 0, 255, '像素容差', true);
|
|
95
|
+
range(config.ratio, 0.01, 100, '像素占比');
|
|
96
|
+
range(config.durationMs, 0, 60000, '观察时长', true);
|
|
97
|
+
range(config.intervalMs, 200, 10000, '采样间隔', true);
|
|
98
|
+
range(config.consecutive, 1, 100, '连续满足帧数', true);
|
|
99
|
+
if (config.consecutive > Math.floor(config.durationMs / config.intervalMs) + 1) throw new Error('观察时间不足以采集连续满足帧数');
|
|
100
|
+
if (config.mode === 'change' && config.durationMs < config.intervalMs) throw new Error('多帧变化的观察时长不能小于采样间隔');
|
|
101
|
+
if (!/^#[0-9a-f]{6}$/i.test(config.color)) throw new Error('目标颜色必须为 #RRGGBB 格式');
|
|
102
|
+
for (const value of [config.template, config.negativeTemplate]) {
|
|
103
|
+
if (value && (value.length > 4 * 1024 * 1024 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value))) throw new Error('模板必须是小于 3MB 的 PNG 图片');
|
|
104
|
+
}
|
|
105
|
+
if (['template', 'state'].includes(config.mode) && !config.template) throw new Error('请采集或上传模板');
|
|
106
|
+
if (config.mode === 'state' && !config.negativeTemplate) throw new Error('请采集或上传未选中模板');
|
|
107
|
+
if (config.target === 'region') {
|
|
108
|
+
const issue = imageTemplateRegionIssue(config);
|
|
109
|
+
if (issue) throw new Error(issue);
|
|
110
|
+
}
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function imageCheckSummary(config?: ImageCheckConfig) {
|
|
115
|
+
if (!config) return '未配置图像判断';
|
|
116
|
+
return `${IMAGE_CHECK_MODES[config.mode]} · ${config.target === 'element' ? '组件区域' : '框选区域'} · ${config.durationMs}ms`;
|
|
117
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TestVariable, VariableExtraction, ScriptReturn } from './variables';
|
|
2
|
+
import type { ImageCheckConfig } from './image-check';
|
|
2
3
|
|
|
3
4
|
export type AppiumBounds = {
|
|
4
5
|
left: number;
|
|
@@ -81,6 +82,7 @@ export type AppiumRecordedStep = {
|
|
|
81
82
|
| 'checkedState'
|
|
82
83
|
| 'radioButtonState'
|
|
83
84
|
| 'aiRecognition'
|
|
85
|
+
| 'imageCheck'
|
|
84
86
|
| 'textClick'
|
|
85
87
|
| 'key'
|
|
86
88
|
| 'waitActivity'
|
|
@@ -150,6 +152,7 @@ export type AppiumRecordedStep = {
|
|
|
150
152
|
percent: number;
|
|
151
153
|
};
|
|
152
154
|
visualChange?: AppiumVisualChangeConfig;
|
|
155
|
+
imageCheck?: ImageCheckConfig;
|
|
153
156
|
snapshot?: {
|
|
154
157
|
text: string;
|
|
155
158
|
resourceId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AppiumRecordedStep } from './types';
|
|
2
2
|
|
|
3
3
|
export type VisualChangeBranchTarget = {
|
|
4
|
+
beforeStepId?: string;
|
|
4
5
|
stepId: string;
|
|
5
6
|
branch: 'yes' | 'no';
|
|
6
7
|
};
|
|
@@ -85,6 +86,7 @@ export function branchPathForInsert(
|
|
|
85
86
|
branchTarget?: VisualChangeBranchTarget,
|
|
86
87
|
) {
|
|
87
88
|
if (branchTarget) {
|
|
89
|
+
if (branchTarget.beforeStepId) return branchPathForStep(steps, steps.find(step => step.id === branchTarget.beforeStepId));
|
|
88
90
|
const condition = steps.find((step) => step.id === branchTarget.stepId);
|
|
89
91
|
return [...branchPathForStep(steps, condition), `${branchTarget.stepId}:${branchTarget.branch}`];
|
|
90
92
|
}
|