android-midscene-automation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +160 -0
  2. package/bin/android-midscene-automation.js +27 -0
  3. package/index.html +12 -0
  4. package/package.json +49 -0
  5. package/remote-agent/index.ts +206 -0
  6. package/server/appium-recorder/appium-runner.ts +427 -0
  7. package/server/appium-recorder/repository.ts +228 -0
  8. package/server/appium-recorder/routes.ts +219 -0
  9. package/server/config-store.ts +167 -0
  10. package/server/config.ts +130 -0
  11. package/server/device-locks/repository.ts +147 -0
  12. package/server/device-locks/service.ts +72 -0
  13. package/server/device-locks/types.ts +22 -0
  14. package/server/device-sessions/repository.ts +169 -0
  15. package/server/device-sessions/service.ts +59 -0
  16. package/server/device-sessions/types.ts +24 -0
  17. package/server/http-api.ts +1389 -0
  18. package/server/model-call-usage-importer.ts +108 -0
  19. package/server/model-tester.ts +104 -0
  20. package/server/model-usage-repository.ts +131 -0
  21. package/server/operations/repository.ts +218 -0
  22. package/server/operations/service.ts +84 -0
  23. package/server/operations/types.ts +27 -0
  24. package/server/paths.ts +27 -0
  25. package/server/remote-agents/protocol.ts +38 -0
  26. package/server/remote-agents/registry.ts +136 -0
  27. package/server/remote-agents/routes.ts +89 -0
  28. package/server/script-agent.ts +284 -0
  29. package/server/script-db.ts +281 -0
  30. package/server/script-runner.ts +551 -0
  31. package/server/storage/sqlite.ts +49 -0
  32. package/server/test-case-import/formatter.ts +28 -0
  33. package/server/test-case-import/parsers/excel.ts +69 -0
  34. package/server/test-case-import/parsers/txt.ts +11 -0
  35. package/server/test-case-import/parsers/word.ts +9 -0
  36. package/server/test-case-import/service.ts +58 -0
  37. package/server/test-case-import/text-normalizer.ts +98 -0
  38. package/server/test-case-import/types.ts +24 -0
  39. package/server/test-case-import/validator.ts +34 -0
  40. package/src/App.vue +1450 -0
  41. package/src/api.ts +290 -0
  42. package/src/appium-recorder/AppiumPage.vue +894 -0
  43. package/src/appium-recorder/api.ts +64 -0
  44. package/src/appium-recorder/components/ComponentTree.vue +44 -0
  45. package/src/appium-recorder/components/NodeDetail.vue +152 -0
  46. package/src/appium-recorder/components/RecordedSteps.vue +79 -0
  47. package/src/appium-recorder/tree.ts +129 -0
  48. package/src/appium-recorder/types.ts +88 -0
  49. package/src/assets/device-actions/back.svg +5 -0
  50. package/src/assets/device-actions/home.svg +3 -0
  51. package/src/assets/device-actions/power.svg +5 -0
  52. package/src/assets/device-actions/tasks.svg +3 -0
  53. package/src/assets/device-actions/volume-down.svg +3 -0
  54. package/src/assets/device-actions/volume-up.svg +3 -0
  55. package/src/components/config/ModelUsageChart.vue +188 -0
  56. package/src/components/device/DevicePreviewPanel.vue +266 -0
  57. package/src/components/generator/GeneratedCodePanel.vue +70 -0
  58. package/src/components/generator/TestCaseFileUpload.vue +97 -0
  59. package/src/config/midscene-model-presets.ts +75 -0
  60. package/src/config/prompt-example.ts +6 -0
  61. package/src/main.ts +7 -0
  62. package/src/pages/AiGeneratorPage.vue +90 -0
  63. package/src/pages/AutomationPage.vue +161 -0
  64. package/src/pages/ConfigPage.vue +273 -0
  65. package/src/pages/GeneratorPage.vue +97 -0
  66. package/src/pages/ManualStepsPage.vue +179 -0
  67. package/src/script-generator/codegen.ts +126 -0
  68. package/src/script-generator/index.ts +4 -0
  69. package/src/script-generator/presets.ts +37 -0
  70. package/src/script-generator/step-options.ts +52 -0
  71. package/src/script-generator/types.ts +27 -0
  72. package/src/style.css +1983 -0
  73. package/src/types.ts +157 -0
  74. package/src/vite-env.d.ts +1 -0
  75. package/tsconfig.app.json +8 -0
  76. package/tsconfig.json +11 -0
  77. package/tsconfig.node.json +16 -0
  78. package/vite.config.ts +28 -0
