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/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 sourcePrompt = ref('');
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 appiumPreviewTimer: number | null = null;
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
- const openActionDialog = (title: string) => {
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
- sourcePrompt.value = '';
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
- openActionDialog('AI生成');
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
- await loadModelUsageRecords().catch(() => undefined);
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' && playgroundFrameUrl.value) {
809
- playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
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 startAppiumPreviewTimer = () => {
866
+ const startDevicePreviewTimer = () => {
816
867
  if (backendOffline.value) return;
817
- if (appiumPreviewTimer) return;
868
+ if (devicePreviewTimer) return;
818
869
  loadAdbPreview();
819
- appiumPreviewTimer = window.setInterval(loadAdbPreview, 1000);
870
+ devicePreviewTimer = window.setInterval(loadAdbPreview, 1000);
820
871
  };
821
872
 
822
- const stopAppiumPreviewTimer = () => {
823
- if (!appiumPreviewTimer) return;
824
- window.clearInterval(appiumPreviewTimer);
825
- appiumPreviewTimer = null;
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
- stopAppiumPreviewTimer();
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
- stopAppiumPreviewTimer();
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
- stopAppiumPreviewTimer();
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') startAppiumPreviewTimer();
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
- stopAppiumPreviewTimer();
1289
+ stopDevicePreviewTimer();
1237
1290
  void loadPlaygroundStatus();
1238
1291
  } else {
1239
- stopAppiumPreviewTimer();
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(), loadModelUsageRecords()]);
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
- stopAppiumPreviewTimer();
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
- <el-button v-if="!actionDialog.loading" type="primary" @click="closeActionDialog">关闭</el-button>
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
- :loading="isGenerating"
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);