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
package/src/App.vue
ADDED
|
@@ -0,0 +1,1450 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue';
|
|
3
|
+
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
CopyDocument,
|
|
7
|
+
Edit,
|
|
8
|
+
Loading,
|
|
9
|
+
Monitor,
|
|
10
|
+
Operation,
|
|
11
|
+
RefreshLeft,
|
|
12
|
+
Setting,
|
|
13
|
+
VideoPause,
|
|
14
|
+
VideoPlay,
|
|
15
|
+
} from '@element-plus/icons-vue';
|
|
16
|
+
import backIcon from './assets/device-actions/back.svg';
|
|
17
|
+
import homeIcon from './assets/device-actions/home.svg';
|
|
18
|
+
import powerIcon from './assets/device-actions/power.svg';
|
|
19
|
+
import tasksIcon from './assets/device-actions/tasks.svg';
|
|
20
|
+
import volumeDownIcon from './assets/device-actions/volume-down.svg';
|
|
21
|
+
import volumeUpIcon from './assets/device-actions/volume-up.svg';
|
|
22
|
+
import { promptDocument } from './config/prompt-example';
|
|
23
|
+
import {
|
|
24
|
+
buildScript,
|
|
25
|
+
type ScriptStep,
|
|
26
|
+
} from './script-generator';
|
|
27
|
+
import {
|
|
28
|
+
codexMidsceneModel,
|
|
29
|
+
midsceneModelPresets,
|
|
30
|
+
type MidsceneModelProvider,
|
|
31
|
+
type MidsceneModelPresetKey,
|
|
32
|
+
} from './config/midscene-model-presets';
|
|
33
|
+
import * as api from './api';
|
|
34
|
+
import AutomationPage from './pages/AutomationPage.vue';
|
|
35
|
+
import AppiumPage from './appium-recorder/AppiumPage.vue';
|
|
36
|
+
import ConfigPage from './pages/ConfigPage.vue';
|
|
37
|
+
import GeneratorPage from './pages/GeneratorPage.vue';
|
|
38
|
+
import type {
|
|
39
|
+
AndroidDevice,
|
|
40
|
+
ConfigForm,
|
|
41
|
+
AppPreset,
|
|
42
|
+
ExecutionStep,
|
|
43
|
+
GeneratorMode,
|
|
44
|
+
MenuKey,
|
|
45
|
+
ModelUsageRecord,
|
|
46
|
+
RunScriptStreamEvent,
|
|
47
|
+
SavedScript,
|
|
48
|
+
} from './types';
|
|
49
|
+
|
|
50
|
+
const activeMenu = ref<MenuKey>('generator');
|
|
51
|
+
const activeGeneratorMode = ref<GeneratorMode>('ai');
|
|
52
|
+
const sourcePrompt = ref('');
|
|
53
|
+
const steps = ref<ScriptStep[]>([]);
|
|
54
|
+
const errorMessage = ref('');
|
|
55
|
+
const isGenerating = ref(false);
|
|
56
|
+
const isSavingModelConfig = ref(false);
|
|
57
|
+
const isSavingAppPreset = ref(false);
|
|
58
|
+
const isRunningScript = ref(false);
|
|
59
|
+
const isStoppingScript = ref(false);
|
|
60
|
+
const showGeneratedCode = ref(false);
|
|
61
|
+
const generatedCodeOverride = shallowRef<string | null>(null);
|
|
62
|
+
const generatedCodeDraft = shallowRef('');
|
|
63
|
+
const generatedCodeEditing = shallowRef(false);
|
|
64
|
+
const generatedCodeEditSaving = shallowRef(false);
|
|
65
|
+
const importingTestCase = shallowRef(false);
|
|
66
|
+
const importedTestCaseFileName = shallowRef('');
|
|
67
|
+
const testingModelKey = ref('');
|
|
68
|
+
const playgroundAvailable = ref(false);
|
|
69
|
+
const playgroundPreviewError = ref('');
|
|
70
|
+
const playgroundDeviceId = ref('');
|
|
71
|
+
const playgroundFrameUrl = ref('');
|
|
72
|
+
const playgroundFrameSignature = ref('');
|
|
73
|
+
const devicePreviewUrl = ref('');
|
|
74
|
+
const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
|
|
75
|
+
const androidDevices = ref<AndroidDevice[]>([]);
|
|
76
|
+
const deviceInterfaceSize = reactive({ width: 0, height: 0 });
|
|
77
|
+
const deviceDebug = reactive({
|
|
78
|
+
rawX: 0,
|
|
79
|
+
rawY: 0,
|
|
80
|
+
mappedX: 0,
|
|
81
|
+
mappedY: 0,
|
|
82
|
+
action: '',
|
|
83
|
+
status: 'idle',
|
|
84
|
+
message: '',
|
|
85
|
+
});
|
|
86
|
+
const selectedScriptId = ref('');
|
|
87
|
+
const executionLog = ref('');
|
|
88
|
+
const executionProcess = ref<ExecutionStep[]>([]);
|
|
89
|
+
const lastRunStatus = ref('');
|
|
90
|
+
const runStartedAt = shallowRef<number | null>(null);
|
|
91
|
+
const runningElapsedNow = shallowRef(0);
|
|
92
|
+
const savedScripts = ref<SavedScript[]>([]);
|
|
93
|
+
const appPresets = ref<AppPreset[]>([]);
|
|
94
|
+
const modelUsageRecords = ref<ModelUsageRecord[]>([]);
|
|
95
|
+
const modelTestStatus = reactive({
|
|
96
|
+
midscene: '',
|
|
97
|
+
scriptOptimizer: '',
|
|
98
|
+
});
|
|
99
|
+
const actionDialog = reactive({
|
|
100
|
+
visible: false,
|
|
101
|
+
title: '',
|
|
102
|
+
loading: false,
|
|
103
|
+
error: '',
|
|
104
|
+
});
|
|
105
|
+
const codeDialog = reactive({
|
|
106
|
+
visible: false,
|
|
107
|
+
title: '',
|
|
108
|
+
scriptId: '',
|
|
109
|
+
code: '',
|
|
110
|
+
draftCode: '',
|
|
111
|
+
editing: false,
|
|
112
|
+
saving: false,
|
|
113
|
+
});
|
|
114
|
+
let playgroundPollTimer: number | null = null;
|
|
115
|
+
let appiumPreviewTimer: number | null = null;
|
|
116
|
+
let executionProgressTimer: number | null = null;
|
|
117
|
+
let scriptRunAbortController: AbortController | null = null;
|
|
118
|
+
|
|
119
|
+
const form = reactive({
|
|
120
|
+
promptTitle: '',
|
|
121
|
+
testName: '',
|
|
122
|
+
appPresetId: '',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const appPresetForm = reactive({
|
|
126
|
+
id: '',
|
|
127
|
+
name: '',
|
|
128
|
+
packageName: '',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const configForm = reactive<ConfigForm>({
|
|
132
|
+
midscene: {
|
|
133
|
+
model: {
|
|
134
|
+
provider: 'custom',
|
|
135
|
+
baseUrl: '',
|
|
136
|
+
apiKey: '',
|
|
137
|
+
name: '',
|
|
138
|
+
family: '',
|
|
139
|
+
},
|
|
140
|
+
env: {},
|
|
141
|
+
},
|
|
142
|
+
scriptOptimizer: {
|
|
143
|
+
model: {
|
|
144
|
+
baseUrl: '',
|
|
145
|
+
apiKey: '',
|
|
146
|
+
name: '',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const lastCustomMidsceneModel = reactive({
|
|
151
|
+
baseUrl: '',
|
|
152
|
+
apiKey: '',
|
|
153
|
+
name: '',
|
|
154
|
+
family: '',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const menuItems = [
|
|
158
|
+
{ key: 'generator', label: '测试脚本生成', icon: Operation },
|
|
159
|
+
{ key: 'automation', label: '自动化测试', icon: Monitor },
|
|
160
|
+
] as const;
|
|
161
|
+
const activeMenuLabel = computed(() => {
|
|
162
|
+
if (activeMenu.value === 'appium') return 'Appium';
|
|
163
|
+
if (activeMenu.value === 'config') return '参数配置';
|
|
164
|
+
return menuItems.find((item) => item.key === activeMenu.value)?.label || 'Midscene';
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const selectedAppPreset = computed(() => appPresets.value.find((item) => item.id === form.appPresetId));
|
|
168
|
+
const appPromptPrefix = computed(() => {
|
|
169
|
+
if (!selectedAppPreset.value) return '';
|
|
170
|
+
const appName = selectedAppPreset.value.name;
|
|
171
|
+
const packageName = selectedAppPreset.value.packageName;
|
|
172
|
+
return [
|
|
173
|
+
`本流程为 ${appName} App ${form.promptTitle}。目标 App 包名为 \`${packageName}\`。`,
|
|
174
|
+
`执行开始时先观察当前手机界面:如果已经在 ${appName} App 内,保持当前状态;如果不在 ${appName} App 内,使用包名 \`${packageName}\` 打开 App 并等待首屏加载完成。不要生成查找桌面图标或从最近任务中选择 App 的步骤。`,
|
|
175
|
+
'',
|
|
176
|
+
].join('\n');
|
|
177
|
+
});
|
|
178
|
+
const effectiveSourcePrompt = computed(() => `${appPromptPrefix.value}${sourcePrompt.value}`.trim());
|
|
179
|
+
const generatedCodeFromSteps = computed(() => buildScript(form, steps.value));
|
|
180
|
+
const generatedCode = computed(() => generatedCodeOverride.value ?? generatedCodeFromSteps.value);
|
|
181
|
+
const selectedScript = computed(() => savedScripts.value.find((item) => item.id === selectedScriptId.value));
|
|
182
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
|
+
const deviceActions = [
|
|
184
|
+
{ key: 'power', label: '关机键', icon: powerIcon, keyCode: 26 },
|
|
185
|
+
{ key: 'volume-up', label: '音量+', icon: volumeUpIcon, keyCode: 24 },
|
|
186
|
+
{ key: 'volume-down', label: '音量-', icon: volumeDownIcon, keyCode: 25 },
|
|
187
|
+
{ key: 'back', label: '返回', icon: backIcon, keyCode: 4 },
|
|
188
|
+
{ key: 'home', label: '桌面', icon: homeIcon, keyCode: 3 },
|
|
189
|
+
{ key: 'tasks', label: '任务', icon: tasksIcon, keyCode: 187 },
|
|
190
|
+
] as const;
|
|
191
|
+
|
|
192
|
+
const selectMenu = (key: string) => {
|
|
193
|
+
activeMenu.value = key as MenuKey;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const resetGeneratedCodeEditor = () => {
|
|
197
|
+
generatedCodeOverride.value = null;
|
|
198
|
+
generatedCodeDraft.value = '';
|
|
199
|
+
generatedCodeEditing.value = false;
|
|
200
|
+
generatedCodeEditSaving.value = false;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
watch(activeGeneratorMode, () => {
|
|
204
|
+
showGeneratedCode.value = false;
|
|
205
|
+
resetGeneratedCodeEditor();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
function formatScriptTime(value: string) {
|
|
209
|
+
if (!value) return '';
|
|
210
|
+
const date = new Date(value);
|
|
211
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
212
|
+
return date.toLocaleString();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function loadSavedScripts(preferredId = selectedScriptId.value) {
|
|
216
|
+
const payload = await api.getScripts();
|
|
217
|
+
savedScripts.value = payload.scripts || [];
|
|
218
|
+
if (preferredId && savedScripts.value.some((item) => item.id === preferredId)) {
|
|
219
|
+
selectedScriptId.value = preferredId;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
selectedScriptId.value = savedScripts.value[0]?.id || '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function migrateLegacySavedScripts() {
|
|
226
|
+
const raw = localStorage.getItem('midscene-saved-scripts');
|
|
227
|
+
if (!raw) return;
|
|
228
|
+
|
|
229
|
+
const legacyScripts = JSON.parse(raw) as Array<Partial<SavedScript>>;
|
|
230
|
+
if (!legacyScripts.length) return;
|
|
231
|
+
|
|
232
|
+
const currentPayload = await api.getScripts();
|
|
233
|
+
if (currentPayload.scripts?.length) return;
|
|
234
|
+
|
|
235
|
+
for (const item of legacyScripts) {
|
|
236
|
+
if (!item.name || !item.code) continue;
|
|
237
|
+
await api.saveScript({
|
|
238
|
+
scriptName: item.name,
|
|
239
|
+
promptTitle: item.promptTitle || item.name,
|
|
240
|
+
sourcePrompt: item.sourcePrompt || '',
|
|
241
|
+
code: item.code,
|
|
242
|
+
steps: item.steps || [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
localStorage.removeItem('midscene-saved-scripts');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const openActionDialog = (title: string) => {
|
|
250
|
+
actionDialog.visible = true;
|
|
251
|
+
actionDialog.title = title;
|
|
252
|
+
actionDialog.loading = true;
|
|
253
|
+
actionDialog.error = '';
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const closeActionDialog = () => {
|
|
257
|
+
actionDialog.visible = false;
|
|
258
|
+
actionDialog.loading = false;
|
|
259
|
+
actionDialog.error = '';
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const openCodeDialog = (script: SavedScript) => {
|
|
263
|
+
codeDialog.title = `${script.promptTitle || script.name} · ${script.name}`;
|
|
264
|
+
codeDialog.scriptId = script.id;
|
|
265
|
+
codeDialog.code = script.code || '';
|
|
266
|
+
codeDialog.draftCode = script.code || '';
|
|
267
|
+
codeDialog.editing = false;
|
|
268
|
+
codeDialog.saving = false;
|
|
269
|
+
codeDialog.visible = true;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const copyDialogCode = async () => {
|
|
273
|
+
await navigator.clipboard.writeText(codeDialog.editing ? codeDialog.draftCode : codeDialog.code);
|
|
274
|
+
ElMessage.success('代码已复制');
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const editCodeDialog = () => {
|
|
278
|
+
codeDialog.draftCode = codeDialog.code;
|
|
279
|
+
codeDialog.editing = true;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const undoCodeDialogChanges = () => {
|
|
283
|
+
codeDialog.draftCode = codeDialog.code;
|
|
284
|
+
codeDialog.editing = false;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const saveCodeDialog = async () => {
|
|
288
|
+
if (!codeDialog.scriptId || codeDialog.saving) return;
|
|
289
|
+
|
|
290
|
+
codeDialog.saving = true;
|
|
291
|
+
try {
|
|
292
|
+
const payload = await api.updateScriptCode({
|
|
293
|
+
id: codeDialog.scriptId,
|
|
294
|
+
code: codeDialog.draftCode,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
codeDialog.code = payload.script?.code || codeDialog.draftCode;
|
|
298
|
+
codeDialog.draftCode = codeDialog.code;
|
|
299
|
+
codeDialog.editing = false;
|
|
300
|
+
await loadSavedScripts(payload.script?.id || codeDialog.scriptId);
|
|
301
|
+
ElMessage.success('代码已保存');
|
|
302
|
+
} catch (error) {
|
|
303
|
+
ElMessage.error(error instanceof Error ? error.message : '代码格式错误,无法保存');
|
|
304
|
+
} finally {
|
|
305
|
+
codeDialog.saving = false;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const failActionDialog = (message: string) => {
|
|
310
|
+
actionDialog.loading = false;
|
|
311
|
+
actionDialog.error = message;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const addStep = () => {
|
|
315
|
+
steps.value.push({
|
|
316
|
+
id: crypto.randomUUID(),
|
|
317
|
+
type: 'act',
|
|
318
|
+
label: '新步骤',
|
|
319
|
+
prompt: '',
|
|
320
|
+
outputVar: '',
|
|
321
|
+
value: '',
|
|
322
|
+
enabled: true,
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const removeStep = (id: string) => {
|
|
327
|
+
steps.value = steps.value.filter((item) => item.id !== id);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const copyCode = async () => {
|
|
331
|
+
const code = generatedCodeEditing.value ? generatedCodeDraft.value : generatedCode.value;
|
|
332
|
+
await navigator.clipboard.writeText(code);
|
|
333
|
+
ElMessage.success('代码已复制');
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const editGeneratedCode = () => {
|
|
337
|
+
if (!showGeneratedCode.value) return;
|
|
338
|
+
generatedCodeDraft.value = generatedCode.value;
|
|
339
|
+
generatedCodeEditing.value = true;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const undoGeneratedCodeChanges = () => {
|
|
343
|
+
generatedCodeDraft.value = generatedCode.value;
|
|
344
|
+
generatedCodeEditing.value = false;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const saveGeneratedCodeChanges = async () => {
|
|
348
|
+
if (generatedCodeEditSaving.value) return;
|
|
349
|
+
|
|
350
|
+
generatedCodeEditSaving.value = true;
|
|
351
|
+
try {
|
|
352
|
+
await api.validateScriptCode({ code: generatedCodeDraft.value });
|
|
353
|
+
generatedCodeOverride.value = generatedCodeDraft.value;
|
|
354
|
+
generatedCodeEditing.value = false;
|
|
355
|
+
ElMessage.success('代码修改已保存');
|
|
356
|
+
} catch (error) {
|
|
357
|
+
ElMessage.error(error instanceof Error ? error.message : '代码格式错误,无法保存');
|
|
358
|
+
} finally {
|
|
359
|
+
generatedCodeEditSaving.value = false;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const importTestCase = async (file: File) => {
|
|
364
|
+
if (importingTestCase.value) return;
|
|
365
|
+
|
|
366
|
+
importingTestCase.value = true;
|
|
367
|
+
try {
|
|
368
|
+
const result = await api.importTestCaseFile(file);
|
|
369
|
+
sourcePrompt.value = result.prompt;
|
|
370
|
+
importedTestCaseFileName.value = result.fileName;
|
|
371
|
+
steps.value = [];
|
|
372
|
+
showGeneratedCode.value = false;
|
|
373
|
+
errorMessage.value = '';
|
|
374
|
+
resetGeneratedCodeEditor();
|
|
375
|
+
ElMessage.success(`已导入 ${result.caseCount} 条测试用例`);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
ElMessage.error(error instanceof Error ? error.message : '测试用例文件导入失败');
|
|
378
|
+
} finally {
|
|
379
|
+
importingTestCase.value = false;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const clearGeneratorPage = () => {
|
|
384
|
+
form.testName = '';
|
|
385
|
+
form.promptTitle = '';
|
|
386
|
+
form.appPresetId = '';
|
|
387
|
+
sourcePrompt.value = '';
|
|
388
|
+
steps.value = [];
|
|
389
|
+
showGeneratedCode.value = false;
|
|
390
|
+
importedTestCaseFileName.value = '';
|
|
391
|
+
errorMessage.value = '';
|
|
392
|
+
resetGeneratedCodeEditor();
|
|
393
|
+
ElMessage.success('页面内容已清除');
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const clearExecutionLog = () => {
|
|
397
|
+
executionLog.value = '';
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const copyExecutionLog = async () => {
|
|
401
|
+
if (!executionLog.value) return;
|
|
402
|
+
|
|
403
|
+
await navigator.clipboard.writeText(executionLog.value);
|
|
404
|
+
ElMessage.success('日志已复制');
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const validateBaseGeneratorInputs = () => {
|
|
408
|
+
const scriptName = form.testName.trim();
|
|
409
|
+
const promptTitle = form.promptTitle.trim();
|
|
410
|
+
|
|
411
|
+
if (!scriptName) {
|
|
412
|
+
ElMessage.error('请输入脚本名称');
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!promptTitle) {
|
|
417
|
+
ElMessage.error('请输入场景标题');
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { scriptName, promptTitle };
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const validateAiGeneratorInputs = () => {
|
|
425
|
+
const baseInput = validateBaseGeneratorInputs();
|
|
426
|
+
if (!baseInput) return null;
|
|
427
|
+
|
|
428
|
+
const rawPrompt = sourcePrompt.value.trim();
|
|
429
|
+
if (!rawPrompt) {
|
|
430
|
+
ElMessage.error('请输入原始 Prompt');
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { ...baseInput, rawPrompt };
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const validateManualGeneratorInputs = () => {
|
|
438
|
+
const baseInput = validateBaseGeneratorInputs();
|
|
439
|
+
if (!baseInput) return null;
|
|
440
|
+
|
|
441
|
+
const enabledSteps = steps.value.filter((step) => step.enabled !== false);
|
|
442
|
+
if (!enabledSteps.length) {
|
|
443
|
+
ElMessage.error('请至少新增一个开启状态的步骤');
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const emptyPromptStep = enabledSteps.find((step) => !step.prompt.trim());
|
|
448
|
+
if (emptyPromptStep) {
|
|
449
|
+
ElMessage.error(`步骤“${emptyPromptStep.label || '未命名步骤'}”的 Prompt 不能为空`);
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return baseInput;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const validateCurrentGeneratorInputs = () => (
|
|
457
|
+
activeGeneratorMode.value === 'manual'
|
|
458
|
+
? validateManualGeneratorInputs()
|
|
459
|
+
: validateAiGeneratorInputs()
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const buildSourcePromptForSave = () => {
|
|
463
|
+
if (activeGeneratorMode.value === 'manual') {
|
|
464
|
+
return appPromptPrefix.value.trim();
|
|
465
|
+
}
|
|
466
|
+
return effectiveSourcePrompt.value;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const saveCurrentScript = async () => {
|
|
470
|
+
const validInput = validateCurrentGeneratorInputs();
|
|
471
|
+
if (!validInput) return;
|
|
472
|
+
|
|
473
|
+
if (generatedCodeEditing.value) {
|
|
474
|
+
ElMessage.error('请先保存或撤回代码修改');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!showGeneratedCode.value || !generatedCode.value.trim()) {
|
|
479
|
+
ElMessage.error('请先点击 AI 生成,生成代码后再保存脚本');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const { scriptName, promptTitle } = validInput;
|
|
484
|
+
const checkPayload = await api.checkScript({ scriptName });
|
|
485
|
+
|
|
486
|
+
if (checkPayload.exists) {
|
|
487
|
+
try {
|
|
488
|
+
await ElMessageBox.confirm(`已存在同名脚本“${scriptName}”,是否覆盖?`, '提示', {
|
|
489
|
+
type: 'warning',
|
|
490
|
+
confirmButtonText: '覆盖',
|
|
491
|
+
cancelButtonText: '取消',
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const startedAt = Date.now();
|
|
499
|
+
openActionDialog('保存脚本');
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const payload = await api.saveScript({
|
|
503
|
+
scriptName,
|
|
504
|
+
code: generatedCode.value,
|
|
505
|
+
promptTitle,
|
|
506
|
+
sourcePrompt: buildSourcePromptForSave(),
|
|
507
|
+
steps: steps.value,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
await loadSavedScripts(payload.script?.id || '');
|
|
511
|
+
await sleep(Math.max(0, 1200 - (Date.now() - startedAt)));
|
|
512
|
+
closeActionDialog();
|
|
513
|
+
} catch (error) {
|
|
514
|
+
failActionDialog(error instanceof Error ? error.message : '保存脚本失败');
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const removeSavedScript = async (id: string) => {
|
|
519
|
+
const script = savedScripts.value.find((item) => item.id === id);
|
|
520
|
+
try {
|
|
521
|
+
await ElMessageBox.confirm(
|
|
522
|
+
`确定删除脚本“${script?.promptTitle || script?.name || '未命名脚本'}”吗?删除后对应的脚本文件也会移除。`,
|
|
523
|
+
'删除脚本',
|
|
524
|
+
{
|
|
525
|
+
type: 'warning',
|
|
526
|
+
confirmButtonText: '删除',
|
|
527
|
+
cancelButtonText: '取消',
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
} catch {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
await api.deleteScript({ id });
|
|
536
|
+
await loadSavedScripts(selectedScriptId.value === id ? '' : selectedScriptId.value);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '删除脚本失败';
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const generateWithModel = async () => {
|
|
543
|
+
if (!validateCurrentGeneratorInputs()) return;
|
|
544
|
+
|
|
545
|
+
if (activeGeneratorMode.value === 'manual') {
|
|
546
|
+
resetGeneratedCodeEditor();
|
|
547
|
+
showGeneratedCode.value = true;
|
|
548
|
+
ElMessage.success('代码已生成');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
isGenerating.value = true;
|
|
553
|
+
errorMessage.value = '';
|
|
554
|
+
openActionDialog('AI生成');
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const payload = await api.generatePlan({
|
|
558
|
+
prompt: effectiveSourcePrompt.value,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
form.promptTitle = payload.promptTitle || form.promptTitle;
|
|
562
|
+
steps.value = (payload.steps || []).map((step) => ({ ...step, id: crypto.randomUUID() }));
|
|
563
|
+
resetGeneratedCodeEditor();
|
|
564
|
+
showGeneratedCode.value = true;
|
|
565
|
+
closeActionDialog();
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const message = error instanceof Error ? error.message : 'AI 生成失败';
|
|
568
|
+
errorMessage.value = message;
|
|
569
|
+
failActionDialog(message);
|
|
570
|
+
} finally {
|
|
571
|
+
await loadModelUsageRecords().catch(() => undefined);
|
|
572
|
+
isGenerating.value = false;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const loadConfig = async () => {
|
|
577
|
+
const payload = await api.getConfig();
|
|
578
|
+
Object.assign(configForm.midscene.model, payload.midscene.model);
|
|
579
|
+
Object.keys(configForm.midscene.env).forEach((key) => {
|
|
580
|
+
delete configForm.midscene.env[key];
|
|
581
|
+
});
|
|
582
|
+
Object.assign(configForm.midscene.env, payload.midscene.env || {});
|
|
583
|
+
Object.assign(configForm.scriptOptimizer.model, payload.scriptOptimizer.model);
|
|
584
|
+
if (configForm.midscene.model.provider !== 'codex') {
|
|
585
|
+
rememberCustomMidsceneModel();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const rememberCustomMidsceneModel = () => {
|
|
590
|
+
if (configForm.midscene.model.provider === 'codex') return;
|
|
591
|
+
lastCustomMidsceneModel.baseUrl = configForm.midscene.model.baseUrl;
|
|
592
|
+
lastCustomMidsceneModel.apiKey = configForm.midscene.model.apiKey;
|
|
593
|
+
lastCustomMidsceneModel.name = configForm.midscene.model.name;
|
|
594
|
+
lastCustomMidsceneModel.family = configForm.midscene.model.family;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const updateMidsceneModelProvider = (provider: MidsceneModelProvider) => {
|
|
598
|
+
if (provider === 'codex') {
|
|
599
|
+
rememberCustomMidsceneModel();
|
|
600
|
+
Object.assign(configForm.midscene.model, codexMidsceneModel);
|
|
601
|
+
} else {
|
|
602
|
+
Object.assign(configForm.midscene.model, {
|
|
603
|
+
provider: 'custom',
|
|
604
|
+
...lastCustomMidsceneModel,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
modelTestStatus.midscene = '';
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const applyMidsceneModelPreset = (key: MidsceneModelPresetKey) => {
|
|
611
|
+
const preset = midsceneModelPresets.find((item) => item.key === key);
|
|
612
|
+
if (!preset) return;
|
|
613
|
+
|
|
614
|
+
configForm.midscene.model.provider = 'custom';
|
|
615
|
+
configForm.midscene.model.baseUrl = preset.baseUrl;
|
|
616
|
+
configForm.midscene.model.name = preset.modelName;
|
|
617
|
+
configForm.midscene.model.family = preset.modelFamily;
|
|
618
|
+
rememberCustomMidsceneModel();
|
|
619
|
+
modelTestStatus.midscene = '';
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const getMidsceneModelConfigError = () => {
|
|
623
|
+
const model = configForm.midscene.model;
|
|
624
|
+
const missing = [];
|
|
625
|
+
|
|
626
|
+
if (!model.baseUrl.trim()) missing.push('Base URL');
|
|
627
|
+
if (!model.name.trim()) missing.push('Model Name');
|
|
628
|
+
if (!model.family.trim()) missing.push('Model Family');
|
|
629
|
+
if (model.provider !== 'codex' && !model.apiKey.trim()) missing.push('API Key');
|
|
630
|
+
|
|
631
|
+
if (!missing.length) return '';
|
|
632
|
+
return `Midscene 模型配置不完整:缺少 ${missing.join('、')}。请在“参数配置 > Midscene 模型”中选择“使用 Codex”,或补齐自定义提供方后保存。`;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const saveModelConfig = async () => {
|
|
636
|
+
isSavingModelConfig.value = true;
|
|
637
|
+
errorMessage.value = '';
|
|
638
|
+
openActionDialog('保存模型配置');
|
|
639
|
+
try {
|
|
640
|
+
await api.saveConfig(configForm);
|
|
641
|
+
closeActionDialog();
|
|
642
|
+
ElMessage.success('模型配置已保存');
|
|
643
|
+
} catch (error) {
|
|
644
|
+
const message = error instanceof Error ? error.message : '模型配置保存失败';
|
|
645
|
+
errorMessage.value = message;
|
|
646
|
+
failActionDialog(message);
|
|
647
|
+
} finally {
|
|
648
|
+
isSavingModelConfig.value = false;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const loadAppPresets = async () => {
|
|
653
|
+
const payload = await api.getAppPresets();
|
|
654
|
+
appPresets.value = payload.apps || [];
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const editAppPreset = (app: AppPreset) => {
|
|
658
|
+
appPresetForm.id = app.id;
|
|
659
|
+
appPresetForm.name = app.name;
|
|
660
|
+
appPresetForm.packageName = app.packageName;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const resetAppPresetForm = () => {
|
|
664
|
+
appPresetForm.id = '';
|
|
665
|
+
appPresetForm.name = '';
|
|
666
|
+
appPresetForm.packageName = '';
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const saveAppPreset = async () => {
|
|
670
|
+
isSavingAppPreset.value = true;
|
|
671
|
+
errorMessage.value = '';
|
|
672
|
+
openActionDialog('保存 App 配置');
|
|
673
|
+
try {
|
|
674
|
+
const payload = await api.saveAppPreset({
|
|
675
|
+
id: appPresetForm.id || undefined,
|
|
676
|
+
name: appPresetForm.name,
|
|
677
|
+
packageName: appPresetForm.packageName,
|
|
678
|
+
});
|
|
679
|
+
await loadAppPresets();
|
|
680
|
+
if (payload.app?.id) {
|
|
681
|
+
form.appPresetId = payload.app.id;
|
|
682
|
+
}
|
|
683
|
+
resetAppPresetForm();
|
|
684
|
+
closeActionDialog();
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const message = error instanceof Error ? error.message : 'App 配置保存失败';
|
|
687
|
+
errorMessage.value = message;
|
|
688
|
+
failActionDialog(message);
|
|
689
|
+
} finally {
|
|
690
|
+
isSavingAppPreset.value = false;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const removeAppPreset = async (id: string) => {
|
|
695
|
+
try {
|
|
696
|
+
await ElMessageBox.confirm('确定删除这个 App 配置吗?', '删除 App 配置', {
|
|
697
|
+
type: 'warning',
|
|
698
|
+
confirmButtonText: '删除',
|
|
699
|
+
cancelButtonText: '取消',
|
|
700
|
+
});
|
|
701
|
+
} catch {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
await api.deleteAppPreset({ id });
|
|
707
|
+
await loadAppPresets();
|
|
708
|
+
if (form.appPresetId === id) {
|
|
709
|
+
form.appPresetId = '';
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
errorMessage.value = error instanceof Error ? error.message : 'App 配置删除失败';
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const loadModelUsageRecords = async () => {
|
|
717
|
+
const payload = await api.getModelUsageRecords(50);
|
|
718
|
+
modelUsageRecords.value = payload.records || [];
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
722
|
+
testingModelKey.value = key;
|
|
723
|
+
errorMessage.value = '';
|
|
724
|
+
modelTestStatus[key] = '';
|
|
725
|
+
const model =
|
|
726
|
+
key === 'midscene'
|
|
727
|
+
? configForm.midscene.model
|
|
728
|
+
: configForm.scriptOptimizer.model;
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const payload = await api.testModel({ modelKey: key, model });
|
|
732
|
+
modelTestStatus[key] = payload.content || '连接成功';
|
|
733
|
+
} catch (error) {
|
|
734
|
+
modelTestStatus[key] = error instanceof Error ? error.message : '测试失败';
|
|
735
|
+
} finally {
|
|
736
|
+
await loadModelUsageRecords().catch(() => undefined);
|
|
737
|
+
testingModelKey.value = '';
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const loadAndroidDevices = async () => {
|
|
742
|
+
const payload = await api.getAndroidDevices();
|
|
743
|
+
androidDevices.value = payload.devices || [];
|
|
744
|
+
playgroundDeviceId.value = payload.currentDeviceId || '';
|
|
745
|
+
playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const loadDeviceInterface = async () => {
|
|
749
|
+
try {
|
|
750
|
+
const payload = await api.getAndroidDisplayInfo(playgroundDeviceId.value);
|
|
751
|
+
deviceInterfaceSize.width = Number(payload.width) || 0;
|
|
752
|
+
deviceInterfaceSize.height = Number(payload.height) || 0;
|
|
753
|
+
} catch {
|
|
754
|
+
deviceInterfaceSize.width = 0;
|
|
755
|
+
deviceInterfaceSize.height = 0;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const loadAdbPreview = () => {
|
|
760
|
+
if (!playgroundDeviceId.value) {
|
|
761
|
+
devicePreviewUrl.value = '';
|
|
762
|
+
devicePreviewMode.value = '';
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
devicePreviewUrl.value = `${api.APP_BASE}/api/android-preview?deviceId=${encodeURIComponent(playgroundDeviceId.value)}&t=${Date.now()}`;
|
|
767
|
+
devicePreviewMode.value = 'screenshot';
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const refreshAdbPreviewAfterInput = () => {
|
|
771
|
+
if (devicePreviewMode.value === 'screenshot' && devicePreviewUrl.value) {
|
|
772
|
+
window.setTimeout(loadAdbPreview, 120);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const refreshDevicePreview = () => {
|
|
777
|
+
if (activeMenu.value === 'appium') {
|
|
778
|
+
loadAdbPreview();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (devicePreviewMode.value === 'stream' && playgroundFrameUrl.value) {
|
|
782
|
+
playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
loadAdbPreview();
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const startAppiumPreviewTimer = () => {
|
|
789
|
+
if (appiumPreviewTimer) return;
|
|
790
|
+
loadAdbPreview();
|
|
791
|
+
appiumPreviewTimer = window.setInterval(loadAdbPreview, 1000);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const stopAppiumPreviewTimer = () => {
|
|
795
|
+
if (!appiumPreviewTimer) return;
|
|
796
|
+
window.clearInterval(appiumPreviewTimer);
|
|
797
|
+
appiumPreviewTimer = null;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const loadPlaygroundStatus = async () => {
|
|
801
|
+
if (!playgroundDeviceId.value) {
|
|
802
|
+
playgroundAvailable.value = false;
|
|
803
|
+
playgroundPreviewError.value = '';
|
|
804
|
+
playgroundFrameUrl.value = '';
|
|
805
|
+
playgroundFrameSignature.value = '';
|
|
806
|
+
devicePreviewUrl.value = '';
|
|
807
|
+
devicePreviewMode.value = '';
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
await loadDeviceInterface();
|
|
813
|
+
const payload = await api.getPlaygroundStatus();
|
|
814
|
+
|
|
815
|
+
const matchedDevice = !payload.deviceId || payload.deviceId === playgroundDeviceId.value;
|
|
816
|
+
const hasRealtimeStream = payload.previewKind === 'scrcpy' && payload.sessionConnected === true;
|
|
817
|
+
const hasPlayground = Boolean(payload.available) && matchedDevice && (hasRealtimeStream || !payload.previewError);
|
|
818
|
+
playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
|
|
819
|
+
playgroundPreviewError.value =
|
|
820
|
+
matchedDevice && !hasRealtimeStream
|
|
821
|
+
? payload.previewError || payload.setupState || ''
|
|
822
|
+
: `未找到设备 ${playgroundDeviceId.value} 对应的 Playground 实例`;
|
|
823
|
+
|
|
824
|
+
const nextSignature = `${payload.url || ''}::${playgroundDeviceId.value}`;
|
|
825
|
+
if (hasPlayground) {
|
|
826
|
+
if (nextSignature !== playgroundFrameSignature.value) {
|
|
827
|
+
playgroundFrameSignature.value = nextSignature;
|
|
828
|
+
playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
|
|
829
|
+
}
|
|
830
|
+
if (activeMenu.value === 'appium') {
|
|
831
|
+
if (devicePreviewMode.value !== 'screenshot' || !devicePreviewUrl.value) {
|
|
832
|
+
loadAdbPreview();
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
devicePreviewUrl.value = '';
|
|
836
|
+
devicePreviewMode.value = 'stream';
|
|
837
|
+
}
|
|
838
|
+
playgroundPreviewError.value = '';
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (!hasPlayground) {
|
|
842
|
+
playgroundFrameUrl.value = '';
|
|
843
|
+
playgroundFrameSignature.value = '';
|
|
844
|
+
loadAdbPreview();
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
playgroundAvailable.value = false;
|
|
848
|
+
playgroundFrameUrl.value = '';
|
|
849
|
+
playgroundFrameSignature.value = '';
|
|
850
|
+
loadAdbPreview();
|
|
851
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '设备预览不可用';
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const tapDevice = async (x: number, y: number) => {
|
|
856
|
+
deviceDebug.action = 'tap';
|
|
857
|
+
deviceDebug.status = 'pending';
|
|
858
|
+
try {
|
|
859
|
+
await api.tapAndroid({
|
|
860
|
+
deviceId: playgroundDeviceId.value,
|
|
861
|
+
x,
|
|
862
|
+
y,
|
|
863
|
+
});
|
|
864
|
+
deviceDebug.status = 'ok';
|
|
865
|
+
deviceDebug.message = 'tap sent';
|
|
866
|
+
refreshAdbPreviewAfterInput();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
deviceDebug.status = 'error';
|
|
869
|
+
deviceDebug.message = error instanceof Error ? error.message : '点击失败';
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const swipeDevice = async (startX: number, startY: number, endX: number, endY: number, duration = 120) => {
|
|
875
|
+
deviceDebug.action = 'swipe';
|
|
876
|
+
deviceDebug.status = 'pending';
|
|
877
|
+
try {
|
|
878
|
+
await api.swipeAndroid({
|
|
879
|
+
deviceId: playgroundDeviceId.value,
|
|
880
|
+
startX,
|
|
881
|
+
startY,
|
|
882
|
+
endX,
|
|
883
|
+
endY,
|
|
884
|
+
duration,
|
|
885
|
+
});
|
|
886
|
+
deviceDebug.status = 'ok';
|
|
887
|
+
deviceDebug.message = 'swipe sent';
|
|
888
|
+
refreshAdbPreviewAfterInput();
|
|
889
|
+
} catch (error) {
|
|
890
|
+
deviceDebug.status = 'error';
|
|
891
|
+
deviceDebug.message = error instanceof Error ? error.message : '滑动失败';
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const switchAndroidDevice = async (deviceId: string) => {
|
|
897
|
+
try {
|
|
898
|
+
const payload = await api.setAndroidDevice({ deviceId });
|
|
899
|
+
playgroundDeviceId.value = payload.currentDeviceId || deviceId;
|
|
900
|
+
await loadPlaygroundStatus();
|
|
901
|
+
} catch (error) {
|
|
902
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '切换设备失败';
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
const triggerDeviceKey = async (keyCode: number) => {
|
|
907
|
+
if (!playgroundDeviceId.value) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
await api.sendAndroidKeyevent({
|
|
912
|
+
deviceId: playgroundDeviceId.value,
|
|
913
|
+
keyCode,
|
|
914
|
+
});
|
|
915
|
+
refreshAdbPreviewAfterInput();
|
|
916
|
+
} catch (error) {
|
|
917
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '操作失败';
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const buildExecutionProcess = (script: SavedScript): ExecutionStep[] => {
|
|
922
|
+
const activeSteps = (script.steps || []).filter((step) => step.enabled !== false);
|
|
923
|
+
if (!activeSteps.length) {
|
|
924
|
+
return [
|
|
925
|
+
{
|
|
926
|
+
id: 'script',
|
|
927
|
+
sourceIndex: 0,
|
|
928
|
+
title: script.promptTitle || script.name,
|
|
929
|
+
method: 'script',
|
|
930
|
+
prompt: script.filePath,
|
|
931
|
+
status: 'pending',
|
|
932
|
+
detail: '等待执行脚本',
|
|
933
|
+
},
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return activeSteps
|
|
938
|
+
.map((step, index) => ({
|
|
939
|
+
id: step.id || `${script.id}-${index}`,
|
|
940
|
+
sourceIndex: index,
|
|
941
|
+
title: step.label || `步骤 ${index + 1}`,
|
|
942
|
+
method: step.type,
|
|
943
|
+
prompt: step.prompt || step.value || '',
|
|
944
|
+
status: 'pending' as const,
|
|
945
|
+
detail: '等待前置步骤完成',
|
|
946
|
+
}))
|
|
947
|
+
.filter((step) => step.title !== '统一处理弹窗');
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const markExecutionProcess = (success: boolean, output: string) => {
|
|
951
|
+
if (!executionProcess.value.length) return;
|
|
952
|
+
const runningIndex = executionProcess.value.findIndex((step) => step.status === 'running');
|
|
953
|
+
const errorIndex = executionProcess.value.findIndex((step) => step.status === 'error');
|
|
954
|
+
const failedIndex = success
|
|
955
|
+
? -1
|
|
956
|
+
: runningIndex >= 0
|
|
957
|
+
? runningIndex
|
|
958
|
+
: errorIndex >= 0
|
|
959
|
+
? errorIndex
|
|
960
|
+
: Math.max(0, executionProcess.value.length - 1);
|
|
961
|
+
executionProcess.value = executionProcess.value.map((step, index) => {
|
|
962
|
+
if (success || index < failedIndex) {
|
|
963
|
+
return { ...step, status: 'success', detail: '执行完成' };
|
|
964
|
+
}
|
|
965
|
+
if (index === failedIndex) {
|
|
966
|
+
return {
|
|
967
|
+
...step,
|
|
968
|
+
status: 'error',
|
|
969
|
+
detail: output || '执行失败',
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return step;
|
|
973
|
+
});
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const formatElapsed = (startedAt: number) => `${Math.max(0, Math.floor((Date.now() - startedAt) / 1000))} 秒`;
|
|
977
|
+
const formatCompactDuration = (milliseconds: number) => {
|
|
978
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
979
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
980
|
+
const seconds = totalSeconds % 60;
|
|
981
|
+
return minutes ? `${minutes}m${seconds}s` : `${seconds}s`;
|
|
982
|
+
};
|
|
983
|
+
const runningElapsedText = computed(() => {
|
|
984
|
+
if (lastRunStatus.value !== '执行中' || !runStartedAt.value) return '';
|
|
985
|
+
return formatCompactDuration(runningElapsedNow.value - runStartedAt.value);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
const updateRunningStepElapsed = () => {
|
|
989
|
+
if (runStartedAt.value) {
|
|
990
|
+
runningElapsedNow.value = Date.now();
|
|
991
|
+
}
|
|
992
|
+
executionProcess.value = executionProcess.value.map((step) => {
|
|
993
|
+
if (step.status !== 'running' || !step.startedAt) {
|
|
994
|
+
return step;
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
...step,
|
|
998
|
+
detail: `执行中,已耗时 ${formatElapsed(step.startedAt)}`,
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const startExecutionProgressTimer = () => {
|
|
1004
|
+
if (executionProgressTimer) {
|
|
1005
|
+
window.clearInterval(executionProgressTimer);
|
|
1006
|
+
}
|
|
1007
|
+
executionProgressTimer = window.setInterval(updateRunningStepElapsed, 1000);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
const stopExecutionProgressTimer = () => {
|
|
1011
|
+
if (!executionProgressTimer) return;
|
|
1012
|
+
window.clearInterval(executionProgressTimer);
|
|
1013
|
+
executionProgressTimer = null;
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const appendExecutionStepLog = (event: Extract<RunScriptStreamEvent, { type: 'step' }>) => {
|
|
1017
|
+
const title = event.title || `步骤 ${event.index + 1}`;
|
|
1018
|
+
if (event.status === 'start') {
|
|
1019
|
+
executionLog.value += `[步骤 ${event.index + 1}] 开始:${title}\n`;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (event.status === 'success') {
|
|
1023
|
+
executionLog.value += `[步骤 ${event.index + 1}] 完成:${title}\n`;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
executionLog.value += `[步骤 ${event.index + 1}] 失败:${title}${event.detail ? `\n${event.detail}` : ''}\n`;
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const applyRunScriptEvent = (event: RunScriptStreamEvent) => {
|
|
1030
|
+
if (event.type === 'output') {
|
|
1031
|
+
executionLog.value += event.chunk;
|
|
1032
|
+
if (!lastRunStatus.value) {
|
|
1033
|
+
lastRunStatus.value = '执行中';
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (event.type === 'error') {
|
|
1039
|
+
executionLog.value += `${event.message}\n`;
|
|
1040
|
+
markExecutionProcess(false, event.message);
|
|
1041
|
+
lastRunStatus.value = '执行失败';
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (event.type === 'step') {
|
|
1046
|
+
appendExecutionStepLog(event);
|
|
1047
|
+
executionProcess.value = executionProcess.value.map((step) => {
|
|
1048
|
+
if (step.sourceIndex !== event.index) {
|
|
1049
|
+
return step;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (event.status === 'start') {
|
|
1053
|
+
const startedAt = Date.now();
|
|
1054
|
+
return { ...step, status: 'running', startedAt, detail: `执行中,已耗时 ${formatElapsed(startedAt)}` };
|
|
1055
|
+
}
|
|
1056
|
+
if (event.status === 'success') {
|
|
1057
|
+
return { ...step, status: 'success', startedAt: undefined, detail: '执行完成' };
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
...step,
|
|
1061
|
+
status: 'error',
|
|
1062
|
+
startedAt: undefined,
|
|
1063
|
+
detail: event.detail || '执行失败',
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (event.type === 'done') {
|
|
1070
|
+
if (event.output && !executionLog.value.trim()) {
|
|
1071
|
+
executionLog.value = event.output;
|
|
1072
|
+
}
|
|
1073
|
+
markExecutionProcess(event.success, event.output || executionLog.value);
|
|
1074
|
+
lastRunStatus.value = event.success ? '执行完成' : event.output.includes('脚本执行已停止') ? '已停止' : '执行失败';
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const runSelectedScript = async () => {
|
|
1079
|
+
if (isRunningScript.value) return;
|
|
1080
|
+
|
|
1081
|
+
if (!selectedScript.value) {
|
|
1082
|
+
lastRunStatus.value = '请先选择脚本';
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const modelConfigError = getMidsceneModelConfigError();
|
|
1087
|
+
if (modelConfigError) {
|
|
1088
|
+
lastRunStatus.value = '模型配置不完整';
|
|
1089
|
+
executionLog.value = modelConfigError;
|
|
1090
|
+
ElMessage.error('Midscene 模型配置不完整');
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
isRunningScript.value = true;
|
|
1095
|
+
isStoppingScript.value = false;
|
|
1096
|
+
runStartedAt.value = Date.now();
|
|
1097
|
+
runningElapsedNow.value = runStartedAt.value;
|
|
1098
|
+
executionLog.value = '';
|
|
1099
|
+
lastRunStatus.value = '执行中';
|
|
1100
|
+
|
|
1101
|
+
try {
|
|
1102
|
+
await loadSavedScripts(selectedScriptId.value);
|
|
1103
|
+
const script = selectedScript.value;
|
|
1104
|
+
if (!script) {
|
|
1105
|
+
throw new Error('脚本不存在');
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
executionProcess.value = buildExecutionProcess(script);
|
|
1109
|
+
if (executionProcess.value[0]) {
|
|
1110
|
+
const startedAt = Date.now();
|
|
1111
|
+
executionProcess.value[0].status = 'running';
|
|
1112
|
+
executionProcess.value[0].startedAt = startedAt;
|
|
1113
|
+
executionProcess.value[0].detail = `执行中,已耗时 ${formatElapsed(startedAt)}`;
|
|
1114
|
+
}
|
|
1115
|
+
startExecutionProgressTimer();
|
|
1116
|
+
|
|
1117
|
+
scriptRunAbortController = new AbortController();
|
|
1118
|
+
await api.runScript(
|
|
1119
|
+
{
|
|
1120
|
+
code: script.code,
|
|
1121
|
+
scriptName: script.name,
|
|
1122
|
+
deviceId: playgroundDeviceId.value,
|
|
1123
|
+
steps: script.steps || [],
|
|
1124
|
+
signal: scriptRunAbortController.signal,
|
|
1125
|
+
},
|
|
1126
|
+
applyRunScriptEvent,
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
if (!lastRunStatus.value) {
|
|
1130
|
+
lastRunStatus.value = '执行完成';
|
|
1131
|
+
}
|
|
1132
|
+
if (!executionLog.value.trim()) {
|
|
1133
|
+
executionLog.value = '执行完成,无输出';
|
|
1134
|
+
}
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
const isAbortError = error instanceof DOMException && error.name === 'AbortError';
|
|
1137
|
+
executionLog.value = isAbortError ? '脚本执行已停止。' : error instanceof Error ? error.message : '执行失败';
|
|
1138
|
+
markExecutionProcess(false, executionLog.value);
|
|
1139
|
+
lastRunStatus.value = isAbortError ? '已停止' : '执行失败';
|
|
1140
|
+
} finally {
|
|
1141
|
+
stopExecutionProgressTimer();
|
|
1142
|
+
await loadModelUsageRecords().catch(() => undefined);
|
|
1143
|
+
isRunningScript.value = false;
|
|
1144
|
+
isStoppingScript.value = false;
|
|
1145
|
+
runStartedAt.value = null;
|
|
1146
|
+
runningElapsedNow.value = 0;
|
|
1147
|
+
scriptRunAbortController = null;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
const stopSelectedScript = async () => {
|
|
1152
|
+
if (!isRunningScript.value || isStoppingScript.value) return;
|
|
1153
|
+
|
|
1154
|
+
try {
|
|
1155
|
+
await ElMessageBox.confirm(
|
|
1156
|
+
'停止后当前脚本进程会立即终止,未完成步骤不会继续执行。确定停止吗?',
|
|
1157
|
+
'确认停止执行',
|
|
1158
|
+
{
|
|
1159
|
+
confirmButtonText: '停止执行',
|
|
1160
|
+
cancelButtonText: '继续执行',
|
|
1161
|
+
type: 'warning',
|
|
1162
|
+
confirmButtonClass: 'el-button--danger',
|
|
1163
|
+
},
|
|
1164
|
+
);
|
|
1165
|
+
} catch {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
isStoppingScript.value = true;
|
|
1170
|
+
lastRunStatus.value = '停止中';
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
await api.stopScript();
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
executionLog.value += `${error instanceof Error ? error.message : '停止执行失败'}\n`;
|
|
1176
|
+
scriptRunAbortController?.abort();
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
watch(
|
|
1181
|
+
() => generatedCode.value,
|
|
1182
|
+
() => {
|
|
1183
|
+
if (!selectedScriptId.value && savedScripts.value.length) {
|
|
1184
|
+
selectedScriptId.value = savedScripts.value[0].id;
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
watch(activeMenu, (menu) => {
|
|
1190
|
+
if (menu === 'appium') {
|
|
1191
|
+
startAppiumPreviewTimer();
|
|
1192
|
+
} else if (menu === 'automation') {
|
|
1193
|
+
stopAppiumPreviewTimer();
|
|
1194
|
+
void loadPlaygroundStatus();
|
|
1195
|
+
} else {
|
|
1196
|
+
stopAppiumPreviewTimer();
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
onMounted(async () => {
|
|
1201
|
+
try {
|
|
1202
|
+
await migrateLegacySavedScripts();
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
console.warn('脚本缓存迁移失败', error);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts(), loadModelUsageRecords()]);
|
|
1208
|
+
await loadPlaygroundStatus();
|
|
1209
|
+
playgroundPollTimer = window.setInterval(() => {
|
|
1210
|
+
void loadAndroidDevices();
|
|
1211
|
+
void loadPlaygroundStatus();
|
|
1212
|
+
}, 2500);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
onUnmounted(() => {
|
|
1216
|
+
if (playgroundPollTimer) {
|
|
1217
|
+
window.clearInterval(playgroundPollTimer);
|
|
1218
|
+
}
|
|
1219
|
+
stopAppiumPreviewTimer();
|
|
1220
|
+
stopExecutionProgressTimer();
|
|
1221
|
+
scriptRunAbortController?.abort();
|
|
1222
|
+
});
|
|
1223
|
+
</script>
|
|
1224
|
+
|
|
1225
|
+
<template>
|
|
1226
|
+
<div class="layout">
|
|
1227
|
+
<el-dialog
|
|
1228
|
+
v-model="actionDialog.visible"
|
|
1229
|
+
:title="actionDialog.title"
|
|
1230
|
+
width="360px"
|
|
1231
|
+
:close-on-click-modal="false"
|
|
1232
|
+
:close-on-press-escape="!actionDialog.loading"
|
|
1233
|
+
:show-close="!actionDialog.loading"
|
|
1234
|
+
class="action-dialog"
|
|
1235
|
+
>
|
|
1236
|
+
<div v-if="actionDialog.loading" class="action-dialog__body">
|
|
1237
|
+
<el-icon class="action-dialog__spinner"><Loading /></el-icon>
|
|
1238
|
+
<span>处理中...</span>
|
|
1239
|
+
</div>
|
|
1240
|
+
<div v-else-if="actionDialog.error" class="action-dialog__error">
|
|
1241
|
+
{{ actionDialog.error }}
|
|
1242
|
+
</div>
|
|
1243
|
+
<template #footer>
|
|
1244
|
+
<el-button v-if="!actionDialog.loading" type="primary" @click="closeActionDialog">关闭</el-button>
|
|
1245
|
+
</template>
|
|
1246
|
+
</el-dialog>
|
|
1247
|
+
|
|
1248
|
+
<el-dialog
|
|
1249
|
+
v-model="codeDialog.visible"
|
|
1250
|
+
:title="codeDialog.title"
|
|
1251
|
+
width="760px"
|
|
1252
|
+
class="code-dialog"
|
|
1253
|
+
>
|
|
1254
|
+
<div class="code-dialog__toolbar">
|
|
1255
|
+
<el-button :icon="CopyDocument" @click="copyDialogCode">复制</el-button>
|
|
1256
|
+
<el-button v-if="!codeDialog.editing" type="primary" :icon="Edit" @click="editCodeDialog">
|
|
1257
|
+
编辑
|
|
1258
|
+
</el-button>
|
|
1259
|
+
<template v-else>
|
|
1260
|
+
<el-button :icon="RefreshLeft" :disabled="codeDialog.saving" @click="undoCodeDialogChanges">
|
|
1261
|
+
撤回修改
|
|
1262
|
+
</el-button>
|
|
1263
|
+
<el-button type="primary" :loading="codeDialog.saving" @click="saveCodeDialog">
|
|
1264
|
+
保存
|
|
1265
|
+
</el-button>
|
|
1266
|
+
</template>
|
|
1267
|
+
</div>
|
|
1268
|
+
<el-input
|
|
1269
|
+
v-if="codeDialog.editing"
|
|
1270
|
+
v-model="codeDialog.draftCode"
|
|
1271
|
+
type="textarea"
|
|
1272
|
+
resize="none"
|
|
1273
|
+
class="code-dialog__editor"
|
|
1274
|
+
/>
|
|
1275
|
+
<pre v-else class="code-block code-dialog__body"><code>{{ codeDialog.code || '暂无代码' }}</code></pre>
|
|
1276
|
+
</el-dialog>
|
|
1277
|
+
|
|
1278
|
+
<aside class="layout-sidebar">
|
|
1279
|
+
<div class="layout-sidebar__top">
|
|
1280
|
+
<div class="layout-sidebar__logo">MS</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
<el-menu :default-active="activeMenu" :default-openeds="['midscene']" class="layout-menu" @select="selectMenu">
|
|
1283
|
+
<el-sub-menu index="midscene">
|
|
1284
|
+
<template #title>
|
|
1285
|
+
<el-icon><Operation /></el-icon>
|
|
1286
|
+
<span>Midscene</span>
|
|
1287
|
+
</template>
|
|
1288
|
+
<el-menu-item v-for="item in menuItems" :key="item.key" :index="item.key">
|
|
1289
|
+
<el-icon><component :is="item.icon" /></el-icon>
|
|
1290
|
+
<span>{{ item.label }}</span>
|
|
1291
|
+
</el-menu-item>
|
|
1292
|
+
</el-sub-menu>
|
|
1293
|
+
<el-menu-item index="appium">
|
|
1294
|
+
<el-icon><Monitor /></el-icon>
|
|
1295
|
+
<span>Appium</span>
|
|
1296
|
+
</el-menu-item>
|
|
1297
|
+
<el-menu-item index="config">
|
|
1298
|
+
<el-icon><Setting /></el-icon>
|
|
1299
|
+
<span>参数配置</span>
|
|
1300
|
+
</el-menu-item>
|
|
1301
|
+
</el-menu>
|
|
1302
|
+
</aside>
|
|
1303
|
+
|
|
1304
|
+
<div class="layout-main">
|
|
1305
|
+
<header class="layout-header">
|
|
1306
|
+
<div class="layout-header__title">
|
|
1307
|
+
{{ activeMenuLabel }}
|
|
1308
|
+
</div>
|
|
1309
|
+
<div class="layout-header__actions">
|
|
1310
|
+
<el-button
|
|
1311
|
+
v-if="activeMenu === 'generator'"
|
|
1312
|
+
type="primary"
|
|
1313
|
+
:icon="Check"
|
|
1314
|
+
:loading="isGenerating"
|
|
1315
|
+
@click="generateWithModel"
|
|
1316
|
+
>
|
|
1317
|
+
AI 生成
|
|
1318
|
+
</el-button>
|
|
1319
|
+
<el-button
|
|
1320
|
+
v-if="activeMenu === 'generator'"
|
|
1321
|
+
:icon="Check"
|
|
1322
|
+
:disabled="!showGeneratedCode || generatedCodeEditing"
|
|
1323
|
+
@click="saveCurrentScript"
|
|
1324
|
+
>
|
|
1325
|
+
保存脚本
|
|
1326
|
+
</el-button>
|
|
1327
|
+
<el-button
|
|
1328
|
+
v-if="activeMenu === 'automation' && !isRunningScript"
|
|
1329
|
+
type="primary"
|
|
1330
|
+
:icon="VideoPlay"
|
|
1331
|
+
@click="runSelectedScript"
|
|
1332
|
+
>
|
|
1333
|
+
开始执行
|
|
1334
|
+
</el-button>
|
|
1335
|
+
<el-button
|
|
1336
|
+
v-if="activeMenu === 'automation' && isRunningScript"
|
|
1337
|
+
type="danger"
|
|
1338
|
+
:icon="VideoPause"
|
|
1339
|
+
:loading="isStoppingScript"
|
|
1340
|
+
@click="stopSelectedScript"
|
|
1341
|
+
>
|
|
1342
|
+
{{ isStoppingScript ? '停止中' : '停止执行' }}
|
|
1343
|
+
</el-button>
|
|
1344
|
+
</div>
|
|
1345
|
+
</header>
|
|
1346
|
+
|
|
1347
|
+
<main
|
|
1348
|
+
class="page-body"
|
|
1349
|
+
:class="{ 'page-body--fixed': activeMenu === 'generator' && activeGeneratorMode === 'ai' }"
|
|
1350
|
+
>
|
|
1351
|
+
<el-alert v-if="activeMenu !== 'appium' && errorMessage" type="error" :closable="false" show-icon class="page-alert">
|
|
1352
|
+
<template #title>{{ errorMessage }}</template>
|
|
1353
|
+
</el-alert>
|
|
1354
|
+
|
|
1355
|
+
<GeneratorPage
|
|
1356
|
+
v-show="activeMenu === 'generator'"
|
|
1357
|
+
v-model:mode="activeGeneratorMode"
|
|
1358
|
+
v-model:source-prompt="sourcePrompt"
|
|
1359
|
+
v-model:generated-code-draft="generatedCodeDraft"
|
|
1360
|
+
:form="form"
|
|
1361
|
+
:app-presets="appPresets"
|
|
1362
|
+
:prompt-example="promptDocument.trim()"
|
|
1363
|
+
:steps="steps"
|
|
1364
|
+
:generated-code="generatedCode"
|
|
1365
|
+
:show-generated-code="showGeneratedCode"
|
|
1366
|
+
:generated-code-editing="generatedCodeEditing"
|
|
1367
|
+
:generated-code-edit-saving="generatedCodeEditSaving"
|
|
1368
|
+
:importing-test-case="importingTestCase"
|
|
1369
|
+
:imported-test-case-file-name="importedTestCaseFileName"
|
|
1370
|
+
@add-step="addStep"
|
|
1371
|
+
@remove-step="removeStep"
|
|
1372
|
+
@copy-code="copyCode"
|
|
1373
|
+
@edit-code="editGeneratedCode"
|
|
1374
|
+
@undo-code="undoGeneratedCodeChanges"
|
|
1375
|
+
@save-code="saveGeneratedCodeChanges"
|
|
1376
|
+
@import-test-case="importTestCase"
|
|
1377
|
+
@clear-all="clearGeneratorPage"
|
|
1378
|
+
/>
|
|
1379
|
+
|
|
1380
|
+
<ConfigPage
|
|
1381
|
+
v-show="activeMenu === 'config'"
|
|
1382
|
+
:config-form="configForm"
|
|
1383
|
+
:app-presets="appPresets"
|
|
1384
|
+
:app-preset-form="appPresetForm"
|
|
1385
|
+
:testing-model-key="testingModelKey"
|
|
1386
|
+
:is-saving-model-config="isSavingModelConfig"
|
|
1387
|
+
:is-saving-app-preset="isSavingAppPreset"
|
|
1388
|
+
:model-test-status="modelTestStatus"
|
|
1389
|
+
:model-usage-records="modelUsageRecords"
|
|
1390
|
+
@test-model="testModel"
|
|
1391
|
+
@save-model-config="saveModelConfig"
|
|
1392
|
+
@save-app-preset="saveAppPreset"
|
|
1393
|
+
@edit-app-preset="editAppPreset"
|
|
1394
|
+
@delete-app-preset="removeAppPreset"
|
|
1395
|
+
@update-midscene-model-provider="updateMidsceneModelProvider"
|
|
1396
|
+
@apply-midscene-model-preset="applyMidsceneModelPreset"
|
|
1397
|
+
/>
|
|
1398
|
+
|
|
1399
|
+
<AutomationPage
|
|
1400
|
+
v-show="activeMenu === 'automation'"
|
|
1401
|
+
v-model:selected-script-id="selectedScriptId"
|
|
1402
|
+
:saved-scripts="savedScripts"
|
|
1403
|
+
:selected-script="selectedScript"
|
|
1404
|
+
:playground-available="playgroundAvailable"
|
|
1405
|
+
:playground-device-id="playgroundDeviceId"
|
|
1406
|
+
:playground-frame-url="activeMenu === 'automation' ? playgroundFrameUrl : ''"
|
|
1407
|
+
:playground-preview-error="playgroundPreviewError"
|
|
1408
|
+
:device-preview-url="activeMenu === 'automation' ? devicePreviewUrl : ''"
|
|
1409
|
+
:android-devices="androidDevices"
|
|
1410
|
+
:device-actions="deviceActions"
|
|
1411
|
+
:device-width="deviceInterfaceSize.width"
|
|
1412
|
+
:device-height="deviceInterfaceSize.height"
|
|
1413
|
+
:execution-log="executionLog"
|
|
1414
|
+
:execution-process="executionProcess"
|
|
1415
|
+
:last-run-status="lastRunStatus"
|
|
1416
|
+
:running-elapsed-text="runningElapsedText"
|
|
1417
|
+
:format-script-time="formatScriptTime"
|
|
1418
|
+
:switch-android-device="switchAndroidDevice"
|
|
1419
|
+
:trigger-device-key="triggerDeviceKey"
|
|
1420
|
+
:refresh-device-preview="refreshDevicePreview"
|
|
1421
|
+
:open-code-dialog="openCodeDialog"
|
|
1422
|
+
:remove-saved-script="removeSavedScript"
|
|
1423
|
+
:tap-device="tapDevice"
|
|
1424
|
+
:swipe-device="swipeDevice"
|
|
1425
|
+
@copy-execution-log="copyExecutionLog"
|
|
1426
|
+
@clear-execution-log="clearExecutionLog"
|
|
1427
|
+
/>
|
|
1428
|
+
|
|
1429
|
+
<AppiumPage
|
|
1430
|
+
v-show="activeMenu === 'appium'"
|
|
1431
|
+
:active="activeMenu === 'appium'"
|
|
1432
|
+
:app-presets="appPresets"
|
|
1433
|
+
:device-actions="deviceActions"
|
|
1434
|
+
:playground-available="playgroundAvailable"
|
|
1435
|
+
:playground-device-id="playgroundDeviceId"
|
|
1436
|
+
:playground-frame-url="''"
|
|
1437
|
+
:playground-preview-error="playgroundPreviewError"
|
|
1438
|
+
:device-preview-url="activeMenu === 'appium' ? devicePreviewUrl : ''"
|
|
1439
|
+
:android-devices="androidDevices"
|
|
1440
|
+
:device-width="deviceInterfaceSize.width"
|
|
1441
|
+
:device-height="deviceInterfaceSize.height"
|
|
1442
|
+
:switch-android-device="switchAndroidDevice"
|
|
1443
|
+
:trigger-device-key="triggerDeviceKey"
|
|
1444
|
+
:refresh-device-preview="refreshDevicePreview"
|
|
1445
|
+
:swipe-device="swipeDevice"
|
|
1446
|
+
/>
|
|
1447
|
+
</main>
|
|
1448
|
+
</div>
|
|
1449
|
+
</div>
|
|
1450
|
+
</template>
|