@@ -0,0 +1,6 @@
1
+ export const promptDocument = `# 示例 App 登录测试
2
+
3
+ 进入目标 App 后,按以下规则处理启动阶段弹窗,这一步必须执行点击动作,不要只判断弹窗是否存在:若弹出位置、存储、相机、麦克风、通知等权限申请弹窗,点击允许;若弹出“声明与条款”、用户协议、隐私政策、服务协议弹窗,点击同意、接受或我已阅读并同意;若弹出广告、活动、版本更新、评分、通知引导等与登录无关的弹窗,点击关闭、跳过、稍后再说或取消。
4
+
5
+ 弹窗处理完成后,确认页面已经稳定停留在目标 App 的登录页、手机号登录页、账号密码登录页,或显示可进入登录流程的入口。页面上不应存在遮挡登录操作的权限、协议、广告、更新或活动弹窗;如果仍存在遮挡弹窗,脚本应失败而不是显示执行完成。
6
+ `;
package/src/main.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { createApp } from 'vue';
2
+ import ElementPlus from 'element-plus';
3
+ import 'element-plus/dist/index.css';
4
+ import App from './App.vue';
5
+ import './style.css';
6
+
7
+ createApp(App).use(ElementPlus).mount('#app');
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef } from 'vue';
3
+ import { Delete, QuestionFilled } from '@element-plus/icons-vue';
4
+ import TestCaseFileUpload from '../components/generator/TestCaseFileUpload.vue';
5
+ import type { AppPreset, GeneratorForm } from '../types';
6
+
7
+ defineProps<{
8
+ form: GeneratorForm;
9
+ appPresets: AppPreset[];
10
+ promptExample: string;
11
+ importingTestCase: boolean;
12
+ importedFileName: string;
13
+ }>();
14
+
15
+ defineEmits<{
16
+ importTestCase: [file: File];
17
+ clearAll: [];
18
+ }>();
19
+
20
+ const sourcePrompt = defineModel<string>('sourcePrompt', { required: true });
21
+ const promptExampleVisible = shallowRef(false);
22
+ </script>
23
+
24
+ <template>
25
+ <section class="page-grid">
26
+ <el-card shadow="never">
27
+ <template #header>基础配置</template>
28
+ <el-form label-position="top">
29
+ <el-form-item label="脚本名称">
30
+ <el-input v-model="form.testName" placeholder="请输入脚本名称" />
31
+ </el-form-item>
32
+ <el-form-item label="场景标题">
33
+ <el-input v-model="form.promptTitle" placeholder="请输入场景标题" />
34
+ </el-form-item>
35
+ <el-form-item label="选择 App">
36
+ <el-select v-model="form.appPresetId" placeholder="选择预设 App">
37
+ <el-option
38
+ v-for="app in appPresets"
39
+ :key="app.id"
40
+ :label="`${app.name} · ${app.packageName}`"
41
+ :value="app.id"
42
+ />
43
+ </el-select>
44
+ </el-form-item>
45
+ </el-form>
46
+ </el-card>
47
+
48
+ <el-card shadow="never" class="prompt-card">
49
+ <template #header>
50
+ <div class="panel-header">
51
+ <span>原始 Prompt</span>
52
+ <div class="panel-header__actions prompt-card__actions">
53
+ <TestCaseFileUpload
54
+ :uploading="importingTestCase"
55
+ :file-name="importedFileName"
56
+ @upload="$emit('importTestCase', $event)"
57
+ />
58
+ <el-button size="small" :icon="Delete" :disabled="importingTestCase" @click="$emit('clearAll')">
59
+ 清除
60
+ </el-button>
61
+ <el-button
62
+ text
63
+ :icon="QuestionFilled"
64
+ aria-label="查看示例文案"
65
+ @click="promptExampleVisible = true"
66
+ />
67
+ </div>
68
+ </div>
69
+ </template>
70
+ <el-input
71
+ v-model="sourcePrompt"
72
+ class="source-prompt-input"
73
+ type="textarea"
74
+ :rows="14"
75
+ placeholder="请输入原始 Prompt,或点击右上角问号查看示例"
76
+ />
77
+ </el-card>
78
+ </section>
79
+
80
+ <el-dialog v-model="promptExampleVisible" title="原始 Prompt 示例" width="720px">
81
+ <pre class="prompt-example"><code>{{ promptExample }}</code></pre>
82
+ </el-dialog>
83
+ </template>
84
+
85
+ <style scoped>
86
+ .prompt-card__actions {
87
+ align-items: center;
88
+ min-width: 0;
89
+ }
90
+ </style>
@@ -0,0 +1,161 @@
1
+ <script setup lang="ts">
2
+ import { CopyDocument, Delete, View } from '@element-plus/icons-vue';
3
+ import DevicePreviewPanel from '../components/device/DevicePreviewPanel.vue';
4
+ import type { AndroidDevice, DeviceAction, ExecutionStep, SavedScript } from '../types';
5
+
6
+ defineProps<{
7
+ savedScripts: SavedScript[];
8
+ selectedScript?: SavedScript;
9
+ playgroundAvailable: boolean;
10
+ playgroundDeviceId: string;
11
+ playgroundFrameUrl: string;
12
+ playgroundPreviewError: string;
13
+ devicePreviewUrl: string;
14
+ androidDevices: AndroidDevice[];
15
+ deviceActions: readonly DeviceAction[];
16
+ executionLog: string;
17
+ executionProcess: ExecutionStep[];
18
+ lastRunStatus: string;
19
+ runningElapsedText: string;
20
+ formatScriptTime: (value: string) => string;
21
+ switchAndroidDevice: (deviceId: string) => void;
22
+ triggerDeviceKey: (keyCode: number) => void;
23
+ refreshDevicePreview: () => void;
24
+ openCodeDialog: (script: SavedScript) => void;
25
+ removeSavedScript: (id: string) => void;
26
+ deviceWidth: number;
27
+ deviceHeight: number;
28
+ tapDevice: (x: number, y: number) => void;
29
+ swipeDevice: (startX: number, startY: number, endX: number, endY: number, duration?: number) => void;
30
+ }>();
31
+
32
+ const selectedScriptId = defineModel<string>('selectedScriptId', { required: true });
33
+
34
+ defineEmits<{
35
+ clearExecutionLog: [];
36
+ copyExecutionLog: [];
37
+ }>();
38
+
39
+ // 执行状态与 Element Plus tag 类型的映射只影响展示,不参与执行流程判断。
40
+ const getRunStatusTagType = (status: string) => {
41
+ if (status === '执行完成') return 'success';
42
+ if (status === '执行中' || status === '停止中' || status === '已停止') return 'warning';
43
+ return 'danger';
44
+ };
45
+
46
+ // 将内部执行状态收敛成短标签文案,避免模板中重复三元判断。
47
+ const getRunStatusTagText = (status: string) => {
48
+ if (status === '执行完成') return '完成';
49
+ if (status === '执行中') return '执行中';
50
+ if (status === '停止中') return '停止中';
51
+ if (status === '已停止') return '已停止';
52
+ return '失败';
53
+ };
54
+ </script>
55
+
56
+ <template>
57
+ <section class="automation-layout">
58
+ <el-card shadow="never" class="automation-card">
59
+ <template #header>脚本列表</template>
60
+ <div class="script-list">
61
+ <div
62
+ v-for="item in savedScripts"
63
+ :key="item.id"
64
+ role="button"
65
+ tabindex="0"
66
+ class="script-row"
67
+ :class="{ 'script-row--active': selectedScriptId === item.id }"
68
+ @click="selectedScriptId = item.id"
69
+ @keydown.enter="selectedScriptId = item.id"
70
+ >
71
+ <div class="script-row__main">
72
+ <strong>{{ item.promptTitle || item.name }}</strong>
73
+ <span>{{ item.name }}</span>
74
+ <small>{{ formatScriptTime(item.updatedAt) }}</small>
75
+ </div>
76
+ <div class="script-row__actions">
77
+ <el-button
78
+ text
79
+ size="small"
80
+ :icon="View"
81
+ title="查看代码"
82
+ aria-label="查看代码"
83
+ @click.stop="openCodeDialog(item)"
84
+ />
85
+ <el-button text size="small" :icon="Delete" @click.stop="removeSavedScript(item.id)" />
86
+ </div>
87
+ </div>
88
+ <el-empty v-if="!savedScripts.length" description="暂无脚本" />
89
+ </div>
90
+ </el-card>
91
+
92
+ <div class="automation-stage">
93
+ <DevicePreviewPanel
94
+ :available="playgroundAvailable"
95
+ :devices="androidDevices"
96
+ :selected-device-id="playgroundDeviceId"
97
+ :frame-url="playgroundFrameUrl"
98
+ :image-url="devicePreviewUrl"
99
+ :preview-error="playgroundPreviewError"
100
+ :actions="deviceActions"
101
+ :device-width="deviceWidth"
102
+ :device-height="deviceHeight"
103
+ @switch-device="switchAndroidDevice"
104
+ @trigger-key="triggerDeviceKey"
105
+ @refresh-preview="refreshDevicePreview"
106
+ @tap="tapDevice($event.x, $event.y)"
107
+ @swipe="swipeDevice($event.startX, $event.startY, $event.endX, $event.endY, $event.duration)"
108
+ />
109
+
110
+ <el-card shadow="never" class="automation-card execution-card">
111
+ <template #header>执行面板</template>
112
+ <el-form label-position="top">
113
+ <el-form-item label="当前脚本">
114
+ <el-input :model-value="selectedScript ? `${selectedScript.promptTitle || selectedScript.name} · ${selectedScript.name}` : ''" readonly />
115
+ </el-form-item>
116
+ <el-form-item class="execution-output-item">
117
+ <div class="execution-output-section">
118
+ <div class="execution-output-header">
119
+ <strong>执行输出</strong>
120
+ <div class="execution-output-toolbar">
121
+ <el-tag
122
+ v-if="lastRunStatus"
123
+ size="small"
124
+ :type="getRunStatusTagType(lastRunStatus)"
125
+ >
126
+ {{ getRunStatusTagText(lastRunStatus) }}
127
+ </el-tag>
128
+ <span v-if="runningElapsedText" class="execution-output-duration">
129
+ {{ runningElapsedText }}
130
+ </span>
131
+ <el-button
132
+ size="small"
133
+ text
134
+ :icon="CopyDocument"
135
+ :disabled="!executionLog"
136
+ @click="$emit('copyExecutionLog')"
137
+ >
138
+ 复制
139
+ </el-button>
140
+ <el-button size="small" text :disabled="!executionLog" @click="$emit('clearExecutionLog')">
141
+ 清除
142
+ </el-button>
143
+ </div>
144
+ </div>
145
+ <div class="execution-log execution-log--panel">
146
+ <textarea
147
+ v-if="executionLog"
148
+ class="execution-log__output"
149
+ :value="executionLog"
150
+ readonly
151
+ wrap="off"
152
+ spellcheck="false"
153
+ />
154
+ </div>
155
+ </div>
156
+ </el-form-item>
157
+ </el-form>
158
+ </el-card>
159
+ </div>
160
+ </section>
161
+ </template>
@@ -0,0 +1,273 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { Delete, Edit } from '@element-plus/icons-vue';
4
+ import ModelUsageChart from '../components/config/ModelUsageChart.vue';
5
+ import {
6
+ codexMidsceneModelOptions,
7
+ midsceneModelFamilyOptions,
8
+ midsceneModelOptions,
9
+ midsceneModelPresets,
10
+ type MidsceneModelProvider,
11
+ type MidsceneModelPresetKey,
12
+ } from '../config/midscene-model-presets';
13
+ import type { AppPreset, ConfigForm, ModelUsageRecord } from '../types';
14
+
15
+ type AppPresetForm = {
16
+ id: string;
17
+ name: string;
18
+ packageName: string;
19
+ };
20
+
21
+ const props = defineProps<{
22
+ configForm: ConfigForm;
23
+ appPresets: AppPreset[];
24
+ appPresetForm: AppPresetForm;
25
+ testingModelKey: string;
26
+ isSavingModelConfig: boolean;
27
+ isSavingAppPreset: boolean;
28
+ modelTestStatus: {
29
+ midscene: string;
30
+ scriptOptimizer: string;
31
+ };
32
+ modelUsageRecords: ModelUsageRecord[];
33
+ }>();
34
+
35
+ const emit = defineEmits<{
36
+ testModel: [key: 'midscene' | 'scriptOptimizer'];
37
+ saveModelConfig: [];
38
+ saveAppPreset: [];
39
+ editAppPreset: [app: AppPreset];
40
+ deleteAppPreset: [id: string];
41
+ updateMidsceneModelProvider: [provider: MidsceneModelProvider];
42
+ applyMidsceneModelPreset: [key: MidsceneModelPresetKey];
43
+ }>();
44
+
45
+ const activeMidsceneProvider = computed<MidsceneModelProvider>(() =>
46
+ props.configForm.midscene.model.provider === 'codex' ? 'codex' : 'custom',
47
+ );
48
+
49
+ const activeMidscenePresetKey = computed(() => {
50
+ const model = props.configForm.midscene.model;
51
+ return midsceneModelPresets.find(
52
+ (preset) =>
53
+ preset.baseUrl === model.baseUrl &&
54
+ preset.modelName === model.name &&
55
+ preset.modelFamily === model.family,
56
+ )?.key || '';
57
+ });
58
+
59
+ const activeMidsceneModelOptions = computed(() =>
60
+ activeMidsceneProvider.value === 'codex' ? codexMidsceneModelOptions : midsceneModelOptions,
61
+ );
62
+
63
+ const updateMidsceneModelName = (value: string) => {
64
+ props.configForm.midscene.model.name = value;
65
+ const option = activeMidsceneModelOptions.value.find((item) => item.value === value);
66
+ if (option) {
67
+ props.configForm.midscene.model.family = option.family;
68
+ }
69
+ };
70
+
71
+ const applyMidscenePreset = (value: string) => {
72
+ if (value === 'gpt' || value === 'doubao') {
73
+ emit('applyMidsceneModelPreset', value);
74
+ }
75
+ };
76
+
77
+ const updateMidsceneProvider = (value: string) => {
78
+ if (value === 'custom' || value === 'codex') {
79
+ emit('updateMidsceneModelProvider', value);
80
+ }
81
+ };
82
+ </script>
83
+
84
+ <template>
85
+ <div class="config-page">
86
+ <section class="config-grid">
87
+ <el-card shadow="never" class="config-module-card">
88
+ <template #header>
89
+ <div class="panel-header">
90
+ <span>模型配置</span>
91
+ <el-button type="primary" :loading="isSavingModelConfig" @click="$emit('saveModelConfig')">
92
+ 保存模型配置
93
+ </el-button>
94
+ </div>
95
+ </template>
96
+
97
+ <div class="config-model-grid">
98
+ <section>
99
+ <div class="panel-header panel-header--sub">
100
+ <span>Midscene 模型</span>
101
+ <el-button
102
+ :loading="testingModelKey === 'midscene'"
103
+ @click="$emit('testModel', 'midscene')"
104
+ >
105
+ 测试模型
106
+ </el-button>
107
+ </div>
108
+ <el-form label-position="top">
109
+ <el-form-item label="接入方式">
110
+ <el-select
111
+ :model-value="activeMidsceneProvider"
112
+ @change="updateMidsceneProvider"
113
+ >
114
+ <el-option label="自定义提供方" value="custom" />
115
+ <el-option label="使用 Codex" value="codex" />
116
+ </el-select>
117
+ </el-form-item>
118
+
119
+ <template v-if="activeMidsceneProvider === 'custom'">
120
+ <el-form-item label="模型预设参数">
121
+ <el-select
122
+ :model-value="activeMidscenePresetKey"
123
+ placeholder="请选择模型预设"
124
+ @change="applyMidscenePreset"
125
+ >
126
+ <el-option
127
+ v-for="preset in midsceneModelPresets"
128
+ :key="preset.key"
129
+ :label="preset.label"
130
+ :value="preset.key"
131
+ />
132
+ </el-select>
133
+ </el-form-item>
134
+ <el-form-item label="Base URL">
135
+ <el-input v-model="configForm.midscene.model.baseUrl" />
136
+ </el-form-item>
137
+ <el-form-item label="API Key">
138
+ <el-input v-model="configForm.midscene.model.apiKey" show-password />
139
+ </el-form-item>
140
+ <el-form-item label="Model Name">
141
+ <el-select
142
+ :model-value="configForm.midscene.model.name"
143
+ filterable
144
+ @change="updateMidsceneModelName"
145
+ >
146
+ <el-option
147
+ v-for="option in activeMidsceneModelOptions"
148
+ :key="option.value"
149
+ :label="option.label"
150
+ :value="option.value"
151
+ />
152
+ </el-select>
153
+ </el-form-item>
154
+ <el-form-item label="Model Family">
155
+ <el-select v-model="configForm.midscene.model.family" filterable>
156
+ <el-option
157
+ v-for="option in midsceneModelFamilyOptions"
158
+ :key="option.value"
159
+ :label="option.label"
160
+ :value="option.value"
161
+ />
162
+ </el-select>
163
+ </el-form-item>
164
+ </template>
165
+
166
+ <template v-else>
167
+ <el-alert
168
+ class="config-form-alert"
169
+ type="info"
170
+ :closable="false"
171
+ title="复用 Codex 登录态,无需 API Key。请确保 codex 在 PATH 中可用,并已完成 codex login。"
172
+ />
173
+ <el-form-item label="Base URL">
174
+ <el-input :model-value="configForm.midscene.model.baseUrl" readonly />
175
+ </el-form-item>
176
+ <el-form-item label="Model Name">
177
+ <el-select
178
+ :model-value="configForm.midscene.model.name"
179
+ @change="updateMidsceneModelName"
180
+ >
181
+ <el-option
182
+ v-for="option in activeMidsceneModelOptions"
183
+ :key="option.value"
184
+ :label="option.label"
185
+ :value="option.value"
186
+ />
187
+ </el-select>
188
+ </el-form-item>
189
+ <el-form-item label="Model Family">
190
+ <el-select v-model="configForm.midscene.model.family" disabled>
191
+ <el-option label="gpt-5" value="gpt-5" />
192
+ </el-select>
193
+ </el-form-item>
194
+ </template>
195
+ <el-form-item v-if="modelTestStatus.midscene" label="测试结果">
196
+ <el-input :model-value="modelTestStatus.midscene" readonly />
197
+ </el-form-item>
198
+ </el-form>
199
+ </section>
200
+
201
+ <section>
202
+ <div class="panel-header panel-header--sub">
203
+ <span>脚本优化模型</span>
204
+ <el-button
205
+ :loading="testingModelKey === 'scriptOptimizer'"
206
+ @click="$emit('testModel', 'scriptOptimizer')"
207
+ >
208
+ 测试模型
209
+ </el-button>
210
+ </div>
211
+ <el-form label-position="top">
212
+ <el-form-item label="Base URL">
213
+ <el-input v-model="configForm.scriptOptimizer.model.baseUrl" />
214
+ </el-form-item>
215
+ <el-form-item label="API Key">
216
+ <el-input v-model="configForm.scriptOptimizer.model.apiKey" show-password />
217
+ </el-form-item>
218
+ <el-form-item label="Model Name">
219
+ <el-input v-model="configForm.scriptOptimizer.model.name" />
220
+ </el-form-item>
221
+ <el-form-item v-if="modelTestStatus.scriptOptimizer" label="测试结果">
222
+ <el-input :model-value="modelTestStatus.scriptOptimizer" readonly />
223
+ </el-form-item>
224
+ </el-form>
225
+ </section>
226
+ </div>
227
+
228
+ <ModelUsageChart
229
+ :records="modelUsageRecords"
230
+ />
231
+ </el-card>
232
+
233
+ <el-card shadow="never" class="config-module-card">
234
+ <template #header>
235
+ <div class="panel-header">
236
+ <div class="panel-header__title">
237
+ <span>预设 App 参数</span>
238
+ <el-tag v-if="appPresetForm.id" size="small" type="warning">编辑中</el-tag>
239
+ </div>
240
+ <div class="panel-header__actions">
241
+ <el-button type="primary" :loading="isSavingAppPreset" @click="$emit('saveAppPreset')">
242
+ {{ appPresetForm.id ? '保存修改' : '保存 App' }}
243
+ </el-button>
244
+ </div>
245
+ </div>
246
+ </template>
247
+
248
+ <el-form label-position="top">
249
+ <el-form-item label="App 名称">
250
+ <el-input v-model="appPresetForm.name" placeholder="例如:示例 App" />
251
+ </el-form-item>
252
+ <el-form-item label="App 包名">
253
+ <el-input v-model="appPresetForm.packageName" placeholder="例如:com.example.app" />
254
+ </el-form-item>
255
+ </el-form>
256
+
257
+ <div class="app-preset-list">
258
+ <div v-for="app in appPresets" :key="app.id" class="app-preset-row">
259
+ <div class="app-preset-row__main">
260
+ <strong>{{ app.name }}</strong>
261
+ <span>{{ app.packageName }}</span>
262
+ </div>
263
+ <div class="script-row__actions">
264
+ <el-button text size="small" :icon="Edit" @click="$emit('editAppPreset', app)" />
265
+ <el-button text size="small" :icon="Delete" @click="$emit('deleteAppPreset', app.id)" />
266
+ </div>
267
+ </div>
268
+ <el-empty v-if="!appPresets.length" description="暂无预设 App" />
269
+ </div>
270
+ </el-card>
271
+ </section>
272
+ </div>
273
+ </template>
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import AiGeneratorPage from './AiGeneratorPage.vue';
3
+ import ManualStepsPage from './ManualStepsPage.vue';
4
+ import GeneratedCodePanel from '../components/generator/GeneratedCodePanel.vue';
5
+ import type { AppPreset, GeneratorForm, GeneratorMode } from '../types';
6
+ import type { ScriptStep } from '../script-generator';
7
+
8
+ defineProps<{
9
+ form: GeneratorForm;
10
+ appPresets: AppPreset[];
11
+ promptExample: string;
12
+ steps: ScriptStep[];
13
+ generatedCode: string;
14
+ showGeneratedCode: boolean;
15
+ generatedCodeEditing: boolean;
16
+ generatedCodeEditSaving: boolean;
17
+ importingTestCase: boolean;
18
+ importedTestCaseFileName: string;
19
+ }>();
20
+
21
+ const mode = defineModel<GeneratorMode>('mode', { required: true });
22
+ const sourcePrompt = defineModel<string>('sourcePrompt', { required: true });
23
+ const generatedCodeDraft = defineModel<string>('generatedCodeDraft', { required: true });
24
+
25
+ defineEmits<{
26
+ addStep: [];
27
+ removeStep: [id: string];
28
+ copyCode: [];
29
+ editCode: [];
30
+ undoCode: [];
31
+ saveCode: [];
32
+ importTestCase: [file: File];
33
+ clearAll: [];
34
+ }>();
35
+ </script>
36
+
37
+ <template>
38
+ <div
39
+ class="generator-page"
40
+ :class="{
41
+ 'generator-page--ai': mode === 'ai',
42
+ 'generator-page--manual': mode === 'manual',
43
+ }"
44
+ >
45
+ <div class="subnav">
46
+ <button
47
+ type="button"
48
+ class="subnav__item"
49
+ :class="{ 'subnav__item--active': mode === 'ai' }"
50
+ @click="mode = 'ai'"
51
+ >
52
+ AI生成
53
+ </button>
54
+ <button
55
+ type="button"
56
+ class="subnav__item"
57
+ :class="{ 'subnav__item--active': mode === 'manual' }"
58
+ @click="mode = 'manual'"
59
+ >
60
+ 自定义步骤编排
61
+ </button>
62
+ </div>
63
+
64
+ <AiGeneratorPage
65
+ v-if="mode === 'ai'"
66
+ v-model:source-prompt="sourcePrompt"
67
+ :form="form"
68
+ :app-presets="appPresets"
69
+ :prompt-example="promptExample"
70
+ :importing-test-case="importingTestCase"
71
+ :imported-file-name="importedTestCaseFileName"
72
+ @import-test-case="$emit('importTestCase', $event)"
73
+ @clear-all="$emit('clearAll')"
74
+ />
75
+
76
+ <ManualStepsPage
77
+ v-else
78
+ :form="form"
79
+ :app-presets="appPresets"
80
+ :steps="steps"
81
+ @add-step="$emit('addStep')"
82
+ @remove-step="$emit('removeStep', $event)"
83
+ />
84
+
85
+ <GeneratedCodePanel
86
+ v-model:draft-code="generatedCodeDraft"
87
+ :code="generatedCode"
88
+ :show-code="showGeneratedCode"
89
+ :editing="generatedCodeEditing"
90
+ :saving="generatedCodeEditSaving"
91
+ @copy="$emit('copyCode')"
92
+ @edit="$emit('editCode')"
93
+ @undo="$emit('undoCode')"
94
+ @save="$emit('saveCode')"
95
+ />
96
+ </div>
97
+ </template>