android-midscene-automation 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -5
- package/README.md +15 -5
- package/package.json +1 -1
- package/server/http-api.ts +141 -105
- package/server/script-agent.ts +50 -45
- package/src/App.vue +133 -54
- package/src/api.ts +15 -10
- package/src/appium-recorder/AppiumPage.vue +156 -55
- package/src/appium-recorder/components/FlowCanvas.vue +2 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +93 -3
- package/src/appium-recorder/components/NestedConditionBranches.vue +6 -0
- package/src/appium-recorder/flow-copy.ts +49 -18
- package/src/appium-recorder/flow-graph.ts +3 -2
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/config/prompt-example.ts +4 -0
- package/src/config/prompt-presets.ts +172 -0
- package/src/pages/AiGeneratorPage.vue +188 -5
- package/src/pages/AutomationPage.vue +3 -3
- package/src/pages/ConfigPage.vue +13 -35
- package/src/pages/GeneratorPage.vue +7 -0
- package/src/style.css +108 -139
- package/src/types.ts +0 -14
- package/server/model-call-usage-importer.ts +0 -108
- package/server/model-usage-repository.ts +0 -131
- package/src/components/config/ModelUsageChart.vue +0 -188
package/src/App.vue
CHANGED
|
@@ -20,15 +20,14 @@ import tasksIcon from './assets/device-actions/tasks.svg';
|
|
|
20
20
|
import volumeDownIcon from './assets/device-actions/volume-down.svg';
|
|
21
21
|
import volumeUpIcon from './assets/device-actions/volume-up.svg';
|
|
22
22
|
import { promptDocument } from './config/prompt-example';
|
|
23
|
+
import { defaultPromptPresetId, promptPresets } from './config/prompt-presets';
|
|
23
24
|
import {
|
|
24
25
|
buildScript,
|
|
25
26
|
type ScriptStep,
|
|
26
27
|
} from './script-generator';
|
|
27
28
|
import {
|
|
28
29
|
codexMidsceneModel,
|
|
29
|
-
midsceneModelPresets,
|
|
30
30
|
type MidsceneModelProvider,
|
|
31
|
-
type MidsceneModelPresetKey,
|
|
32
31
|
} from './config/midscene-model-presets';
|
|
33
32
|
import * as api from './api';
|
|
34
33
|
import AutomationPage from './pages/AutomationPage.vue';
|
|
@@ -42,7 +41,6 @@ import type {
|
|
|
42
41
|
ExecutionStep,
|
|
43
42
|
GeneratorMode,
|
|
44
43
|
MenuKey,
|
|
45
|
-
ModelUsageRecord,
|
|
46
44
|
RunScriptStreamEvent,
|
|
47
45
|
SavedScript,
|
|
48
46
|
} from './types';
|
|
@@ -52,7 +50,9 @@ const menuKeys: MenuKey[] = ['generator', 'automation', 'config', 'appium'];
|
|
|
52
50
|
const storedMenu = window.localStorage.getItem(ACTIVE_MENU_STORAGE_KEY) as MenuKey | null;
|
|
53
51
|
const activeMenu = ref<MenuKey>(storedMenu && menuKeys.includes(storedMenu) ? storedMenu : 'generator');
|
|
54
52
|
const activeGeneratorMode = ref<GeneratorMode>('ai');
|
|
55
|
-
const
|
|
53
|
+
const defaultSourcePrompt = promptPresets.find((item) => item.id === defaultPromptPresetId)?.content || '';
|
|
54
|
+
const sourcePrompt = ref(defaultSourcePrompt);
|
|
55
|
+
const promptPresetId = shallowRef(defaultPromptPresetId);
|
|
56
56
|
const steps = ref<ScriptStep[]>([]);
|
|
57
57
|
const errorMessage = ref('');
|
|
58
58
|
const isGenerating = ref(false);
|
|
@@ -73,6 +73,7 @@ const playgroundPreviewError = ref('');
|
|
|
73
73
|
const playgroundDeviceId = ref('');
|
|
74
74
|
const playgroundFrameUrl = ref('');
|
|
75
75
|
const playgroundFrameSignature = ref('');
|
|
76
|
+
const restartingPlaygroundPreview = shallowRef(false);
|
|
76
77
|
const devicePreviewUrl = ref('');
|
|
77
78
|
const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
|
|
78
79
|
const backendOffline = shallowRef(false);
|
|
@@ -95,7 +96,6 @@ const runStartedAt = shallowRef<number | null>(null);
|
|
|
95
96
|
const runningElapsedNow = shallowRef(0);
|
|
96
97
|
const savedScripts = ref<SavedScript[]>([]);
|
|
97
98
|
const appPresets = ref<AppPreset[]>([]);
|
|
98
|
-
const modelUsageRecords = ref<ModelUsageRecord[]>([]);
|
|
99
99
|
const modelTestStatus = reactive({
|
|
100
100
|
midscene: '',
|
|
101
101
|
scriptOptimizer: '',
|
|
@@ -105,6 +105,10 @@ const actionDialog = reactive({
|
|
|
105
105
|
title: '',
|
|
106
106
|
loading: false,
|
|
107
107
|
error: '',
|
|
108
|
+
canRunInBackground: false,
|
|
109
|
+
canCancel: false,
|
|
110
|
+
cancelText: '终止',
|
|
111
|
+
canceling: false,
|
|
108
112
|
});
|
|
109
113
|
const codeDialog = reactive({
|
|
110
114
|
visible: false,
|
|
@@ -116,8 +120,9 @@ const codeDialog = reactive({
|
|
|
116
120
|
saving: false,
|
|
117
121
|
});
|
|
118
122
|
let playgroundPollTimer: number | null = null;
|
|
119
|
-
let
|
|
123
|
+
let devicePreviewTimer: number | null = null;
|
|
120
124
|
let executionProgressTimer: number | null = null;
|
|
125
|
+
let aiGenerateAbortController: AbortController | null = null;
|
|
121
126
|
let scriptRunAbortController: AbortController | null = null;
|
|
122
127
|
|
|
123
128
|
const isBackendFetchError = (error: unknown) => {
|
|
@@ -265,17 +270,36 @@ async function migrateLegacySavedScripts() {
|
|
|
265
270
|
localStorage.removeItem('midscene-saved-scripts');
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
type ActionDialogOptions = {
|
|
274
|
+
canRunInBackground?: boolean;
|
|
275
|
+
canCancel?: boolean;
|
|
276
|
+
cancelText?: string;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const openActionDialog = (title: string, options: ActionDialogOptions = {}) => {
|
|
269
280
|
actionDialog.visible = true;
|
|
270
281
|
actionDialog.title = title;
|
|
271
282
|
actionDialog.loading = true;
|
|
272
283
|
actionDialog.error = '';
|
|
284
|
+
actionDialog.canRunInBackground = !!options.canRunInBackground;
|
|
285
|
+
actionDialog.canCancel = !!options.canCancel;
|
|
286
|
+
actionDialog.cancelText = options.cancelText || '终止';
|
|
287
|
+
actionDialog.canceling = false;
|
|
273
288
|
};
|
|
274
289
|
|
|
275
290
|
const closeActionDialog = () => {
|
|
276
291
|
actionDialog.visible = false;
|
|
277
292
|
actionDialog.loading = false;
|
|
278
293
|
actionDialog.error = '';
|
|
294
|
+
actionDialog.canRunInBackground = false;
|
|
295
|
+
actionDialog.canCancel = false;
|
|
296
|
+
actionDialog.cancelText = '终止';
|
|
297
|
+
actionDialog.canceling = false;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const sendActionDialogToBackground = () => {
|
|
301
|
+
if (!actionDialog.loading || !actionDialog.canRunInBackground) return;
|
|
302
|
+
actionDialog.visible = false;
|
|
279
303
|
};
|
|
280
304
|
|
|
281
305
|
const openCodeDialog = (script: SavedScript) => {
|
|
@@ -328,6 +352,7 @@ const saveCodeDialog = async () => {
|
|
|
328
352
|
const failActionDialog = (message: string) => {
|
|
329
353
|
actionDialog.loading = false;
|
|
330
354
|
actionDialog.error = message;
|
|
355
|
+
actionDialog.canceling = false;
|
|
331
356
|
};
|
|
332
357
|
|
|
333
358
|
const addStep = () => {
|
|
@@ -403,7 +428,8 @@ const clearGeneratorPage = () => {
|
|
|
403
428
|
form.testName = '';
|
|
404
429
|
form.promptTitle = '';
|
|
405
430
|
form.appPresetId = '';
|
|
406
|
-
|
|
431
|
+
promptPresetId.value = defaultPromptPresetId;
|
|
432
|
+
sourcePrompt.value = defaultSourcePrompt;
|
|
407
433
|
steps.value = [];
|
|
408
434
|
showGeneratedCode.value = false;
|
|
409
435
|
importedTestCaseFileName.value = '';
|
|
@@ -534,6 +560,15 @@ const saveCurrentScript = async () => {
|
|
|
534
560
|
}
|
|
535
561
|
};
|
|
536
562
|
|
|
563
|
+
const isAbortError = (error: unknown) =>
|
|
564
|
+
error instanceof DOMException && error.name === 'AbortError';
|
|
565
|
+
|
|
566
|
+
const stopGenerateWithModel = () => {
|
|
567
|
+
if (!aiGenerateAbortController || aiGenerateAbortController.signal.aborted) return;
|
|
568
|
+
actionDialog.canceling = true;
|
|
569
|
+
aiGenerateAbortController.abort();
|
|
570
|
+
};
|
|
571
|
+
|
|
537
572
|
const removeSavedScript = async (id: string) => {
|
|
538
573
|
const script = savedScripts.value.find((item) => item.id === id);
|
|
539
574
|
try {
|
|
@@ -559,6 +594,11 @@ const removeSavedScript = async (id: string) => {
|
|
|
559
594
|
};
|
|
560
595
|
|
|
561
596
|
const generateWithModel = async () => {
|
|
597
|
+
if (isGenerating.value) {
|
|
598
|
+
actionDialog.visible = true;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
562
602
|
if (!validateCurrentGeneratorInputs()) return;
|
|
563
603
|
|
|
564
604
|
if (activeGeneratorMode.value === 'manual') {
|
|
@@ -570,11 +610,18 @@ const generateWithModel = async () => {
|
|
|
570
610
|
|
|
571
611
|
isGenerating.value = true;
|
|
572
612
|
errorMessage.value = '';
|
|
573
|
-
|
|
613
|
+
const controller = new AbortController();
|
|
614
|
+
aiGenerateAbortController = controller;
|
|
615
|
+
openActionDialog('AI生成', {
|
|
616
|
+
canRunInBackground: true,
|
|
617
|
+
canCancel: true,
|
|
618
|
+
cancelText: '终止',
|
|
619
|
+
});
|
|
574
620
|
|
|
575
621
|
try {
|
|
576
622
|
const payload = await api.generatePlan({
|
|
577
623
|
prompt: effectiveSourcePrompt.value,
|
|
624
|
+
signal: controller.signal,
|
|
578
625
|
});
|
|
579
626
|
|
|
580
627
|
form.promptTitle = payload.promptTitle || form.promptTitle;
|
|
@@ -583,11 +630,19 @@ const generateWithModel = async () => {
|
|
|
583
630
|
showGeneratedCode.value = true;
|
|
584
631
|
closeActionDialog();
|
|
585
632
|
} catch (error) {
|
|
633
|
+
if (isAbortError(error)) {
|
|
634
|
+
closeActionDialog();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
586
638
|
const message = error instanceof Error ? error.message : 'AI 生成失败';
|
|
587
639
|
errorMessage.value = message;
|
|
588
640
|
failActionDialog(message);
|
|
589
641
|
} finally {
|
|
590
|
-
|
|
642
|
+
if (aiGenerateAbortController === controller) {
|
|
643
|
+
aiGenerateAbortController = null;
|
|
644
|
+
}
|
|
645
|
+
actionDialog.canceling = false;
|
|
591
646
|
isGenerating.value = false;
|
|
592
647
|
}
|
|
593
648
|
};
|
|
@@ -627,18 +682,6 @@ const updateMidsceneModelProvider = (provider: MidsceneModelProvider) => {
|
|
|
627
682
|
modelTestStatus.midscene = '';
|
|
628
683
|
};
|
|
629
684
|
|
|
630
|
-
const applyMidsceneModelPreset = (key: MidsceneModelPresetKey) => {
|
|
631
|
-
const preset = midsceneModelPresets.find((item) => item.key === key);
|
|
632
|
-
if (!preset) return;
|
|
633
|
-
|
|
634
|
-
configForm.midscene.model.provider = 'custom';
|
|
635
|
-
configForm.midscene.model.baseUrl = preset.baseUrl;
|
|
636
|
-
configForm.midscene.model.name = preset.modelName;
|
|
637
|
-
configForm.midscene.model.family = preset.modelFamily;
|
|
638
|
-
rememberCustomMidsceneModel();
|
|
639
|
-
modelTestStatus.midscene = '';
|
|
640
|
-
};
|
|
641
|
-
|
|
642
685
|
const getMidsceneModelConfigError = () => {
|
|
643
686
|
const model = configForm.midscene.model;
|
|
644
687
|
const missing = [];
|
|
@@ -733,11 +776,6 @@ const removeAppPreset = async (id: string) => {
|
|
|
733
776
|
}
|
|
734
777
|
};
|
|
735
778
|
|
|
736
|
-
const loadModelUsageRecords = async () => {
|
|
737
|
-
const payload = await api.getModelUsageRecords(50);
|
|
738
|
-
modelUsageRecords.value = payload.records || [];
|
|
739
|
-
};
|
|
740
|
-
|
|
741
779
|
const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
742
780
|
testingModelKey.value = key;
|
|
743
781
|
errorMessage.value = '';
|
|
@@ -753,7 +791,6 @@ const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
|
753
791
|
} catch (error) {
|
|
754
792
|
modelTestStatus[key] = error instanceof Error ? error.message : '测试失败';
|
|
755
793
|
} finally {
|
|
756
|
-
await loadModelUsageRecords().catch(() => undefined);
|
|
757
794
|
testingModelKey.value = '';
|
|
758
795
|
}
|
|
759
796
|
};
|
|
@@ -789,11 +826,13 @@ const loadDeviceInterface = async () => {
|
|
|
789
826
|
const loadAdbPreview = () => {
|
|
790
827
|
if (backendOffline.value) return;
|
|
791
828
|
if (!playgroundDeviceId.value) {
|
|
829
|
+
playgroundFrameUrl.value = '';
|
|
792
830
|
devicePreviewUrl.value = '';
|
|
793
831
|
devicePreviewMode.value = '';
|
|
794
832
|
return;
|
|
795
833
|
}
|
|
796
834
|
|
|
835
|
+
playgroundFrameUrl.value = '';
|
|
797
836
|
devicePreviewUrl.value = `${api.APP_BASE}/api/android-preview?deviceId=${encodeURIComponent(playgroundDeviceId.value)}&t=${Date.now()}`;
|
|
798
837
|
devicePreviewMode.value = 'screenshot';
|
|
799
838
|
};
|
|
@@ -804,25 +843,37 @@ const refreshAdbPreviewAfterInput = () => {
|
|
|
804
843
|
}
|
|
805
844
|
};
|
|
806
845
|
|
|
807
|
-
const refreshDevicePreview = () => {
|
|
808
|
-
if (devicePreviewMode.value === 'stream' &&
|
|
809
|
-
|
|
846
|
+
const refreshDevicePreview = async () => {
|
|
847
|
+
if (devicePreviewMode.value === 'stream' && playgroundDeviceId.value) {
|
|
848
|
+
if (restartingPlaygroundPreview.value) return;
|
|
849
|
+
restartingPlaygroundPreview.value = true;
|
|
850
|
+
playgroundFrameUrl.value = '';
|
|
851
|
+
playgroundFrameSignature.value = '';
|
|
852
|
+
try {
|
|
853
|
+
await api.restartPlaygroundPreview({ deviceId: playgroundDeviceId.value });
|
|
854
|
+
restartingPlaygroundPreview.value = false;
|
|
855
|
+
await loadPlaygroundStatus();
|
|
856
|
+
} catch (error) {
|
|
857
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '重启实时预览失败';
|
|
858
|
+
} finally {
|
|
859
|
+
restartingPlaygroundPreview.value = false;
|
|
860
|
+
}
|
|
810
861
|
return;
|
|
811
862
|
}
|
|
812
863
|
loadAdbPreview();
|
|
813
864
|
};
|
|
814
865
|
|
|
815
|
-
const
|
|
866
|
+
const startDevicePreviewTimer = () => {
|
|
816
867
|
if (backendOffline.value) return;
|
|
817
|
-
if (
|
|
868
|
+
if (devicePreviewTimer) return;
|
|
818
869
|
loadAdbPreview();
|
|
819
|
-
|
|
870
|
+
devicePreviewTimer = window.setInterval(loadAdbPreview, 1000);
|
|
820
871
|
};
|
|
821
872
|
|
|
822
|
-
const
|
|
823
|
-
if (!
|
|
824
|
-
window.clearInterval(
|
|
825
|
-
|
|
873
|
+
const stopDevicePreviewTimer = () => {
|
|
874
|
+
if (!devicePreviewTimer) return;
|
|
875
|
+
window.clearInterval(devicePreviewTimer);
|
|
876
|
+
devicePreviewTimer = null;
|
|
826
877
|
};
|
|
827
878
|
|
|
828
879
|
const markBackendOffline = () => {
|
|
@@ -836,13 +887,13 @@ const markBackendOffline = () => {
|
|
|
836
887
|
playgroundPreviewError.value = '本地服务已断开,请重新运行启动命令后刷新页面。';
|
|
837
888
|
errorMessage.value = playgroundPreviewError.value;
|
|
838
889
|
stopPlaygroundPollTimer();
|
|
839
|
-
|
|
890
|
+
stopDevicePreviewTimer();
|
|
840
891
|
};
|
|
841
892
|
|
|
842
893
|
const loadPlaygroundStatus = async () => {
|
|
843
|
-
if (backendOffline.value) return;
|
|
894
|
+
if (backendOffline.value || restartingPlaygroundPreview.value) return;
|
|
844
895
|
if (!playgroundDeviceId.value) {
|
|
845
|
-
|
|
896
|
+
stopDevicePreviewTimer();
|
|
846
897
|
playgroundAvailable.value = false;
|
|
847
898
|
playgroundPreviewError.value = '';
|
|
848
899
|
playgroundFrameUrl.value = '';
|
|
@@ -854,7 +905,9 @@ const loadPlaygroundStatus = async () => {
|
|
|
854
905
|
|
|
855
906
|
try {
|
|
856
907
|
await loadDeviceInterface();
|
|
908
|
+
if (restartingPlaygroundPreview.value) return;
|
|
857
909
|
const payload = await api.getPlaygroundStatus();
|
|
910
|
+
if (restartingPlaygroundPreview.value) return;
|
|
858
911
|
|
|
859
912
|
const matchedDevice = !payload.deviceId || payload.deviceId === playgroundDeviceId.value;
|
|
860
913
|
const hasRealtimeStream = payload.previewKind === 'scrcpy' && payload.sessionConnected === true;
|
|
@@ -867,7 +920,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
867
920
|
|
|
868
921
|
const nextSignature = `${payload.url || ''}::${playgroundDeviceId.value}`;
|
|
869
922
|
if (hasPlayground) {
|
|
870
|
-
|
|
923
|
+
stopDevicePreviewTimer();
|
|
871
924
|
if (nextSignature !== playgroundFrameSignature.value || !playgroundFrameUrl.value) {
|
|
872
925
|
playgroundFrameSignature.value = nextSignature;
|
|
873
926
|
playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
|
|
@@ -881,7 +934,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
881
934
|
playgroundFrameUrl.value = '';
|
|
882
935
|
playgroundFrameSignature.value = '';
|
|
883
936
|
loadAdbPreview();
|
|
884
|
-
if (activeMenu.value === 'appium')
|
|
937
|
+
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
885
938
|
}
|
|
886
939
|
} catch (error) {
|
|
887
940
|
if (isBackendFetchError(error)) {
|
|
@@ -892,6 +945,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
892
945
|
playgroundFrameUrl.value = '';
|
|
893
946
|
playgroundFrameSignature.value = '';
|
|
894
947
|
loadAdbPreview();
|
|
948
|
+
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
895
949
|
playgroundPreviewError.value = error instanceof Error ? error.message : '设备预览不可用';
|
|
896
950
|
}
|
|
897
951
|
};
|
|
@@ -1183,7 +1237,6 @@ const runSelectedScript = async () => {
|
|
|
1183
1237
|
lastRunStatus.value = isAbortError ? '已停止' : '执行失败';
|
|
1184
1238
|
} finally {
|
|
1185
1239
|
stopExecutionProgressTimer();
|
|
1186
|
-
await loadModelUsageRecords().catch(() => undefined);
|
|
1187
1240
|
isRunningScript.value = false;
|
|
1188
1241
|
isStoppingScript.value = false;
|
|
1189
1242
|
runStartedAt.value = null;
|
|
@@ -1233,10 +1286,10 @@ watch(
|
|
|
1233
1286
|
watch(activeMenu, (menu) => {
|
|
1234
1287
|
window.localStorage.setItem(ACTIVE_MENU_STORAGE_KEY, menu);
|
|
1235
1288
|
if (menu === 'appium' || menu === 'automation') {
|
|
1236
|
-
|
|
1289
|
+
stopDevicePreviewTimer();
|
|
1237
1290
|
void loadPlaygroundStatus();
|
|
1238
1291
|
} else {
|
|
1239
|
-
|
|
1292
|
+
stopDevicePreviewTimer();
|
|
1240
1293
|
}
|
|
1241
1294
|
}, { immediate: true });
|
|
1242
1295
|
|
|
@@ -1247,7 +1300,7 @@ onMounted(async () => {
|
|
|
1247
1300
|
console.warn('脚本缓存迁移失败', error);
|
|
1248
1301
|
}
|
|
1249
1302
|
|
|
1250
|
-
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts()
|
|
1303
|
+
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts()]);
|
|
1251
1304
|
await loadPlaygroundStatus();
|
|
1252
1305
|
playgroundPollTimer = window.setInterval(() => {
|
|
1253
1306
|
if (backendOffline.value) {
|
|
@@ -1261,8 +1314,9 @@ onMounted(async () => {
|
|
|
1261
1314
|
|
|
1262
1315
|
onUnmounted(() => {
|
|
1263
1316
|
stopPlaygroundPollTimer();
|
|
1264
|
-
|
|
1317
|
+
stopDevicePreviewTimer();
|
|
1265
1318
|
stopExecutionProgressTimer();
|
|
1319
|
+
aiGenerateAbortController?.abort();
|
|
1266
1320
|
scriptRunAbortController?.abort();
|
|
1267
1321
|
});
|
|
1268
1322
|
</script>
|
|
@@ -1286,7 +1340,31 @@ onUnmounted(() => {
|
|
|
1286
1340
|
{{ actionDialog.error }}
|
|
1287
1341
|
</div>
|
|
1288
1342
|
<template #footer>
|
|
1289
|
-
<
|
|
1343
|
+
<div v-if="actionDialog.loading" class="action-dialog__footer-actions">
|
|
1344
|
+
<el-button
|
|
1345
|
+
v-if="actionDialog.canCancel"
|
|
1346
|
+
class="action-dialog__footer-button action-dialog__footer-button--secondary"
|
|
1347
|
+
:icon="VideoPause"
|
|
1348
|
+
:loading="actionDialog.canceling"
|
|
1349
|
+
@click="stopGenerateWithModel"
|
|
1350
|
+
>
|
|
1351
|
+
{{ actionDialog.cancelText === '终止' ? '终止生成' : actionDialog.cancelText }}
|
|
1352
|
+
</el-button>
|
|
1353
|
+
<el-button
|
|
1354
|
+
v-if="actionDialog.canRunInBackground"
|
|
1355
|
+
class="action-dialog__footer-button action-dialog__footer-button--primary"
|
|
1356
|
+
@click="sendActionDialogToBackground"
|
|
1357
|
+
>
|
|
1358
|
+
后台处理
|
|
1359
|
+
</el-button>
|
|
1360
|
+
</div>
|
|
1361
|
+
<el-button
|
|
1362
|
+
v-else
|
|
1363
|
+
class="action-dialog__footer-button action-dialog__footer-button--primary"
|
|
1364
|
+
@click="closeActionDialog"
|
|
1365
|
+
>
|
|
1366
|
+
关闭
|
|
1367
|
+
</el-button>
|
|
1290
1368
|
</template>
|
|
1291
1369
|
</el-dialog>
|
|
1292
1370
|
|
|
@@ -1355,11 +1433,11 @@ onUnmounted(() => {
|
|
|
1355
1433
|
<el-button
|
|
1356
1434
|
v-if="activeMenu === 'generator'"
|
|
1357
1435
|
type="primary"
|
|
1358
|
-
:icon="Check"
|
|
1359
|
-
:
|
|
1436
|
+
:icon="isGenerating ? Loading : Check"
|
|
1437
|
+
:class="{ 'layout-header__button--loading': isGenerating }"
|
|
1360
1438
|
@click="generateWithModel"
|
|
1361
1439
|
>
|
|
1362
|
-
AI 生成
|
|
1440
|
+
{{ isGenerating ? 'AI 生成中' : 'AI 生成' }}
|
|
1363
1441
|
</el-button>
|
|
1364
1442
|
<el-button
|
|
1365
1443
|
v-if="activeMenu === 'generator'"
|
|
@@ -1404,15 +1482,18 @@ onUnmounted(() => {
|
|
|
1404
1482
|
v-show="activeMenu === 'generator'"
|
|
1405
1483
|
v-model:mode="activeGeneratorMode"
|
|
1406
1484
|
v-model:source-prompt="sourcePrompt"
|
|
1485
|
+
v-model:prompt-preset-id="promptPresetId"
|
|
1407
1486
|
v-model:generated-code-draft="generatedCodeDraft"
|
|
1408
1487
|
:form="form"
|
|
1409
1488
|
:app-presets="appPresets"
|
|
1489
|
+
:prompt-presets="promptPresets"
|
|
1410
1490
|
:prompt-example="promptDocument.trim()"
|
|
1411
1491
|
:steps="steps"
|
|
1412
1492
|
:generated-code="generatedCode"
|
|
1413
1493
|
:show-generated-code="showGeneratedCode"
|
|
1414
1494
|
:generated-code-editing="generatedCodeEditing"
|
|
1415
1495
|
:generated-code-edit-saving="generatedCodeEditSaving"
|
|
1496
|
+
:is-generating="isGenerating"
|
|
1416
1497
|
:importing-test-case="importingTestCase"
|
|
1417
1498
|
:imported-test-case-file-name="importedTestCaseFileName"
|
|
1418
1499
|
@add-step="addStep"
|
|
@@ -1434,14 +1515,12 @@ onUnmounted(() => {
|
|
|
1434
1515
|
:is-saving-model-config="isSavingModelConfig"
|
|
1435
1516
|
:is-saving-app-preset="isSavingAppPreset"
|
|
1436
1517
|
:model-test-status="modelTestStatus"
|
|
1437
|
-
:model-usage-records="modelUsageRecords"
|
|
1438
1518
|
@test-model="testModel"
|
|
1439
1519
|
@save-model-config="saveModelConfig"
|
|
1440
1520
|
@save-app-preset="saveAppPreset"
|
|
1441
1521
|
@edit-app-preset="editAppPreset"
|
|
1442
1522
|
@delete-app-preset="removeAppPreset"
|
|
1443
1523
|
@update-midscene-model-provider="updateMidsceneModelProvider"
|
|
1444
|
-
@apply-midscene-model-preset="applyMidsceneModelPreset"
|
|
1445
1524
|
/>
|
|
1446
1525
|
|
|
1447
1526
|
<AutomationPage
|
package/src/api.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
ConfigForm,
|
|
6
6
|
DeviceLock,
|
|
7
7
|
DeviceSession,
|
|
8
|
-
ModelUsageRecord,
|
|
9
8
|
OperationEvent,
|
|
10
9
|
OperationRecord,
|
|
11
10
|
RunScriptStreamEvent,
|
|
@@ -27,12 +26,16 @@ async function readJson<T>(response: Response) {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
// 项目内大部分接口都是 JSON POST,这里集中处理请求头和错误转换。
|
|
30
|
-
async function postJson<T>(url: string, body?: unknown) {
|
|
29
|
+
async function postJson<T>(url: string, body?: unknown, options: { signal?: AbortSignal } = {}) {
|
|
31
30
|
const response = await fetch(url, {
|
|
32
31
|
method: 'POST',
|
|
33
32
|
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
signal: options.signal,
|
|
34
34
|
body: JSON.stringify(body || {}),
|
|
35
35
|
}).catch((error) => {
|
|
36
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
36
39
|
throw new Error(`无法连接本地后端 ${url}:${error instanceof Error ? error.message : '网络请求失败'}`);
|
|
37
40
|
});
|
|
38
41
|
return readJson<T>(response);
|
|
@@ -86,7 +89,7 @@ export function updateScriptCode(input: { id: string; code: string }) {
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
// 调用脚本优化模型,把原始 Prompt 转成项目内的结构化步骤。
|
|
89
|
-
export function generatePlan(input: { prompt: string }) {
|
|
92
|
+
export function generatePlan(input: { prompt: string; signal?: AbortSignal }) {
|
|
90
93
|
return postJson<{
|
|
91
94
|
promptTitle?: string;
|
|
92
95
|
steps?: Array<Omit<ScriptStep, 'id'>>;
|
|
@@ -97,7 +100,7 @@ export function generatePlan(input: { prompt: string }) {
|
|
|
97
100
|
completionTokens?: number;
|
|
98
101
|
totalTokens?: number;
|
|
99
102
|
};
|
|
100
|
-
}>(`${APP_BASE}/api/generate`, input);
|
|
103
|
+
}>(`${APP_BASE}/api/generate`, { prompt: input.prompt }, { signal: input.signal });
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
// 读取模型配置和脚本优化模型配置。
|
|
@@ -133,12 +136,6 @@ export function testModel(input: {
|
|
|
133
136
|
}>(`${APP_BASE}/api/test-model`, input);
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
// 读取模型测试消耗记录,后端按时间保存在 SQLite。
|
|
137
|
-
export async function getModelUsageRecords(limit = 50) {
|
|
138
|
-
const response = await fetch(`${APP_BASE}/api/model-usage-records?limit=${encodeURIComponent(String(limit))}`);
|
|
139
|
-
return readJson<{ records?: ModelUsageRecord[] }>(response);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
139
|
// 读取预设 App 列表,测试脚本生成时用于自动拼接 App 名称和包名上下文。
|
|
143
140
|
export async function getAppPresets() {
|
|
144
141
|
const response = await fetch(`${APP_BASE}/api/app-presets`);
|
|
@@ -205,6 +202,14 @@ export async function getPlaygroundStatus() {
|
|
|
205
202
|
}>(response);
|
|
206
203
|
}
|
|
207
204
|
|
|
205
|
+
// 重启本地 Android Playground 实时预览,用于恢复花屏或卡住的 scrcpy 流。
|
|
206
|
+
export function restartPlaygroundPreview(input: { deviceId: string }) {
|
|
207
|
+
return postJson<{ success?: boolean; started?: boolean; available?: boolean; url?: string; skippedReason?: string }>(
|
|
208
|
+
`${APP_BASE}/api/playground-restart`,
|
|
209
|
+
input,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
208
213
|
// 切换当前自动化测试设备,并创建或复用对应设备会话。
|
|
209
214
|
export function setAndroidDevice(input: { deviceId: string }) {
|
|
210
215
|
return postJson<{ currentDeviceId?: string }>(`${APP_BASE}/api/android-device`, input);
|