android-midscene-automation 0.1.43 → 0.2.2
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/{AI_RECOGNITION_GUIDE.md → AI/350/257/206/345/210/253/346/223/215/344/275/234/350/257/264/346/230/216.md} +78 -6
- package/CHANGELOG.md +13 -0
- package/FAQ.md +8 -8
- package/README.md +143 -17
- package/USAGE.md +120 -8
- package/package.json +3 -2
- package/server/analytics.test.ts +46 -0
- package/server/analytics.ts +24 -0
- package/server/api-safety-analytics.test.ts +57 -0
- package/server/appium-auto-save.test.ts +6 -0
- package/server/appium-recorder/ai-recognition.test.ts +27 -0
- package/server/appium-recorder/ai-recognition.ts +16 -4
- package/server/appium-recorder/appium-runner.ts +11 -1
- package/server/appium-recorder/appium-version.test.ts +55 -0
- package/server/appium-recorder/managed-appium.ts +37 -1
- package/server/appium-recorder/repository.ts +6 -2
- package/server/appium-recorder/routes.ts +2 -12
- package/server/appium-recorder/run-history.ts +11 -0
- package/server/config-access.test.ts +27 -0
- package/server/config-access.ts +27 -0
- package/server/http-api.ts +63 -54
- package/server/operations/repository.ts +8 -0
- package/server/remote-agents/routes.ts +2 -12
- package/server/request-body.test.ts +40 -0
- package/server/request-body.ts +48 -0
- package/src/App.vue +98 -742
- package/src/analytics/summary.ts +75 -0
- package/src/api.ts +11 -0
- package/src/appium-recorder/AppiumPage.vue +4 -1
- package/src/appium-recorder/ai-recognition.ts +7 -0
- package/src/appium-recorder/components/AiRecognitionSettings.vue +27 -0
- package/src/appium-recorder/flow-labels.ts +1 -1
- package/src/appium-recorder/types.ts +2 -1
- package/src/components/AppSidebar.vue +31 -0
- package/src/components/analytics/AnalysisCharts.vue +189 -0
- package/src/components/analytics/ReportAnalysis.vue +78 -0
- package/src/components/config/AiPromptPresetsSettings.vue +55 -5
- package/src/components/config/AppiumConfigPanel.vue +4 -1
- package/src/components/config/AppiumModelSettings.vue +1 -1
- package/src/components/config/BaseConfigPanel.vue +6 -2
- package/src/components/config/MidsceneConfigPanel.vue +4 -5
- package/src/components/help/HelpCenter.vue +185 -0
- package/src/components/help/MarkdownDocument.vue +139 -0
- package/src/composables/use-device-preview.ts +279 -0
- package/src/composables/use-initial-resource.ts +17 -0
- package/src/composables/use-script-execution.ts +277 -0
- package/src/config/credentials.ts +2 -0
- package/src/config/use-appium-auto-save.ts +22 -11
- package/src/config/use-model-config.ts +157 -0
- package/src/main.ts +2 -2
- package/src/pages/AnalysisPage.vue +60 -0
- package/src/pages/ConfigPage.vue +36 -5
- package/src/style.css +716 -91
- package/src/types.ts +7 -1
- package//346/265/201/347/250/213/350/212/202/347/202/271/346/223/215/344/275/234/350/257/264/346/230/216.md +408 -0
- package/src/ui-refresh.css +0 -677
package/src/App.vue
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { isAiRecognitionModelConfigured } from './appium-recorder/ai-recognition';
|
|
4
|
-
import { resolveAiDeduplication } from './appium-recorder/ai-deduplication';
|
|
5
|
-
import { resolveAiPromptPresets } from './appium-recorder/ai-prompt-presets';
|
|
6
|
-
import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue';
|
|
2
|
+
import { computed, defineAsyncComponent, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue';
|
|
7
3
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
8
4
|
import {
|
|
9
5
|
Check,
|
|
6
|
+
Close,
|
|
7
|
+
DataAnalysis,
|
|
10
8
|
CopyDocument,
|
|
11
9
|
Edit,
|
|
12
10
|
Loading,
|
|
13
11
|
Monitor,
|
|
14
12
|
Operation,
|
|
13
|
+
InfoFilled,
|
|
14
|
+
Fold,
|
|
15
|
+
Expand,
|
|
15
16
|
RefreshLeft,
|
|
16
17
|
Setting,
|
|
17
18
|
VideoPause,
|
|
@@ -29,30 +30,27 @@ import {
|
|
|
29
30
|
buildScript,
|
|
30
31
|
type ScriptStep,
|
|
31
32
|
} from './script-generator';
|
|
32
|
-
import {
|
|
33
|
-
selectMidsceneModelProvider,
|
|
34
|
-
type MidsceneModelProvider,
|
|
35
|
-
} from './config/midscene-model-presets';
|
|
36
33
|
import * as api from './api';
|
|
37
|
-
import {
|
|
38
|
-
import
|
|
39
|
-
import
|
|
40
|
-
import {
|
|
41
|
-
import
|
|
42
|
-
|
|
34
|
+
import { useModelConfig } from './config/use-model-config';
|
|
35
|
+
import { useDevicePreview } from './composables/use-device-preview';
|
|
36
|
+
import { useScriptExecution } from './composables/use-script-execution';
|
|
37
|
+
import { useInitialResource } from './composables/use-initial-resource';
|
|
38
|
+
import AppSidebar from './components/AppSidebar.vue';
|
|
39
|
+
const AnalysisPage = defineAsyncComponent(() => import('./pages/AnalysisPage.vue'));
|
|
40
|
+
const AutomationPage = defineAsyncComponent(() => import('./pages/AutomationPage.vue'));
|
|
41
|
+
const AppiumPage = defineAsyncComponent(() => import('./appium-recorder/AppiumPage.vue'));
|
|
42
|
+
const ConfigPage = defineAsyncComponent(() => import('./pages/ConfigPage.vue'));
|
|
43
|
+
const GeneratorPage = defineAsyncComponent(() => import('./pages/GeneratorPage.vue'));
|
|
44
|
+
const HelpCenter = defineAsyncComponent(() => import('./components/help/HelpCenter.vue'));
|
|
43
45
|
import type {
|
|
44
|
-
AndroidDevice,
|
|
45
|
-
ConfigForm,
|
|
46
46
|
AppPreset,
|
|
47
|
-
ExecutionStep,
|
|
48
47
|
GeneratorMode,
|
|
49
48
|
MenuKey,
|
|
50
|
-
RunScriptStreamEvent,
|
|
51
49
|
SavedScript,
|
|
52
50
|
} from './types';
|
|
53
51
|
|
|
54
52
|
const ACTIVE_MENU_STORAGE_KEY = 'android-midscene-automation:active-menu';
|
|
55
|
-
const menuKeys: MenuKey[] = ['generator', 'automation', 'config', 'appium'];
|
|
53
|
+
const menuKeys: MenuKey[] = ['analysis', 'generator', 'automation', 'config', 'appium', 'about'];
|
|
56
54
|
const storedMenu = window.localStorage.getItem(ACTIVE_MENU_STORAGE_KEY) as MenuKey | null;
|
|
57
55
|
const activeMenu = ref<MenuKey>(storedMenu && menuKeys.includes(storedMenu) ? storedMenu : 'generator');
|
|
58
56
|
const activeGeneratorMode = ref<GeneratorMode>('ai');
|
|
@@ -62,10 +60,7 @@ const promptPresetId = shallowRef(defaultPromptPresetId);
|
|
|
62
60
|
const steps = ref<ScriptStep[]>([]);
|
|
63
61
|
const errorMessage = ref('');
|
|
64
62
|
const isGenerating = ref(false);
|
|
65
|
-
const isSavingModelConfig = ref(false);
|
|
66
63
|
const isSavingAppPreset = ref(false);
|
|
67
|
-
const isRunningScript = ref(false);
|
|
68
|
-
const isStoppingScript = ref(false);
|
|
69
64
|
const showGeneratedCode = ref(false);
|
|
70
65
|
const generatedCodeOverride = shallowRef<string | null>(null);
|
|
71
66
|
const generatedCodeDraft = shallowRef('');
|
|
@@ -73,42 +68,9 @@ const generatedCodeEditing = shallowRef(false);
|
|
|
73
68
|
const generatedCodeEditSaving = shallowRef(false);
|
|
74
69
|
const importingTestCase = shallowRef(false);
|
|
75
70
|
const importedTestCaseFileName = shallowRef('');
|
|
76
|
-
const testingModelKey = ref('');
|
|
77
|
-
const playgroundAvailable = ref(false);
|
|
78
|
-
const playgroundPreviewError = ref('');
|
|
79
|
-
const playgroundDeviceId = ref('');
|
|
80
|
-
const playgroundFrameUrl = ref('');
|
|
81
|
-
const playgroundFrameSignature = ref('');
|
|
82
|
-
const restartingPlaygroundPreview = shallowRef(false);
|
|
83
|
-
const devicePreviewUrl = ref('');
|
|
84
|
-
const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
|
|
85
|
-
const backendOffline = shallowRef(false);
|
|
86
|
-
const androidDevices = ref<AndroidDevice[]>([]);
|
|
87
|
-
const deviceInterfaceSize = reactive({ width: 0, height: 0 });
|
|
88
|
-
const deviceDebug = reactive({
|
|
89
|
-
rawX: 0,
|
|
90
|
-
rawY: 0,
|
|
91
|
-
mappedX: 0,
|
|
92
|
-
mappedY: 0,
|
|
93
|
-
action: '',
|
|
94
|
-
status: 'idle',
|
|
95
|
-
message: '',
|
|
96
|
-
});
|
|
97
71
|
const selectedScriptId = ref('');
|
|
98
|
-
const executionLog = ref('');
|
|
99
|
-
const executionProcess = ref<ExecutionStep[]>([]);
|
|
100
|
-
const lastRunStatus = ref('');
|
|
101
|
-
const runStartedAt = shallowRef<number | null>(null);
|
|
102
|
-
const runningElapsedNow = shallowRef(0);
|
|
103
72
|
const savedScripts = ref<SavedScript[]>([]);
|
|
104
73
|
const appPresets = ref<AppPreset[]>([]);
|
|
105
|
-
const modelTestStatus = reactive({
|
|
106
|
-
midscene: '',
|
|
107
|
-
scriptOptimizer: '',
|
|
108
|
-
appium: '',
|
|
109
|
-
promptOptimizer: '',
|
|
110
|
-
});
|
|
111
|
-
const aiRecognitionModelConfigured = shallowRef(false);
|
|
112
74
|
const actionDialog = reactive({
|
|
113
75
|
visible: false,
|
|
114
76
|
title: '',
|
|
@@ -128,22 +90,7 @@ const codeDialog = reactive({
|
|
|
128
90
|
editing: false,
|
|
129
91
|
saving: false,
|
|
130
92
|
});
|
|
131
|
-
let playgroundPollTimer: number | null = null;
|
|
132
|
-
let devicePreviewTimer: number | null = null;
|
|
133
|
-
let executionProgressTimer: number | null = null;
|
|
134
93
|
let aiGenerateAbortController: AbortController | null = null;
|
|
135
|
-
let scriptRunAbortController: AbortController | null = null;
|
|
136
|
-
|
|
137
|
-
const isBackendFetchError = (error: unknown) => {
|
|
138
|
-
const message = error instanceof Error ? error.message : String(error || '');
|
|
139
|
-
return error instanceof TypeError || /Failed to fetch|NetworkError|Load failed/i.test(message);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const stopPlaygroundPollTimer = () => {
|
|
143
|
-
if (!playgroundPollTimer) return;
|
|
144
|
-
window.clearInterval(playgroundPollTimer);
|
|
145
|
-
playgroundPollTimer = null;
|
|
146
|
-
};
|
|
147
94
|
|
|
148
95
|
const form = reactive({
|
|
149
96
|
promptTitle: '',
|
|
@@ -157,50 +104,29 @@ const appPresetForm = reactive({
|
|
|
157
104
|
packageName: '',
|
|
158
105
|
});
|
|
159
106
|
|
|
160
|
-
const configForm =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
family: '',
|
|
173
|
-
},
|
|
174
|
-
env: {},
|
|
175
|
-
},
|
|
176
|
-
scriptOptimizer: {
|
|
177
|
-
model: {
|
|
178
|
-
baseUrl: '',
|
|
179
|
-
apiKey: '',
|
|
180
|
-
name: '',
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
const lastCustomMidsceneModel = reactive({
|
|
185
|
-
baseUrl: '',
|
|
186
|
-
apiKey: '',
|
|
187
|
-
name: '',
|
|
188
|
-
family: '',
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const appiumAutoSave = useAppiumAutoSave(configForm, async value => {
|
|
192
|
-
await api.saveAppiumConfig(value);
|
|
193
|
-
}, error => {
|
|
194
|
-
void ElMessageBox.alert(error instanceof Error ? error.message : 'Appium 配置自动保存失败', '自动保存失败', { type: 'error' }).catch(() => {});
|
|
195
|
-
});
|
|
107
|
+
const { ready: configReady, configForm, appiumAutoSave, isSavingModelConfig, testingModelKey, modelTestStatus, aiRecognitionModelConfigured, loadConfig, updateMidsceneModelProvider, getMidsceneModelConfigError, saveModelConfig, testModel } = useModelConfig();
|
|
108
|
+
const { connectionError, reconnecting, reconnect, playgroundAvailable, playgroundPreviewError, playgroundDeviceId, playgroundFrameUrl, devicePreviewUrl, androidDevices, deviceInterfaceSize, refreshDevicePreview, tapDevice, swipeDevice, switchAndroidDevice, triggerDeviceKey } = useDevicePreview(activeMenu);
|
|
109
|
+
const { isRunningScript, isStoppingScript, executionLog, executionProcess, lastRunStatus, runningElapsedText, runSelectedScript, stopSelectedScript } = useScriptExecution(computed(() => selectedScript.value), selectedScriptId, playgroundDeviceId, id => loadSavedScripts(id), getMidsceneModelConfigError);
|
|
110
|
+
const visitedMenus = reactive(new Set<MenuKey>([activeMenu.value]));
|
|
111
|
+
const openMenus = ref<MenuKey[]>([...menuKeys]);
|
|
112
|
+
const visibleMenuItems = computed(() => openMenus.value.map(key => menuItems.find(item => item.key === key)!));
|
|
113
|
+
function closeMenu(key: MenuKey) {
|
|
114
|
+
if (openMenus.value.length === 1) return;
|
|
115
|
+
const index = openMenus.value.indexOf(key);
|
|
116
|
+
openMenus.value.splice(index, 1);
|
|
117
|
+
if (activeMenu.value === key) activeMenu.value = openMenus.value[Math.max(0, index - 1)];
|
|
118
|
+
}
|
|
196
119
|
|
|
120
|
+
const sidebarCollapsed = shallowRef(false);
|
|
197
121
|
const menuItems = [
|
|
122
|
+
{ key: 'analysis', label: '分析页', icon: DataAnalysis },
|
|
198
123
|
{ key: 'generator', label: '测试脚本生成', icon: Operation },
|
|
199
124
|
{ key: 'automation', label: '自动化测试', icon: Monitor },
|
|
125
|
+
{ key: 'appium', label: 'Appium', icon: Monitor },
|
|
126
|
+
{ key: 'config', label: '参数配置', icon: Setting },
|
|
127
|
+
{ key: 'about', label: '关于', icon: InfoFilled },
|
|
200
128
|
] as const;
|
|
201
129
|
const activeMenuLabel = computed(() => {
|
|
202
|
-
if (activeMenu.value === 'appium') return 'Appium';
|
|
203
|
-
if (activeMenu.value === 'config') return '参数配置';
|
|
204
130
|
return menuItems.find((item) => item.key === activeMenu.value)?.label || 'Midscene';
|
|
205
131
|
});
|
|
206
132
|
|
|
@@ -663,76 +589,6 @@ const generateWithModel = async () => {
|
|
|
663
589
|
}
|
|
664
590
|
};
|
|
665
591
|
|
|
666
|
-
const loadConfig = async () => {
|
|
667
|
-
const payload = await api.getConfig();
|
|
668
|
-
Object.assign(configForm.runtime, payload.runtime || {});
|
|
669
|
-
Object.assign(configForm.midscene.model, payload.midscene.model);
|
|
670
|
-
Object.keys(configForm.midscene.env).forEach((key) => {
|
|
671
|
-
delete configForm.midscene.env[key];
|
|
672
|
-
});
|
|
673
|
-
Object.assign(configForm.midscene.env, payload.midscene.env || {});
|
|
674
|
-
Object.assign(configForm.scriptOptimizer.model, payload.scriptOptimizer.model);
|
|
675
|
-
Object.assign(configForm.appium.model, payload.appium?.model || { baseUrl: '', apiKey: '', name: '' });
|
|
676
|
-
Object.assign(configForm.appium.promptOptimizer!.model, payload.appium?.promptOptimizer?.model || { baseUrl: '', apiKey: '', name: '' });
|
|
677
|
-
configForm.appium.screenshotReport = payload.appium?.screenshotReport === true;
|
|
678
|
-
configForm.appium.reportSummary = resolveReportSummary(payload.appium?.reportSummary);
|
|
679
|
-
configForm.appium.aiDeduplication = resolveAiDeduplication(payload.appium?.aiDeduplication);
|
|
680
|
-
configForm.appium.aiPromptPresets = resolveAiPromptPresets(payload.appium?.aiPromptPresets);
|
|
681
|
-
configForm.appium.flowBackgroundColor = normalizeFlowBackground(payload.appium?.flowBackgroundColor);
|
|
682
|
-
configForm.appium.flowLineColor = normalizeFlowLineColor(payload.appium?.flowLineColor);
|
|
683
|
-
appiumAutoSave.initialize();
|
|
684
|
-
aiRecognitionModelConfigured.value = isAiRecognitionModelConfigured(configForm.appium.model);
|
|
685
|
-
if (configForm.midscene.model.provider !== 'codex') {
|
|
686
|
-
rememberCustomMidsceneModel();
|
|
687
|
-
}
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
const rememberCustomMidsceneModel = () => {
|
|
691
|
-
if (configForm.midscene.model.provider === 'codex') return;
|
|
692
|
-
lastCustomMidsceneModel.baseUrl = configForm.midscene.model.baseUrl;
|
|
693
|
-
lastCustomMidsceneModel.apiKey = configForm.midscene.model.apiKey;
|
|
694
|
-
lastCustomMidsceneModel.name = configForm.midscene.model.name;
|
|
695
|
-
lastCustomMidsceneModel.family = configForm.midscene.model.family;
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
const updateMidsceneModelProvider = (provider: MidsceneModelProvider) => {
|
|
699
|
-
if (configForm.midscene.model.provider !== 'codex') {
|
|
700
|
-
rememberCustomMidsceneModel();
|
|
701
|
-
}
|
|
702
|
-
Object.assign(configForm.midscene.model, selectMidsceneModelProvider(provider, configForm.midscene.model, lastCustomMidsceneModel));
|
|
703
|
-
modelTestStatus.midscene = '';
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
const getMidsceneModelConfigError = () => {
|
|
707
|
-
const model = configForm.midscene.model;
|
|
708
|
-
const missing = [];
|
|
709
|
-
|
|
710
|
-
if (!model.baseUrl.trim()) missing.push('Base URL');
|
|
711
|
-
if (!model.name.trim()) missing.push('Model Name');
|
|
712
|
-
if (!model.family.trim()) missing.push('Model Family');
|
|
713
|
-
if (model.provider !== 'codex' && !model.apiKey.trim()) missing.push('API Key');
|
|
714
|
-
|
|
715
|
-
if (!missing.length) return '';
|
|
716
|
-
return `Midscene 模型配置不完整:缺少 ${missing.join('、')}。请在“参数配置 > Midscene配置 > Midscene 模型”中选择“使用 Codex”,或补齐自定义提供方后测试并保存。`;
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
const saveModelConfig = async () => {
|
|
720
|
-
isSavingModelConfig.value = true;
|
|
721
|
-
errorMessage.value = '';
|
|
722
|
-
openActionDialog('保存参数配置');
|
|
723
|
-
try {
|
|
724
|
-
await api.saveConfig({ runtime: { ...configForm.runtime } });
|
|
725
|
-
closeActionDialog();
|
|
726
|
-
ElMessage.success('参数配置已保存');
|
|
727
|
-
} catch (error) {
|
|
728
|
-
const message = error instanceof Error ? error.message : '参数配置保存失败';
|
|
729
|
-
errorMessage.value = message;
|
|
730
|
-
failActionDialog(message);
|
|
731
|
-
} finally {
|
|
732
|
-
isSavingModelConfig.value = false;
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
|
|
736
592
|
const loadAppPresets = async () => {
|
|
737
593
|
const payload = await api.getAppPresets();
|
|
738
594
|
appPresets.value = payload.apps || [];
|
|
@@ -798,511 +654,6 @@ const removeAppPreset = async (id: string) => {
|
|
|
798
654
|
}
|
|
799
655
|
};
|
|
800
656
|
|
|
801
|
-
const testModel = async (key: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer') => {
|
|
802
|
-
if (testingModelKey.value) return;
|
|
803
|
-
testingModelKey.value = key;
|
|
804
|
-
errorMessage.value = '';
|
|
805
|
-
modelTestStatus[key] = '';
|
|
806
|
-
const model = { ...(key === 'promptOptimizer'
|
|
807
|
-
? configForm.appium.promptOptimizer!.model
|
|
808
|
-
: configForm[key].model) };
|
|
809
|
-
|
|
810
|
-
try {
|
|
811
|
-
const payload = await api.testModel({ modelKey: key, model, save: true });
|
|
812
|
-
if (!payload.saved) throw new Error('模型未保存,请重试');
|
|
813
|
-
const successMessage = `测试通过,配置已保存${payload.content ? `:${payload.content}` : ''}`;
|
|
814
|
-
modelTestStatus[key] = '';
|
|
815
|
-
if (key === 'appium') aiRecognitionModelConfigured.value = isAiRecognitionModelConfigured(model);
|
|
816
|
-
void ElMessageBox.alert(successMessage, '模型测试成功', {
|
|
817
|
-
type: 'success',
|
|
818
|
-
confirmButtonText: '确定',
|
|
819
|
-
}).catch(() => {});
|
|
820
|
-
} catch (error) {
|
|
821
|
-
modelTestStatus[key] = `未保存:${error instanceof Error ? error.message : '测试失败'}`;
|
|
822
|
-
} finally {
|
|
823
|
-
testingModelKey.value = '';
|
|
824
|
-
}
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
const loadAndroidDevices = async () => {
|
|
828
|
-
if (backendOffline.value) return;
|
|
829
|
-
try {
|
|
830
|
-
const payload = await api.getAndroidDevices();
|
|
831
|
-
backendOffline.value = false;
|
|
832
|
-
androidDevices.value = payload.devices || [];
|
|
833
|
-
playgroundDeviceId.value = payload.currentDeviceId || '';
|
|
834
|
-
playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
|
|
835
|
-
} catch (error) {
|
|
836
|
-
if (isBackendFetchError(error)) {
|
|
837
|
-
markBackendOffline();
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
throw error;
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
|
|
844
|
-
const loadDeviceInterface = async () => {
|
|
845
|
-
try {
|
|
846
|
-
const payload = await api.getAndroidDisplayInfo(playgroundDeviceId.value);
|
|
847
|
-
deviceInterfaceSize.width = Number(payload.width) || 0;
|
|
848
|
-
deviceInterfaceSize.height = Number(payload.height) || 0;
|
|
849
|
-
} catch {
|
|
850
|
-
deviceInterfaceSize.width = 0;
|
|
851
|
-
deviceInterfaceSize.height = 0;
|
|
852
|
-
}
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
const loadAdbPreview = () => {
|
|
856
|
-
if (backendOffline.value) return;
|
|
857
|
-
if (!playgroundDeviceId.value) {
|
|
858
|
-
playgroundFrameUrl.value = '';
|
|
859
|
-
devicePreviewUrl.value = '';
|
|
860
|
-
devicePreviewMode.value = '';
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
playgroundFrameUrl.value = '';
|
|
865
|
-
devicePreviewUrl.value = `${api.APP_BASE}/api/android-preview?deviceId=${encodeURIComponent(playgroundDeviceId.value)}&t=${Date.now()}`;
|
|
866
|
-
devicePreviewMode.value = 'screenshot';
|
|
867
|
-
};
|
|
868
|
-
|
|
869
|
-
const refreshAdbPreviewAfterInput = () => {
|
|
870
|
-
if (devicePreviewMode.value === 'screenshot' && devicePreviewUrl.value) {
|
|
871
|
-
window.setTimeout(loadAdbPreview, 120);
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
const refreshDevicePreview = async () => {
|
|
876
|
-
if (devicePreviewMode.value === 'stream' && playgroundDeviceId.value) {
|
|
877
|
-
if (restartingPlaygroundPreview.value) return;
|
|
878
|
-
restartingPlaygroundPreview.value = true;
|
|
879
|
-
playgroundFrameUrl.value = '';
|
|
880
|
-
playgroundFrameSignature.value = '';
|
|
881
|
-
try {
|
|
882
|
-
await api.restartPlaygroundPreview({ deviceId: playgroundDeviceId.value });
|
|
883
|
-
restartingPlaygroundPreview.value = false;
|
|
884
|
-
await loadPlaygroundStatus();
|
|
885
|
-
} catch (error) {
|
|
886
|
-
playgroundPreviewError.value = error instanceof Error ? error.message : '重启实时预览失败';
|
|
887
|
-
} finally {
|
|
888
|
-
restartingPlaygroundPreview.value = false;
|
|
889
|
-
}
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
loadAdbPreview();
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
const startDevicePreviewTimer = () => {
|
|
896
|
-
if (backendOffline.value) return;
|
|
897
|
-
if (devicePreviewTimer) return;
|
|
898
|
-
loadAdbPreview();
|
|
899
|
-
devicePreviewTimer = window.setInterval(loadAdbPreview, 1000);
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
const stopDevicePreviewTimer = () => {
|
|
903
|
-
if (!devicePreviewTimer) return;
|
|
904
|
-
window.clearInterval(devicePreviewTimer);
|
|
905
|
-
devicePreviewTimer = null;
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
const markBackendOffline = () => {
|
|
909
|
-
if (backendOffline.value) return;
|
|
910
|
-
backendOffline.value = true;
|
|
911
|
-
playgroundAvailable.value = false;
|
|
912
|
-
playgroundFrameUrl.value = '';
|
|
913
|
-
playgroundFrameSignature.value = '';
|
|
914
|
-
devicePreviewUrl.value = '';
|
|
915
|
-
devicePreviewMode.value = '';
|
|
916
|
-
playgroundPreviewError.value = '本地服务已断开,请重新运行启动命令后刷新页面。';
|
|
917
|
-
errorMessage.value = playgroundPreviewError.value;
|
|
918
|
-
stopPlaygroundPollTimer();
|
|
919
|
-
stopDevicePreviewTimer();
|
|
920
|
-
};
|
|
921
|
-
|
|
922
|
-
const loadPlaygroundStatus = async () => {
|
|
923
|
-
if (backendOffline.value || restartingPlaygroundPreview.value) return;
|
|
924
|
-
if (!playgroundDeviceId.value) {
|
|
925
|
-
stopDevicePreviewTimer();
|
|
926
|
-
playgroundAvailable.value = false;
|
|
927
|
-
playgroundPreviewError.value = '';
|
|
928
|
-
playgroundFrameUrl.value = '';
|
|
929
|
-
playgroundFrameSignature.value = '';
|
|
930
|
-
devicePreviewUrl.value = '';
|
|
931
|
-
devicePreviewMode.value = '';
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
try {
|
|
936
|
-
await loadDeviceInterface();
|
|
937
|
-
if (restartingPlaygroundPreview.value) return;
|
|
938
|
-
const payload = await api.getPlaygroundStatus();
|
|
939
|
-
if (restartingPlaygroundPreview.value) return;
|
|
940
|
-
|
|
941
|
-
const matchedDevice = !payload.deviceId || payload.deviceId === playgroundDeviceId.value;
|
|
942
|
-
const hasRealtimeStream = payload.previewKind === 'scrcpy' && payload.sessionConnected === true;
|
|
943
|
-
const hasPlayground = Boolean(payload.available) && matchedDevice && hasRealtimeStream;
|
|
944
|
-
playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
|
|
945
|
-
playgroundPreviewError.value =
|
|
946
|
-
matchedDevice && !hasRealtimeStream
|
|
947
|
-
? payload.previewError || payload.setupState || ''
|
|
948
|
-
: `未找到设备 ${playgroundDeviceId.value} 对应的 Playground 实例`;
|
|
949
|
-
|
|
950
|
-
const nextSignature = `${payload.url || ''}::${playgroundDeviceId.value}`;
|
|
951
|
-
if (hasPlayground) {
|
|
952
|
-
stopDevicePreviewTimer();
|
|
953
|
-
if (nextSignature !== playgroundFrameSignature.value || !playgroundFrameUrl.value) {
|
|
954
|
-
playgroundFrameSignature.value = nextSignature;
|
|
955
|
-
playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
|
|
956
|
-
}
|
|
957
|
-
devicePreviewUrl.value = '';
|
|
958
|
-
devicePreviewMode.value = 'stream';
|
|
959
|
-
playgroundPreviewError.value = '';
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (!hasPlayground) {
|
|
963
|
-
playgroundFrameUrl.value = '';
|
|
964
|
-
playgroundFrameSignature.value = '';
|
|
965
|
-
loadAdbPreview();
|
|
966
|
-
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
967
|
-
}
|
|
968
|
-
} catch (error) {
|
|
969
|
-
if (isBackendFetchError(error)) {
|
|
970
|
-
markBackendOffline();
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
playgroundAvailable.value = false;
|
|
974
|
-
playgroundFrameUrl.value = '';
|
|
975
|
-
playgroundFrameSignature.value = '';
|
|
976
|
-
loadAdbPreview();
|
|
977
|
-
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
978
|
-
playgroundPreviewError.value = error instanceof Error ? error.message : '设备预览不可用';
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
const tapDevice = async (x: number, y: number) => {
|
|
983
|
-
deviceDebug.action = 'tap';
|
|
984
|
-
deviceDebug.status = 'pending';
|
|
985
|
-
try {
|
|
986
|
-
await api.tapAndroid({
|
|
987
|
-
deviceId: playgroundDeviceId.value,
|
|
988
|
-
x,
|
|
989
|
-
y,
|
|
990
|
-
});
|
|
991
|
-
deviceDebug.status = 'ok';
|
|
992
|
-
deviceDebug.message = 'tap sent';
|
|
993
|
-
refreshAdbPreviewAfterInput();
|
|
994
|
-
} catch (error) {
|
|
995
|
-
deviceDebug.status = 'error';
|
|
996
|
-
deviceDebug.message = error instanceof Error ? error.message : '点击失败';
|
|
997
|
-
throw error;
|
|
998
|
-
}
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
const swipeDevice = async (startX: number, startY: number, endX: number, endY: number, duration = 120) => {
|
|
1002
|
-
deviceDebug.action = 'swipe';
|
|
1003
|
-
deviceDebug.status = 'pending';
|
|
1004
|
-
try {
|
|
1005
|
-
await api.swipeAndroid({
|
|
1006
|
-
deviceId: playgroundDeviceId.value,
|
|
1007
|
-
startX,
|
|
1008
|
-
startY,
|
|
1009
|
-
endX,
|
|
1010
|
-
endY,
|
|
1011
|
-
duration,
|
|
1012
|
-
});
|
|
1013
|
-
deviceDebug.status = 'ok';
|
|
1014
|
-
deviceDebug.message = 'swipe sent';
|
|
1015
|
-
refreshAdbPreviewAfterInput();
|
|
1016
|
-
} catch (error) {
|
|
1017
|
-
deviceDebug.status = 'error';
|
|
1018
|
-
deviceDebug.message = error instanceof Error ? error.message : '滑动失败';
|
|
1019
|
-
throw error;
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
const switchAndroidDevice = async (deviceId: string) => {
|
|
1024
|
-
try {
|
|
1025
|
-
const payload = await api.setAndroidDevice({ deviceId });
|
|
1026
|
-
playgroundDeviceId.value = payload.currentDeviceId || deviceId;
|
|
1027
|
-
await loadPlaygroundStatus();
|
|
1028
|
-
} catch (error) {
|
|
1029
|
-
playgroundPreviewError.value = error instanceof Error ? error.message : '切换设备失败';
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
const triggerDeviceKey = async (keyCode: number) => {
|
|
1034
|
-
if (!playgroundDeviceId.value) {
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
try {
|
|
1038
|
-
await api.sendAndroidKeyevent({
|
|
1039
|
-
deviceId: playgroundDeviceId.value,
|
|
1040
|
-
keyCode,
|
|
1041
|
-
});
|
|
1042
|
-
refreshAdbPreviewAfterInput();
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
playgroundPreviewError.value = error instanceof Error ? error.message : '操作失败';
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
|
|
1048
|
-
const buildExecutionProcess = (script: SavedScript): ExecutionStep[] => {
|
|
1049
|
-
const activeSteps = (script.steps || []).filter((step) => step.enabled !== false);
|
|
1050
|
-
if (!activeSteps.length) {
|
|
1051
|
-
return [
|
|
1052
|
-
{
|
|
1053
|
-
id: 'script',
|
|
1054
|
-
sourceIndex: 0,
|
|
1055
|
-
title: script.promptTitle || script.name,
|
|
1056
|
-
method: 'script',
|
|
1057
|
-
prompt: script.filePath,
|
|
1058
|
-
status: 'pending',
|
|
1059
|
-
detail: '等待执行脚本',
|
|
1060
|
-
},
|
|
1061
|
-
];
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
return activeSteps
|
|
1065
|
-
.map((step, index) => ({
|
|
1066
|
-
id: step.id || `${script.id}-${index}`,
|
|
1067
|
-
sourceIndex: index,
|
|
1068
|
-
title: step.label || `步骤 ${index + 1}`,
|
|
1069
|
-
method: step.type,
|
|
1070
|
-
prompt: step.prompt || step.value || '',
|
|
1071
|
-
status: 'pending' as const,
|
|
1072
|
-
detail: '等待前置步骤完成',
|
|
1073
|
-
}))
|
|
1074
|
-
.filter((step) => step.title !== '统一处理弹窗');
|
|
1075
|
-
};
|
|
1076
|
-
|
|
1077
|
-
const markExecutionProcess = (success: boolean, output: string) => {
|
|
1078
|
-
if (!executionProcess.value.length) return;
|
|
1079
|
-
const runningIndex = executionProcess.value.findIndex((step) => step.status === 'running');
|
|
1080
|
-
const errorIndex = executionProcess.value.findIndex((step) => step.status === 'error');
|
|
1081
|
-
const failedIndex = success
|
|
1082
|
-
? -1
|
|
1083
|
-
: runningIndex >= 0
|
|
1084
|
-
? runningIndex
|
|
1085
|
-
: errorIndex >= 0
|
|
1086
|
-
? errorIndex
|
|
1087
|
-
: Math.max(0, executionProcess.value.length - 1);
|
|
1088
|
-
executionProcess.value = executionProcess.value.map((step, index) => {
|
|
1089
|
-
if (success || index < failedIndex) {
|
|
1090
|
-
return { ...step, status: 'success', detail: '执行完成' };
|
|
1091
|
-
}
|
|
1092
|
-
if (index === failedIndex) {
|
|
1093
|
-
return {
|
|
1094
|
-
...step,
|
|
1095
|
-
status: 'error',
|
|
1096
|
-
detail: output || '执行失败',
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
return step;
|
|
1100
|
-
});
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
const formatElapsed = (startedAt: number) => `${Math.max(0, Math.floor((Date.now() - startedAt) / 1000))} 秒`;
|
|
1104
|
-
const formatCompactDuration = (milliseconds: number) => {
|
|
1105
|
-
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
1106
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1107
|
-
const seconds = totalSeconds % 60;
|
|
1108
|
-
return minutes ? `${minutes}m${seconds}s` : `${seconds}s`;
|
|
1109
|
-
};
|
|
1110
|
-
const runningElapsedText = computed(() => {
|
|
1111
|
-
if (lastRunStatus.value !== '执行中' || !runStartedAt.value) return '';
|
|
1112
|
-
return formatCompactDuration(runningElapsedNow.value - runStartedAt.value);
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
const updateRunningStepElapsed = () => {
|
|
1116
|
-
if (runStartedAt.value) {
|
|
1117
|
-
runningElapsedNow.value = Date.now();
|
|
1118
|
-
}
|
|
1119
|
-
executionProcess.value = executionProcess.value.map((step) => {
|
|
1120
|
-
if (step.status !== 'running' || !step.startedAt) {
|
|
1121
|
-
return step;
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
...step,
|
|
1125
|
-
detail: `执行中,已耗时 ${formatElapsed(step.startedAt)}`,
|
|
1126
|
-
};
|
|
1127
|
-
});
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
const startExecutionProgressTimer = () => {
|
|
1131
|
-
if (executionProgressTimer) {
|
|
1132
|
-
window.clearInterval(executionProgressTimer);
|
|
1133
|
-
}
|
|
1134
|
-
executionProgressTimer = window.setInterval(updateRunningStepElapsed, 1000);
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
const stopExecutionProgressTimer = () => {
|
|
1138
|
-
if (!executionProgressTimer) return;
|
|
1139
|
-
window.clearInterval(executionProgressTimer);
|
|
1140
|
-
executionProgressTimer = null;
|
|
1141
|
-
};
|
|
1142
|
-
|
|
1143
|
-
const appendExecutionStepLog = (event: Extract<RunScriptStreamEvent, { type: 'step' }>) => {
|
|
1144
|
-
const title = event.title || `步骤 ${event.index + 1}`;
|
|
1145
|
-
if (event.status === 'start') {
|
|
1146
|
-
executionLog.value += `[步骤 ${event.index + 1}] 开始:${title}\n`;
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (event.status === 'success') {
|
|
1150
|
-
executionLog.value += `[步骤 ${event.index + 1}] 完成:${title}\n`;
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
executionLog.value += `[步骤 ${event.index + 1}] 失败:${title}${event.detail ? `\n${event.detail}` : ''}\n`;
|
|
1154
|
-
};
|
|
1155
|
-
|
|
1156
|
-
const applyRunScriptEvent = (event: RunScriptStreamEvent) => {
|
|
1157
|
-
if (event.type === 'output') {
|
|
1158
|
-
executionLog.value += event.chunk;
|
|
1159
|
-
if (!lastRunStatus.value) {
|
|
1160
|
-
lastRunStatus.value = '执行中';
|
|
1161
|
-
}
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
if (event.type === 'error') {
|
|
1166
|
-
executionLog.value += `${event.message}\n`;
|
|
1167
|
-
markExecutionProcess(false, event.message);
|
|
1168
|
-
lastRunStatus.value = '执行失败';
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
if (event.type === 'step') {
|
|
1173
|
-
appendExecutionStepLog(event);
|
|
1174
|
-
executionProcess.value = executionProcess.value.map((step) => {
|
|
1175
|
-
if (step.sourceIndex !== event.index) {
|
|
1176
|
-
return step;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
if (event.status === 'start') {
|
|
1180
|
-
const startedAt = Date.now();
|
|
1181
|
-
return { ...step, status: 'running', startedAt, detail: `执行中,已耗时 ${formatElapsed(startedAt)}` };
|
|
1182
|
-
}
|
|
1183
|
-
if (event.status === 'success') {
|
|
1184
|
-
return { ...step, status: 'success', startedAt: undefined, detail: '执行完成' };
|
|
1185
|
-
}
|
|
1186
|
-
return {
|
|
1187
|
-
...step,
|
|
1188
|
-
status: 'error',
|
|
1189
|
-
startedAt: undefined,
|
|
1190
|
-
detail: event.detail || '执行失败',
|
|
1191
|
-
};
|
|
1192
|
-
});
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
if (event.type === 'done') {
|
|
1197
|
-
if (event.output && !executionLog.value.trim()) {
|
|
1198
|
-
executionLog.value = event.output;
|
|
1199
|
-
}
|
|
1200
|
-
markExecutionProcess(event.success, event.output || executionLog.value);
|
|
1201
|
-
lastRunStatus.value = event.success ? '执行完成' : event.output.includes('脚本执行已停止') ? '已停止' : '执行失败';
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
|
|
1205
|
-
const runSelectedScript = async () => {
|
|
1206
|
-
if (isRunningScript.value) return;
|
|
1207
|
-
|
|
1208
|
-
if (!selectedScript.value) {
|
|
1209
|
-
lastRunStatus.value = '请先选择脚本';
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
const modelConfigError = getMidsceneModelConfigError();
|
|
1214
|
-
if (modelConfigError) {
|
|
1215
|
-
lastRunStatus.value = '模型配置不完整';
|
|
1216
|
-
executionLog.value = modelConfigError;
|
|
1217
|
-
ElMessage.error('Midscene 模型配置不完整');
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
isRunningScript.value = true;
|
|
1222
|
-
isStoppingScript.value = false;
|
|
1223
|
-
runStartedAt.value = Date.now();
|
|
1224
|
-
runningElapsedNow.value = runStartedAt.value;
|
|
1225
|
-
executionLog.value = '';
|
|
1226
|
-
lastRunStatus.value = '执行中';
|
|
1227
|
-
|
|
1228
|
-
try {
|
|
1229
|
-
await loadSavedScripts(selectedScriptId.value);
|
|
1230
|
-
const script = selectedScript.value;
|
|
1231
|
-
if (!script) {
|
|
1232
|
-
throw new Error('脚本不存在');
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
executionProcess.value = buildExecutionProcess(script);
|
|
1236
|
-
if (executionProcess.value[0]) {
|
|
1237
|
-
const startedAt = Date.now();
|
|
1238
|
-
executionProcess.value[0].status = 'running';
|
|
1239
|
-
executionProcess.value[0].startedAt = startedAt;
|
|
1240
|
-
executionProcess.value[0].detail = `执行中,已耗时 ${formatElapsed(startedAt)}`;
|
|
1241
|
-
}
|
|
1242
|
-
startExecutionProgressTimer();
|
|
1243
|
-
|
|
1244
|
-
scriptRunAbortController = new AbortController();
|
|
1245
|
-
await api.runScript(
|
|
1246
|
-
{
|
|
1247
|
-
code: script.code,
|
|
1248
|
-
scriptName: script.name,
|
|
1249
|
-
deviceId: playgroundDeviceId.value,
|
|
1250
|
-
steps: script.steps || [],
|
|
1251
|
-
signal: scriptRunAbortController.signal,
|
|
1252
|
-
},
|
|
1253
|
-
applyRunScriptEvent,
|
|
1254
|
-
);
|
|
1255
|
-
|
|
1256
|
-
if (!lastRunStatus.value) {
|
|
1257
|
-
lastRunStatus.value = '执行完成';
|
|
1258
|
-
}
|
|
1259
|
-
if (!executionLog.value.trim()) {
|
|
1260
|
-
executionLog.value = '执行完成,无输出';
|
|
1261
|
-
}
|
|
1262
|
-
} catch (error) {
|
|
1263
|
-
const isAbortError = error instanceof DOMException && error.name === 'AbortError';
|
|
1264
|
-
executionLog.value = isAbortError ? '脚本执行已停止。' : error instanceof Error ? error.message : '执行失败';
|
|
1265
|
-
markExecutionProcess(false, executionLog.value);
|
|
1266
|
-
lastRunStatus.value = isAbortError ? '已停止' : '执行失败';
|
|
1267
|
-
} finally {
|
|
1268
|
-
stopExecutionProgressTimer();
|
|
1269
|
-
isRunningScript.value = false;
|
|
1270
|
-
isStoppingScript.value = false;
|
|
1271
|
-
runStartedAt.value = null;
|
|
1272
|
-
runningElapsedNow.value = 0;
|
|
1273
|
-
scriptRunAbortController = null;
|
|
1274
|
-
}
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
const stopSelectedScript = async () => {
|
|
1278
|
-
if (!isRunningScript.value || isStoppingScript.value) return;
|
|
1279
|
-
|
|
1280
|
-
try {
|
|
1281
|
-
await ElMessageBox.confirm(
|
|
1282
|
-
'停止后当前脚本进程会立即终止,未完成步骤不会继续执行。确定停止吗?',
|
|
1283
|
-
'确认停止执行',
|
|
1284
|
-
{
|
|
1285
|
-
confirmButtonText: '停止执行',
|
|
1286
|
-
cancelButtonText: '继续执行',
|
|
1287
|
-
type: 'warning',
|
|
1288
|
-
confirmButtonClass: 'el-button--danger',
|
|
1289
|
-
},
|
|
1290
|
-
);
|
|
1291
|
-
} catch {
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
isStoppingScript.value = true;
|
|
1296
|
-
lastRunStatus.value = '停止中';
|
|
1297
|
-
|
|
1298
|
-
try {
|
|
1299
|
-
await api.stopScript();
|
|
1300
|
-
} catch (error) {
|
|
1301
|
-
executionLog.value += `${error instanceof Error ? error.message : '停止执行失败'}\n`;
|
|
1302
|
-
scriptRunAbortController?.abort();
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
|
|
1306
657
|
watch(
|
|
1307
658
|
() => generatedCode.value,
|
|
1308
659
|
() => {
|
|
@@ -1312,46 +663,35 @@ watch(
|
|
|
1312
663
|
},
|
|
1313
664
|
);
|
|
1314
665
|
|
|
1315
|
-
|
|
666
|
+
const configResource = useInitialResource('参数配置', loadConfig);
|
|
667
|
+
const presetsResource = useInitialResource('应用预设', loadAppPresets);
|
|
668
|
+
const scriptsResource = useInitialResource('脚本列表', loadSavedScripts);
|
|
669
|
+
const initialResources = [configResource, presetsResource, scriptsResource];
|
|
670
|
+
const failedResources = computed(() => initialResources.filter(resource => resource.error.value));
|
|
671
|
+
watch(activeMenu, menu => {
|
|
1316
672
|
window.localStorage.setItem(ACTIVE_MENU_STORAGE_KEY, menu);
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
673
|
+
visitedMenus.add(menu);
|
|
674
|
+
if (!openMenus.value.includes(menu)) openMenus.value.push(menu);
|
|
675
|
+
}, { flush: 'sync' });
|
|
676
|
+
const warnBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
677
|
+
if (!appiumAutoSave.dirty.value) return;
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
event.returnValue = '';
|
|
680
|
+
};
|
|
1325
681
|
onMounted(async () => {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts()]);
|
|
1333
|
-
await loadPlaygroundStatus();
|
|
1334
|
-
playgroundPollTimer = window.setInterval(() => {
|
|
1335
|
-
if (backendOffline.value) {
|
|
1336
|
-
stopPlaygroundPollTimer();
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
void loadAndroidDevices();
|
|
1340
|
-
void loadPlaygroundStatus();
|
|
1341
|
-
}, 2500);
|
|
682
|
+
window.addEventListener('beforeunload', warnBeforeUnload);
|
|
683
|
+
try { await migrateLegacySavedScripts(); }
|
|
684
|
+
catch (error) { ElMessage.warning(error instanceof Error ? error.message : '脚本缓存迁移失败'); }
|
|
685
|
+
await Promise.all(initialResources.map(resource => resource.load()));
|
|
1342
686
|
});
|
|
1343
|
-
|
|
1344
687
|
onUnmounted(() => {
|
|
1345
|
-
|
|
1346
|
-
stopDevicePreviewTimer();
|
|
1347
|
-
stopExecutionProgressTimer();
|
|
688
|
+
window.removeEventListener('beforeunload', warnBeforeUnload);
|
|
1348
689
|
aiGenerateAbortController?.abort();
|
|
1349
|
-
scriptRunAbortController?.abort();
|
|
1350
690
|
});
|
|
1351
691
|
</script>
|
|
1352
692
|
|
|
1353
693
|
<template>
|
|
1354
|
-
<div class="layout">
|
|
694
|
+
<div class="layout" :class="{ 'layout--collapsed': sidebarCollapsed }">
|
|
1355
695
|
<el-dialog
|
|
1356
696
|
v-model="actionDialog.visible"
|
|
1357
697
|
:title="actionDialog.title"
|
|
@@ -1427,38 +767,20 @@ onUnmounted(() => {
|
|
|
1427
767
|
<pre v-else class="code-block code-dialog__body"><code>{{ codeDialog.code || '暂无代码' }}</code></pre>
|
|
1428
768
|
</el-dialog>
|
|
1429
769
|
|
|
1430
|
-
<
|
|
1431
|
-
|
|
1432
|
-
<div class="layout-sidebar__logo">MS</div>
|
|
1433
|
-
</div>
|
|
1434
|
-
<el-menu :default-active="activeMenu" :default-openeds="['midscene']" class="layout-menu" @select="selectMenu">
|
|
1435
|
-
<el-sub-menu index="midscene">
|
|
1436
|
-
<template #title>
|
|
1437
|
-
<el-icon><Operation /></el-icon>
|
|
1438
|
-
<span>Midscene</span>
|
|
1439
|
-
</template>
|
|
1440
|
-
<el-menu-item v-for="item in menuItems" :key="item.key" :index="item.key">
|
|
1441
|
-
<el-icon><component :is="item.icon" /></el-icon>
|
|
1442
|
-
<span>{{ item.label }}</span>
|
|
1443
|
-
</el-menu-item>
|
|
1444
|
-
</el-sub-menu>
|
|
1445
|
-
<el-menu-item index="appium">
|
|
1446
|
-
<el-icon><Monitor /></el-icon>
|
|
1447
|
-
<span>Appium</span>
|
|
1448
|
-
</el-menu-item>
|
|
1449
|
-
<el-menu-item index="config">
|
|
1450
|
-
<el-icon><Setting /></el-icon>
|
|
1451
|
-
<span>参数配置</span>
|
|
1452
|
-
</el-menu-item>
|
|
1453
|
-
</el-menu>
|
|
1454
|
-
</aside>
|
|
770
|
+
<AppSidebar :active-menu="activeMenu" :collapsed="sidebarCollapsed"
|
|
771
|
+
@select="selectMenu" />
|
|
1455
772
|
|
|
1456
773
|
<div class="layout-main">
|
|
1457
774
|
<header class="layout-header">
|
|
1458
775
|
<div class="layout-header__title">
|
|
1459
|
-
|
|
776
|
+
<el-button text class="layout-collapse" :icon="sidebarCollapsed ? Expand : Fold"
|
|
777
|
+
:aria-label="sidebarCollapsed ? '展开导航' : '收起导航'" :aria-expanded="!sidebarCollapsed"
|
|
778
|
+
@click="sidebarCollapsed = !sidebarCollapsed" />
|
|
779
|
+
<span class="layout-header__breadcrumb">工作空间 <span>/</span></span>
|
|
780
|
+
<span>{{ activeMenuLabel }}</span>
|
|
1460
781
|
</div>
|
|
1461
782
|
<div class="layout-header__actions">
|
|
783
|
+
<div id="appium-header-actions" v-show="activeMenu === 'appium'"></div>
|
|
1462
784
|
<el-button
|
|
1463
785
|
v-if="activeMenu === 'generator'"
|
|
1464
786
|
type="primary"
|
|
@@ -1496,18 +818,42 @@ onUnmounted(() => {
|
|
|
1496
818
|
</div>
|
|
1497
819
|
</header>
|
|
1498
820
|
|
|
821
|
+
<nav class="layout-page-tabs" aria-label="页面导航">
|
|
822
|
+
<div v-for="item in visibleMenuItems" :key="item.key" class="layout-page-tab" :class="{ 'is-active': activeMenu === item.key }">
|
|
823
|
+
<button type="button" class="layout-page-tab__select"
|
|
824
|
+
:aria-current="activeMenu === item.key ? 'page' : undefined" @click="selectMenu(item.key)">
|
|
825
|
+
<el-icon><component :is="item.icon" /></el-icon>{{ item.label }}
|
|
826
|
+
</button>
|
|
827
|
+
<button type="button" class="layout-page-tab__close" :aria-label="`关闭${item.label}页签`"
|
|
828
|
+
:disabled="openMenus.length === 1" :title="openMenus.length === 1 ? '至少保留一个页签' : '关闭页签,编辑状态仍保留'" @click="closeMenu(item.key)">
|
|
829
|
+
<el-icon><Close /></el-icon>
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
</nav>
|
|
833
|
+
|
|
834
|
+
<div v-if="connectionError" class="connection-status" role="status">
|
|
835
|
+
<span>{{ connectionError }}</span><el-button link type="primary" :loading="reconnecting" @click="reconnect">重新连接</el-button>
|
|
836
|
+
</div>
|
|
837
|
+
<div v-for="resource in failedResources" :key="resource.name" class="connection-status" role="alert">
|
|
838
|
+
<span>{{ resource.name }}加载失败:{{ resource.error.value }}</span>
|
|
839
|
+
<el-button link type="primary" :loading="resource.loading.value" @click="resource.load">重试</el-button>
|
|
840
|
+
</div>
|
|
1499
841
|
<main
|
|
1500
842
|
class="page-body"
|
|
1501
843
|
:class="{
|
|
1502
844
|
'page-body--fixed': activeMenu === 'generator' && activeGeneratorMode === 'ai',
|
|
1503
845
|
'page-body--appium': activeMenu === 'appium',
|
|
846
|
+
'page-body--config': activeMenu === 'config',
|
|
1504
847
|
}"
|
|
1505
848
|
>
|
|
1506
|
-
<el-alert v-if="activeMenu !== 'appium' && activeMenu !== 'config' && errorMessage" type="error" :closable="false" show-icon class="page-alert">
|
|
849
|
+
<el-alert v-if="activeMenu !== 'appium' && activeMenu !== 'config' && activeMenu !== 'about' && errorMessage" type="error" :closable="false" show-icon class="page-alert">
|
|
1507
850
|
<template #title>{{ errorMessage }}</template>
|
|
1508
851
|
</el-alert>
|
|
1509
852
|
|
|
853
|
+
<AnalysisPage v-if="visitedMenus.has('analysis')" v-show="activeMenu === 'analysis'" :active="activeMenu === 'analysis'" />
|
|
854
|
+
|
|
1510
855
|
<GeneratorPage
|
|
856
|
+
v-if="visitedMenus.has('generator')"
|
|
1511
857
|
v-show="activeMenu === 'generator'"
|
|
1512
858
|
v-model:mode="activeGeneratorMode"
|
|
1513
859
|
v-model:source-prompt="sourcePrompt"
|
|
@@ -1535,7 +881,10 @@ onUnmounted(() => {
|
|
|
1535
881
|
@clear-all="clearGeneratorPage"
|
|
1536
882
|
/>
|
|
1537
883
|
|
|
884
|
+
<div v-if="activeMenu === 'config' && !configReady && !configResource.error.value" class="page-loading" role="status">正在加载参数配置…</div>
|
|
885
|
+
|
|
1538
886
|
<ConfigPage
|
|
887
|
+
v-if="visitedMenus.has('config') && configReady"
|
|
1539
888
|
v-show="activeMenu === 'config'"
|
|
1540
889
|
:config-form="configForm"
|
|
1541
890
|
:app-presets="appPresets"
|
|
@@ -1545,6 +894,8 @@ onUnmounted(() => {
|
|
|
1545
894
|
:is-saving-app-preset="isSavingAppPreset"
|
|
1546
895
|
:model-test-status="modelTestStatus"
|
|
1547
896
|
:appium-save-status="appiumAutoSave.status.value"
|
|
897
|
+
:appium-save-failed="appiumAutoSave.failed.value"
|
|
898
|
+
@retry-appium-save="appiumAutoSave.retry"
|
|
1548
899
|
@test-model="testModel"
|
|
1549
900
|
@save-model-config="saveModelConfig"
|
|
1550
901
|
@save-app-preset="saveAppPreset"
|
|
@@ -1555,6 +906,7 @@ onUnmounted(() => {
|
|
|
1555
906
|
/>
|
|
1556
907
|
|
|
1557
908
|
<AutomationPage
|
|
909
|
+
v-if="visitedMenus.has('automation')"
|
|
1558
910
|
v-show="activeMenu === 'automation'"
|
|
1559
911
|
v-model:selected-script-id="selectedScriptId"
|
|
1560
912
|
:saved-scripts="savedScripts"
|
|
@@ -1585,6 +937,7 @@ onUnmounted(() => {
|
|
|
1585
937
|
/>
|
|
1586
938
|
|
|
1587
939
|
<AppiumPage
|
|
940
|
+
v-if="visitedMenus.has('appium')"
|
|
1588
941
|
v-show="activeMenu === 'appium'"
|
|
1589
942
|
:active="activeMenu === 'appium'"
|
|
1590
943
|
:app-presets="appPresets"
|
|
@@ -1607,7 +960,10 @@ onUnmounted(() => {
|
|
|
1607
960
|
:refresh-device-preview="refreshDevicePreview"
|
|
1608
961
|
:swipe-device="swipeDevice"
|
|
1609
962
|
/>
|
|
963
|
+
|
|
964
|
+
<HelpCenter v-if="visitedMenus.has('about')" v-show="activeMenu === 'about'" :active="activeMenu === 'about'" />
|
|
1610
965
|
</main>
|
|
1611
966
|
</div>
|
|
967
|
+
|
|
1612
968
|
</div>
|
|
1613
969
|
</template>
|