android-midscene-automation 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/README.md +160 -0
- package/bin/android-midscene-automation.js +27 -0
- package/index.html +12 -0
- package/package.json +49 -0
- package/remote-agent/index.ts +206 -0
- package/server/appium-recorder/appium-runner.ts +427 -0
- package/server/appium-recorder/repository.ts +228 -0
- package/server/appium-recorder/routes.ts +219 -0
- package/server/config-store.ts +167 -0
- package/server/config.ts +130 -0
- package/server/device-locks/repository.ts +147 -0
- package/server/device-locks/service.ts +72 -0
- package/server/device-locks/types.ts +22 -0
- package/server/device-sessions/repository.ts +169 -0
- package/server/device-sessions/service.ts +59 -0
- package/server/device-sessions/types.ts +24 -0
- package/server/http-api.ts +1389 -0
- package/server/model-call-usage-importer.ts +108 -0
- package/server/model-tester.ts +104 -0
- package/server/model-usage-repository.ts +131 -0
- package/server/operations/repository.ts +218 -0
- package/server/operations/service.ts +84 -0
- package/server/operations/types.ts +27 -0
- package/server/paths.ts +27 -0
- package/server/remote-agents/protocol.ts +38 -0
- package/server/remote-agents/registry.ts +136 -0
- package/server/remote-agents/routes.ts +89 -0
- package/server/script-agent.ts +284 -0
- package/server/script-db.ts +281 -0
- package/server/script-runner.ts +551 -0
- package/server/storage/sqlite.ts +49 -0
- package/server/test-case-import/formatter.ts +28 -0
- package/server/test-case-import/parsers/excel.ts +69 -0
- package/server/test-case-import/parsers/txt.ts +11 -0
- package/server/test-case-import/parsers/word.ts +9 -0
- package/server/test-case-import/service.ts +58 -0
- package/server/test-case-import/text-normalizer.ts +98 -0
- package/server/test-case-import/types.ts +24 -0
- package/server/test-case-import/validator.ts +34 -0
- package/src/App.vue +1450 -0
- package/src/api.ts +290 -0
- package/src/appium-recorder/AppiumPage.vue +894 -0
- package/src/appium-recorder/api.ts +64 -0
- package/src/appium-recorder/components/ComponentTree.vue +44 -0
- package/src/appium-recorder/components/NodeDetail.vue +152 -0
- package/src/appium-recorder/components/RecordedSteps.vue +79 -0
- package/src/appium-recorder/tree.ts +129 -0
- package/src/appium-recorder/types.ts +88 -0
- package/src/assets/device-actions/back.svg +5 -0
- package/src/assets/device-actions/home.svg +3 -0
- package/src/assets/device-actions/power.svg +5 -0
- package/src/assets/device-actions/tasks.svg +3 -0
- package/src/assets/device-actions/volume-down.svg +3 -0
- package/src/assets/device-actions/volume-up.svg +3 -0
- package/src/components/config/ModelUsageChart.vue +188 -0
- package/src/components/device/DevicePreviewPanel.vue +266 -0
- package/src/components/generator/GeneratedCodePanel.vue +70 -0
- package/src/components/generator/TestCaseFileUpload.vue +97 -0
- package/src/config/midscene-model-presets.ts +75 -0
- package/src/config/prompt-example.ts +6 -0
- package/src/main.ts +7 -0
- package/src/pages/AiGeneratorPage.vue +90 -0
- package/src/pages/AutomationPage.vue +161 -0
- package/src/pages/ConfigPage.vue +273 -0
- package/src/pages/GeneratorPage.vue +97 -0
- package/src/pages/ManualStepsPage.vue +179 -0
- package/src/script-generator/codegen.ts +126 -0
- package/src/script-generator/index.ts +4 -0
- package/src/script-generator/presets.ts +37 -0
- package/src/script-generator/step-options.ts +52 -0
- package/src/script-generator/types.ts +27 -0
- package/src/style.css +1983 -0
- package/src/types.ts +157 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +8 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +16 -0
- package/vite.config.ts +28 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { Delete, Plus } from '@element-plus/icons-vue';
|
|
4
|
+
import {
|
|
5
|
+
createPromptPreset,
|
|
6
|
+
stepTypeDescriptions,
|
|
7
|
+
stepTypeOptions,
|
|
8
|
+
type ScriptStep,
|
|
9
|
+
} from '../script-generator';
|
|
10
|
+
import type { AppPreset, GeneratorForm } from '../types';
|
|
11
|
+
|
|
12
|
+
defineProps<{
|
|
13
|
+
form: GeneratorForm;
|
|
14
|
+
appPresets: AppPreset[];
|
|
15
|
+
steps: ScriptStep[];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
defineEmits<{
|
|
19
|
+
addStep: [];
|
|
20
|
+
removeStep: [id: string];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const stepsDialogVisible = ref(false);
|
|
24
|
+
const exampleSteps = createPromptPreset();
|
|
25
|
+
|
|
26
|
+
const formatRepeatValue = (value: number | string | undefined) => {
|
|
27
|
+
const repeat = Number(value);
|
|
28
|
+
return Number.isFinite(repeat) && repeat > 0 ? String(Math.floor(repeat)) : '';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const parseRepeatValue = (value: string) => {
|
|
32
|
+
const repeat = Number(value);
|
|
33
|
+
return Number.isFinite(repeat) && repeat > 0 ? Math.floor(repeat) : 0;
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<section class="page-grid page-grid--single">
|
|
39
|
+
<el-card shadow="never">
|
|
40
|
+
<template #header>基础配置</template>
|
|
41
|
+
<el-form label-position="top">
|
|
42
|
+
<el-form-item label="脚本名称">
|
|
43
|
+
<el-input v-model="form.testName" placeholder="请输入脚本名称" />
|
|
44
|
+
</el-form-item>
|
|
45
|
+
<el-form-item label="场景标题">
|
|
46
|
+
<el-input v-model="form.promptTitle" placeholder="请输入场景标题" />
|
|
47
|
+
</el-form-item>
|
|
48
|
+
<el-form-item label="选择 App">
|
|
49
|
+
<el-select v-model="form.appPresetId" placeholder="选择预设 App">
|
|
50
|
+
<el-option
|
|
51
|
+
v-for="app in appPresets"
|
|
52
|
+
:key="app.id"
|
|
53
|
+
:label="`${app.name} · ${app.packageName}`"
|
|
54
|
+
:value="app.id"
|
|
55
|
+
/>
|
|
56
|
+
</el-select>
|
|
57
|
+
</el-form-item>
|
|
58
|
+
</el-form>
|
|
59
|
+
</el-card>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<el-card shadow="never" class="manual-steps-card">
|
|
63
|
+
<template #header>
|
|
64
|
+
<span>步骤编排</span>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<details class="method-guide">
|
|
68
|
+
<summary class="method-guide__summary">方法说明</summary>
|
|
69
|
+
<div class="method-guide__content">
|
|
70
|
+
<div v-for="option in stepTypeOptions" :key="option.value" class="method-guide__row">
|
|
71
|
+
<strong>{{ option.label }}</strong>
|
|
72
|
+
<div class="method-guide__text">
|
|
73
|
+
<span>{{ stepTypeDescriptions[option.value].description }}</span>
|
|
74
|
+
<code>{{ stepTypeDescriptions[option.value].example }}</code>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</details>
|
|
79
|
+
|
|
80
|
+
<div class="manual-steps-toolbar">
|
|
81
|
+
<el-button type="primary" plain @click="stepsDialogVisible = true">步骤示例</el-button>
|
|
82
|
+
<el-button type="primary" :icon="Plus" @click="$emit('addStep')">新增步骤</el-button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="step-list">
|
|
86
|
+
<div v-for="(step, index) in steps" :key="step.id" class="step-item">
|
|
87
|
+
<div class="step-item__top">
|
|
88
|
+
<span class="step-item__index">步骤 {{ index + 1 }}</span>
|
|
89
|
+
<div class="step-item__actions">
|
|
90
|
+
<el-switch v-model="step.enabled" inline-prompt active-text="开" inactive-text="关" />
|
|
91
|
+
<el-button circle :icon="Delete" @click="$emit('removeStep', step.id)" />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<el-form label-position="top" class="step-form">
|
|
95
|
+
<div class="step-grid">
|
|
96
|
+
<el-form-item label="方法">
|
|
97
|
+
<el-select v-model="step.type">
|
|
98
|
+
<el-option
|
|
99
|
+
v-for="option in stepTypeOptions"
|
|
100
|
+
:key="option.value"
|
|
101
|
+
:label="option.label"
|
|
102
|
+
:value="option.value"
|
|
103
|
+
/>
|
|
104
|
+
</el-select>
|
|
105
|
+
</el-form-item>
|
|
106
|
+
<el-form-item label="步骤标题">
|
|
107
|
+
<el-input v-model="step.label" />
|
|
108
|
+
</el-form-item>
|
|
109
|
+
<el-form-item label="Prompt" class="step-grid__wide">
|
|
110
|
+
<el-input v-model="step.prompt" type="textarea" :rows="3" />
|
|
111
|
+
</el-form-item>
|
|
112
|
+
<el-form-item v-if="['act', 'tap'].includes(step.type)" label="重复次数">
|
|
113
|
+
<el-input-number
|
|
114
|
+
v-model="step.repeat"
|
|
115
|
+
:min="0"
|
|
116
|
+
:max="10"
|
|
117
|
+
:formatter="formatRepeatValue"
|
|
118
|
+
:parser="parseRepeatValue"
|
|
119
|
+
placeholder="不重复"
|
|
120
|
+
/>
|
|
121
|
+
</el-form-item>
|
|
122
|
+
<el-form-item
|
|
123
|
+
v-if="['query', 'boolean', 'string', 'number'].includes(step.type)"
|
|
124
|
+
label="输出变量"
|
|
125
|
+
>
|
|
126
|
+
<el-input v-model="step.outputVar" />
|
|
127
|
+
</el-form-item>
|
|
128
|
+
<el-form-item v-if="step.type === 'input'" label="输入值">
|
|
129
|
+
<el-input v-model="step.value" />
|
|
130
|
+
</el-form-item>
|
|
131
|
+
</div>
|
|
132
|
+
</el-form>
|
|
133
|
+
</div>
|
|
134
|
+
<el-empty v-if="!steps.length" description="暂无步骤,点击新增步骤开始编排" />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<el-dialog v-model="stepsDialogVisible" title="步骤示例" width="920px" class="steps-dialog">
|
|
138
|
+
<div class="step-list step-list--dialog">
|
|
139
|
+
<div v-for="(step, index) in exampleSteps" :key="step.id" class="step-item step-item--readonly">
|
|
140
|
+
<div class="step-item__top">
|
|
141
|
+
<span class="step-item__index">步骤 {{ index + 1 }}</span>
|
|
142
|
+
</div>
|
|
143
|
+
<el-form label-position="top" class="step-form">
|
|
144
|
+
<div class="step-grid">
|
|
145
|
+
<el-form-item label="方法">
|
|
146
|
+
<el-input
|
|
147
|
+
:model-value="stepTypeOptions.find((option) => option.value === step.type)?.label || step.type"
|
|
148
|
+
readonly
|
|
149
|
+
/>
|
|
150
|
+
</el-form-item>
|
|
151
|
+
<el-form-item label="步骤标题">
|
|
152
|
+
<el-input :model-value="step.label" readonly />
|
|
153
|
+
</el-form-item>
|
|
154
|
+
<el-form-item label="Prompt" class="step-grid__wide">
|
|
155
|
+
<el-input :model-value="step.prompt" type="textarea" :rows="3" readonly />
|
|
156
|
+
</el-form-item>
|
|
157
|
+
<el-form-item v-if="['act', 'tap'].includes(step.type)" label="重复次数">
|
|
158
|
+
<el-input :model-value="step.repeat && step.repeat > 1 ? String(step.repeat) : '不重复'" readonly />
|
|
159
|
+
</el-form-item>
|
|
160
|
+
<el-form-item
|
|
161
|
+
v-if="['query', 'boolean', 'string', 'number'].includes(step.type)"
|
|
162
|
+
label="输出变量"
|
|
163
|
+
>
|
|
164
|
+
<el-input :model-value="step.outputVar || ''" readonly />
|
|
165
|
+
</el-form-item>
|
|
166
|
+
<el-form-item v-if="step.type === 'input'" label="输入值">
|
|
167
|
+
<el-input :model-value="step.value || ''" readonly />
|
|
168
|
+
</el-form-item>
|
|
169
|
+
</div>
|
|
170
|
+
</el-form>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<template #footer>
|
|
175
|
+
<el-button @click="stepsDialogVisible = false">关闭</el-button>
|
|
176
|
+
</template>
|
|
177
|
+
</el-dialog>
|
|
178
|
+
</el-card>
|
|
179
|
+
</template>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ScriptForm, ScriptStep } from './types';
|
|
2
|
+
|
|
3
|
+
const safeVar = (value: string, fallback: string) => {
|
|
4
|
+
const normalized = value.trim().replace(/[^a-zA-Z0-9_$]+/g, '_').replace(/^(\d)/, '_$1');
|
|
5
|
+
return normalized || fallback;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const escapeTemplate = (value: string) => value.replace(/`/g, '\\`');
|
|
9
|
+
|
|
10
|
+
function getRepeatCount(step: ScriptStep) {
|
|
11
|
+
const repeat = Number(step.repeat || 1);
|
|
12
|
+
if (!Number.isFinite(repeat)) return 1;
|
|
13
|
+
return Math.max(1, Math.min(10, Math.floor(repeat)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function withRepeat(step: ScriptStep, body: string) {
|
|
17
|
+
const repeat = getRepeatCount(step);
|
|
18
|
+
if (repeat <= 1) return body;
|
|
19
|
+
|
|
20
|
+
const inner = body
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((line) => ` ${line.replace(/^ /, '')}`)
|
|
23
|
+
.join('\n');
|
|
24
|
+
|
|
25
|
+
return ` for (let attempt = 1; attempt <= ${repeat}; attempt += 1) {
|
|
26
|
+
console.log(${JSON.stringify(`执行${step.label || step.type}`)}, attempt);
|
|
27
|
+
${inner}
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
29
|
+
}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 瞬时提示可能在下一次单帧截图前消失,因此只对明确标记的动作开启观察窗口。
|
|
33
|
+
function withObservation(step: ScriptStep, index: number, body: string) {
|
|
34
|
+
const assertion = step.observePrompt?.trim();
|
|
35
|
+
if (!assertion) return body;
|
|
36
|
+
|
|
37
|
+
const observerVar = `observer${index + 1}`;
|
|
38
|
+
const nestedBody = body
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((line) => ` ${line}`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
|
|
43
|
+
return ` const ${observerVar} = await agent.startObserving({ intervalMs: 500, maxFrames: 20 });
|
|
44
|
+
try {
|
|
45
|
+
${nestedBody}
|
|
46
|
+
} finally {
|
|
47
|
+
await ${observerVar}.stop();
|
|
48
|
+
}
|
|
49
|
+
await ${observerVar}.aiAssert(\`${escapeTemplate(assertion)}\`);`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getBooleanFailureRule(step: ScriptStep, outputVar: string) {
|
|
53
|
+
const signalText = `${outputVar} ${step.label}`;
|
|
54
|
+
const shouldFailWhenTrue =
|
|
55
|
+
/has.*(modal|popup|dialog|blocking)|hasBlocking|blockingModal/i.test(signalText) ||
|
|
56
|
+
step.label.includes('确认无干扰弹窗') ||
|
|
57
|
+
step.label.includes('遮挡弹窗');
|
|
58
|
+
const shouldFailWhenFalse =
|
|
59
|
+
/is.*(ready|loaded|success)|isLoginReady|loginReady/i.test(signalText) ||
|
|
60
|
+
step.label.includes('验证登录页就绪') ||
|
|
61
|
+
step.label.includes('登录页就绪');
|
|
62
|
+
return { shouldFailWhenTrue, shouldFailWhenFalse };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildStep(step: ScriptStep, index: number) {
|
|
66
|
+
const prompt = escapeTemplate(step.prompt);
|
|
67
|
+
switch (step.type) {
|
|
68
|
+
case 'comment':
|
|
69
|
+
return ` // ${step.label || step.prompt}`;
|
|
70
|
+
case 'waitFor':
|
|
71
|
+
return ` await agent.aiWaitFor(\`${prompt}\`);`;
|
|
72
|
+
case 'act':
|
|
73
|
+
return withObservation(step, index, withRepeat(step, ` await agent.aiAct(\`${prompt}\`);`));
|
|
74
|
+
case 'tap':
|
|
75
|
+
return withObservation(step, index, withRepeat(step, ` await agent.aiTap(\`${prompt}\`);`));
|
|
76
|
+
case 'input':
|
|
77
|
+
return withObservation(
|
|
78
|
+
step,
|
|
79
|
+
index,
|
|
80
|
+
` await agent.aiInput(\`${prompt}\`, { value: ${JSON.stringify(step.value || '')} });`,
|
|
81
|
+
);
|
|
82
|
+
case 'query':
|
|
83
|
+
return ` const ${safeVar(step.outputVar || 'result', 'result')} = await agent.aiQuery(${JSON.stringify(step.prompt)});\n console.log(${JSON.stringify(step.label || step.outputVar || 'query result')}, ${safeVar(step.outputVar || 'result', 'result')});`;
|
|
84
|
+
case 'boolean':
|
|
85
|
+
{
|
|
86
|
+
const outputVar = safeVar(step.outputVar || 'flag', 'flag');
|
|
87
|
+
const { shouldFailWhenTrue, shouldFailWhenFalse } = getBooleanFailureRule(step, outputVar);
|
|
88
|
+
return ` const ${outputVar} = await agent.aiBoolean(\`${prompt}\`);\n console.log(${JSON.stringify(step.label || step.outputVar || 'boolean result')}, ${outputVar});${
|
|
89
|
+
shouldFailWhenTrue
|
|
90
|
+
? `\n if (${outputVar}) {\n throw new Error(${JSON.stringify(`${step.label || outputVar} 未通过:仍存在遮挡登录操作的弹窗`)});\n }`
|
|
91
|
+
: ''
|
|
92
|
+
}${
|
|
93
|
+
shouldFailWhenFalse
|
|
94
|
+
? `\n if (!${outputVar}) {\n throw new Error(${JSON.stringify(`${step.label || outputVar} 未通过:页面未达到预期状态`)});\n }`
|
|
95
|
+
: ''
|
|
96
|
+
}`;
|
|
97
|
+
}
|
|
98
|
+
case 'string':
|
|
99
|
+
return ` const ${safeVar(step.outputVar || 'text', 'text')} = await agent.aiString(\`${prompt}\`);\n console.log(${JSON.stringify(step.label || step.outputVar || 'string result')}, ${safeVar(step.outputVar || 'text', 'text')});`;
|
|
100
|
+
case 'number':
|
|
101
|
+
return ` const ${safeVar(step.outputVar || 'count', 'count')} = await agent.aiNumber(\`${prompt}\`);\n console.log(${JSON.stringify(step.label || step.outputVar || 'number result')}, ${safeVar(step.outputVar || 'count', 'count')});`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildScript(form: ScriptForm, steps: ScriptStep[]) {
|
|
106
|
+
const activeSteps = steps.filter((step) => step.enabled);
|
|
107
|
+
|
|
108
|
+
return `import { agentFromAdbDevice } from '@midscene/android';
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const agent = await agentFromAdbDevice(process.env.ANDROID_SERIAL);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// ${form.promptTitle}
|
|
115
|
+
${activeSteps.map((step, index) => buildStep(step, index)).join('\n')}
|
|
116
|
+
} finally {
|
|
117
|
+
await agent.destroy?.();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
console.error(error);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ScriptStep } from './types';
|
|
2
|
+
|
|
3
|
+
export function createPromptPreset(): ScriptStep[] {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: crypto.randomUUID(),
|
|
7
|
+
type: 'comment',
|
|
8
|
+
label: '进入目标 App',
|
|
9
|
+
prompt: '确保进入目标 App,再处理启动阶段弹窗',
|
|
10
|
+
enabled: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
type: 'act',
|
|
15
|
+
label: '进入目标 App',
|
|
16
|
+
prompt:
|
|
17
|
+
'先观察当前界面:如果已经在目标 App 内,保持当前状态;如果不在目标 App 内,使用包名打开目标 App 并等待首屏加载完成。不要查找桌面图标或从最近任务中选择 App。',
|
|
18
|
+
enabled: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: crypto.randomUUID(),
|
|
22
|
+
type: 'act',
|
|
23
|
+
label: '循环处理启动弹窗',
|
|
24
|
+
prompt:
|
|
25
|
+
'检查当前页面:如果仍有启动阶段的声明与条款、协议、隐私政策、权限、广告、活动、更新或通知引导弹窗遮挡业务操作,点击同意、允许、关闭、跳过或稍后再说。如果没有遮挡弹窗,不要点击业务内容、表单字段、复选框或提交按钮。',
|
|
26
|
+
repeat: 1,
|
|
27
|
+
enabled: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
type: 'waitFor',
|
|
32
|
+
label: '等待起始页面稳定',
|
|
33
|
+
prompt: '页面已经稳定显示为本次测试的起始页面,并且没有协议、权限、广告、更新、活动或通知引导弹窗遮挡业务操作',
|
|
34
|
+
enabled: true,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { StepType } from './types';
|
|
2
|
+
|
|
3
|
+
export const stepTypeOptions: Array<{ label: string; value: StepType }> = [
|
|
4
|
+
{ label: '注释', value: 'comment' },
|
|
5
|
+
{ label: 'aiAct', value: 'act' },
|
|
6
|
+
{ label: 'aiWaitFor', value: 'waitFor' },
|
|
7
|
+
{ label: 'aiTap', value: 'tap' },
|
|
8
|
+
{ label: 'aiInput', value: 'input' },
|
|
9
|
+
{ label: 'aiQuery', value: 'query' },
|
|
10
|
+
{ label: 'aiBoolean', value: 'boolean' },
|
|
11
|
+
{ label: 'aiString', value: 'string' },
|
|
12
|
+
{ label: 'aiNumber', value: 'number' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const stepTypeDescriptions: Record<StepType, { description: string; example: string }> = {
|
|
16
|
+
comment: {
|
|
17
|
+
description: '只写注释,不执行动作。适合说明意图、标记阶段或补充上下文。',
|
|
18
|
+
example: '示例:处理登录前弹窗',
|
|
19
|
+
},
|
|
20
|
+
act: {
|
|
21
|
+
description: '适合一段自然语言动作,比如连续点击、关闭弹窗、完成一组界面操作。',
|
|
22
|
+
example: '示例:若出现权限弹窗则点击允许,若出现广告弹窗则关闭',
|
|
23
|
+
},
|
|
24
|
+
waitFor: {
|
|
25
|
+
description: '等待某个界面条件成立后再继续,适合加载完成、页面稳定、元素出现。',
|
|
26
|
+
example: '示例:登录页面已经稳定显示,并且可以开始输入账号密码',
|
|
27
|
+
},
|
|
28
|
+
tap: {
|
|
29
|
+
description: '点击一个明确目标,适合按钮、列表项、返回键这类单步操作。',
|
|
30
|
+
example: '示例:登录按钮',
|
|
31
|
+
},
|
|
32
|
+
input: {
|
|
33
|
+
description: '向输入框填写内容。Prompt 写定位目标,输入值单独填在“输入值”。',
|
|
34
|
+
example: '示例:Prompt=手机号输入框;输入值=13800138000',
|
|
35
|
+
},
|
|
36
|
+
query: {
|
|
37
|
+
description: '提取结构化数据,适合列表、表格、对象数组,输出到变量供后续使用。',
|
|
38
|
+
example: '示例:{ title: string, price: number }[], 当前页面商品列表',
|
|
39
|
+
},
|
|
40
|
+
boolean: {
|
|
41
|
+
description: '判断真假,适合判断弹窗是否存在、状态是否满足、按钮是否可见。',
|
|
42
|
+
example: '示例:当前页面上是否还存在会遮挡登录操作的弹窗',
|
|
43
|
+
},
|
|
44
|
+
string: {
|
|
45
|
+
description: '提取单条文本,适合标题、用户名、标签内容。',
|
|
46
|
+
example: '示例:当前页面最显眼的标题文本',
|
|
47
|
+
},
|
|
48
|
+
number: {
|
|
49
|
+
description: '提取数字,适合计数、金额、数量、页码。',
|
|
50
|
+
example: '示例:购物车角标上的商品数量',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type StepType =
|
|
2
|
+
| 'comment'
|
|
3
|
+
| 'waitFor'
|
|
4
|
+
| 'act'
|
|
5
|
+
| 'tap'
|
|
6
|
+
| 'input'
|
|
7
|
+
| 'query'
|
|
8
|
+
| 'boolean'
|
|
9
|
+
| 'string'
|
|
10
|
+
| 'number';
|
|
11
|
+
|
|
12
|
+
export interface ScriptStep {
|
|
13
|
+
id: string;
|
|
14
|
+
type: StepType;
|
|
15
|
+
label: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
outputVar?: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
observePrompt?: string;
|
|
20
|
+
repeat?: number;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ScriptForm {
|
|
25
|
+
promptTitle: string;
|
|
26
|
+
testName: string;
|
|
27
|
+
}
|