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,188 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import type { ModelUsageRecord } from '../../types';
4
+
5
+ const CHART_WIDTH = 720;
6
+ const CHART_HEIGHT = 180;
7
+ const CHART_PADDING = 28;
8
+
9
+ const props = defineProps<{
10
+ records: ModelUsageRecord[];
11
+ }>();
12
+
13
+ type ChartPoint = {
14
+ x: number;
15
+ y: number;
16
+ };
17
+
18
+ const recentRecords = computed(() => props.records.slice(-20));
19
+ const displayRecords = computed(() => recentRecords.value.slice().reverse());
20
+ const latestRecord = computed(() => recentRecords.value[recentRecords.value.length - 1]);
21
+ const tokenRecords = computed(() =>
22
+ recentRecords.value.filter((record) => typeof record.totalTokens === 'number'),
23
+ );
24
+
25
+ const maxDurationMs = computed(() =>
26
+ Math.max(1, ...recentRecords.value.map((record) => record.durationMs)),
27
+ );
28
+
29
+ const maxTotalTokens = computed(() =>
30
+ Math.max(
31
+ 1,
32
+ ...tokenRecords.value.flatMap((record) => [
33
+ record.promptTokens || 0,
34
+ record.completionTokens || 0,
35
+ record.totalTokens || 0,
36
+ ]),
37
+ ),
38
+ );
39
+
40
+ const buildPoints = (records: ModelUsageRecord[], valueOf: (record: ModelUsageRecord) => number, maxValue: number) => {
41
+ const plotWidth = CHART_WIDTH - CHART_PADDING * 2;
42
+ const plotHeight = CHART_HEIGHT - CHART_PADDING * 2;
43
+ return records.map<ChartPoint>((record, index) => {
44
+ const count = Math.max(1, records.length - 1);
45
+ return {
46
+ x: CHART_PADDING + (plotWidth * index) / count,
47
+ y: CHART_HEIGHT - CHART_PADDING - (plotHeight * valueOf(record)) / maxValue,
48
+ };
49
+ });
50
+ };
51
+
52
+ const durationPoints = computed(() =>
53
+ buildPoints(recentRecords.value, (record) => record.durationMs, maxDurationMs.value),
54
+ );
55
+
56
+ const promptTokenPoints = computed(() =>
57
+ buildPoints(tokenRecords.value, (record) => record.promptTokens || 0, maxTotalTokens.value),
58
+ );
59
+
60
+ const completionTokenPoints = computed(() =>
61
+ buildPoints(tokenRecords.value, (record) => record.completionTokens || 0, maxTotalTokens.value),
62
+ );
63
+
64
+ const toPolyline = (points: ChartPoint[]) =>
65
+ points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ');
66
+
67
+ const durationPolyline = computed(() => toPolyline(durationPoints.value));
68
+ const promptTokenPolyline = computed(() => toPolyline(promptTokenPoints.value));
69
+ const completionTokenPolyline = computed(() => toPolyline(completionTokenPoints.value));
70
+
71
+ const tokenLabel = (value?: number) => (typeof value === 'number' ? String(value) : '未返回');
72
+
73
+ const formatTime = (isoTime: string) =>
74
+ new Intl.DateTimeFormat('zh-CN', {
75
+ hour: '2-digit',
76
+ minute: '2-digit',
77
+ second: '2-digit',
78
+ }).format(new Date(isoTime));
79
+ </script>
80
+
81
+ <template>
82
+ <section class="model-usage-chart">
83
+ <div class="panel-header panel-header--sub">
84
+ <div class="panel-header__title">
85
+ <span>测试消耗统计</span>
86
+ </div>
87
+ </div>
88
+
89
+ <div v-if="recentRecords.length" class="model-usage-chart__body">
90
+ <div class="model-usage-chart__summary">
91
+ <span>最近 {{ recentRecords.length }} 次</span>
92
+ <span>最新耗时 {{ latestRecord?.durationMs || 0 }}ms</span>
93
+ <span>输入 {{ tokenLabel(latestRecord?.promptTokens) }}</span>
94
+ <span>输出 {{ tokenLabel(latestRecord?.completionTokens) }}</span>
95
+ <span>总计 {{ tokenLabel(latestRecord?.totalTokens) }}</span>
96
+ </div>
97
+
98
+ <svg
99
+ class="model-usage-chart__svg"
100
+ :viewBox="`0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`"
101
+ role="img"
102
+ aria-label="测试模型消耗统计曲线"
103
+ >
104
+ <line
105
+ :x1="CHART_PADDING"
106
+ :y1="CHART_HEIGHT - CHART_PADDING"
107
+ :x2="CHART_WIDTH - CHART_PADDING"
108
+ :y2="CHART_HEIGHT - CHART_PADDING"
109
+ class="model-usage-chart__axis"
110
+ />
111
+ <line
112
+ :x1="CHART_PADDING"
113
+ :y1="CHART_PADDING"
114
+ :x2="CHART_PADDING"
115
+ :y2="CHART_HEIGHT - CHART_PADDING"
116
+ class="model-usage-chart__axis"
117
+ />
118
+ <polyline
119
+ v-if="promptTokenPolyline"
120
+ :points="promptTokenPolyline"
121
+ class="model-usage-chart__line model-usage-chart__line--prompt"
122
+ />
123
+ <polyline
124
+ v-if="completionTokenPolyline"
125
+ :points="completionTokenPolyline"
126
+ class="model-usage-chart__line model-usage-chart__line--completion"
127
+ />
128
+ <polyline
129
+ :points="durationPolyline"
130
+ class="model-usage-chart__line model-usage-chart__line--duration"
131
+ />
132
+ <circle
133
+ v-for="point in promptTokenPoints"
134
+ :key="`prompt-${point.x}-${point.y}`"
135
+ :cx="point.x"
136
+ :cy="point.y"
137
+ r="3"
138
+ class="model-usage-chart__point model-usage-chart__point--prompt"
139
+ />
140
+ <circle
141
+ v-for="point in completionTokenPoints"
142
+ :key="`completion-${point.x}-${point.y}`"
143
+ :cx="point.x"
144
+ :cy="point.y"
145
+ r="3"
146
+ class="model-usage-chart__point model-usage-chart__point--completion"
147
+ />
148
+ <circle
149
+ v-for="point in durationPoints"
150
+ :key="`duration-${point.x}-${point.y}`"
151
+ :cx="point.x"
152
+ :cy="point.y"
153
+ r="3"
154
+ class="model-usage-chart__point model-usage-chart__point--duration"
155
+ />
156
+ </svg>
157
+
158
+ <div class="model-usage-chart__legend">
159
+ <span><i class="model-usage-chart__swatch model-usage-chart__swatch--prompt"></i>输入 tokens</span>
160
+ <span><i class="model-usage-chart__swatch model-usage-chart__swatch--completion"></i>输出 tokens</span>
161
+ <span><i class="model-usage-chart__swatch model-usage-chart__swatch--duration"></i>耗时 ms</span>
162
+ </div>
163
+
164
+ <div class="model-usage-chart__records">
165
+ <div class="model-usage-chart__record model-usage-chart__record--head">
166
+ <span>时间</span>
167
+ <span>状态</span>
168
+ <span>模型</span>
169
+ <span>输入</span>
170
+ <span>输出</span>
171
+ <span>总计</span>
172
+ <span>耗时</span>
173
+ </div>
174
+ <div v-for="record in displayRecords" :key="record.id" class="model-usage-chart__record">
175
+ <span>{{ formatTime(record.createdAt) }}</span>
176
+ <span>{{ record.success ? '成功' : '失败' }}</span>
177
+ <span>{{ record.modelName }}</span>
178
+ <span>{{ tokenLabel(record.promptTokens) }}</span>
179
+ <span>{{ tokenLabel(record.completionTokens) }}</span>
180
+ <span>{{ tokenLabel(record.totalTokens) }}</span>
181
+ <span>{{ record.durationMs }}ms</span>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <el-empty v-else description="测试 AI 生成模型或生成脚本后显示消耗曲线" />
187
+ </section>
188
+ </template>
@@ -0,0 +1,266 @@
1
+ <script setup lang="ts">
2
+ import { computed, shallowRef } from 'vue';
3
+ import { Cpu, Refresh } from '@element-plus/icons-vue';
4
+ import type { AndroidDevice, DeviceAction } from '../../types';
5
+
6
+ type DeviceOverlayBounds = {
7
+ id: string;
8
+ left: number;
9
+ top: number;
10
+ right: number;
11
+ bottom: number;
12
+ };
13
+
14
+ const props = defineProps<{
15
+ available: boolean;
16
+ devices: AndroidDevice[];
17
+ selectedDeviceId: string;
18
+ frameUrl?: string;
19
+ imageUrl?: string;
20
+ previewError?: string;
21
+ actions?: readonly DeviceAction[];
22
+ overlayBounds?: readonly DeviceOverlayBounds[];
23
+ selectedBounds?: DeviceOverlayBounds;
24
+ deviceWidth?: number;
25
+ deviceHeight?: number;
26
+ }>();
27
+
28
+ const emit = defineEmits<{
29
+ switchDevice: [deviceId: string];
30
+ triggerKey: [keyCode: number];
31
+ previewLoaded: [size: { width: number; height: number }];
32
+ previewError: [];
33
+ refreshPreview: [];
34
+ tap: [point: { x: number; y: number }];
35
+ swipe: [gesture: {
36
+ startX: number;
37
+ startY: number;
38
+ endX: number;
39
+ endY: number;
40
+ duration: number;
41
+ }];
42
+ }>();
43
+
44
+ const imageSize = shallowRef({ width: 0, height: 0 });
45
+ let pointerSession: {
46
+ pointerId: number;
47
+ start: { x: number; y: number };
48
+ startClientX: number;
49
+ startClientY: number;
50
+ } | null = null;
51
+ const hasPreview = computed(() => Boolean(props.frameUrl || props.imageUrl));
52
+ const imageBoxStyle = computed(() => {
53
+ if (props.frameUrl) return {};
54
+ const width = props.deviceWidth || imageSize.value.width;
55
+ const height = props.deviceHeight || imageSize.value.height;
56
+ return { aspectRatio: width && height ? `${width} / ${height}` : '9 / 20' };
57
+ });
58
+ const overlayViewBox = computed(() => {
59
+ const width = props.deviceWidth || imageSize.value.width;
60
+ const height = props.deviceHeight || imageSize.value.height;
61
+ return width && height ? `0 0 ${width} ${height}` : '';
62
+ });
63
+
64
+ function updateImageSize(event: Event) {
65
+ const image = event.target as HTMLImageElement;
66
+ imageSize.value = { width: image.naturalWidth, height: image.naturalHeight };
67
+ emit('previewLoaded', imageSize.value);
68
+ }
69
+
70
+ function getDevicePoint(target: HTMLElement, clientX: number, clientY: number) {
71
+ const width = props.deviceWidth || imageSize.value.width;
72
+ const height = props.deviceHeight || imageSize.value.height;
73
+ if (!width || !height) return;
74
+ const rect = target.getBoundingClientRect();
75
+ if (!rect.width || !rect.height) return;
76
+
77
+ const deviceAspect = width / height;
78
+ const containerAspect = rect.width / rect.height;
79
+ let drawWidth = rect.width;
80
+ let drawHeight = rect.height;
81
+ let offsetX = 0;
82
+ let offsetY = 0;
83
+
84
+ if (containerAspect > deviceAspect) {
85
+ drawWidth = rect.height * deviceAspect;
86
+ offsetX = (rect.width - drawWidth) / 2;
87
+ } else {
88
+ drawHeight = rect.width / deviceAspect;
89
+ offsetY = (rect.height - drawHeight) / 2;
90
+ }
91
+
92
+ const localX = Math.max(0, Math.min(drawWidth, clientX - rect.left - offsetX));
93
+ const localY = Math.max(0, Math.min(drawHeight, clientY - rect.top - offsetY));
94
+ return {
95
+ x: Math.round((localX / drawWidth) * width),
96
+ y: Math.round((localY / drawHeight) * height),
97
+ };
98
+ }
99
+
100
+ function handlePointerDown(event: PointerEvent) {
101
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
102
+ const target = event.currentTarget as HTMLElement;
103
+ const point = getDevicePoint(target, event.clientX, event.clientY);
104
+ if (!point) return;
105
+ target.setPointerCapture(event.pointerId);
106
+ pointerSession = {
107
+ pointerId: event.pointerId,
108
+ start: point,
109
+ startClientX: event.clientX,
110
+ startClientY: event.clientY,
111
+ };
112
+ }
113
+
114
+ function handlePointerUp(event: PointerEvent) {
115
+ if (!pointerSession || pointerSession.pointerId !== event.pointerId) return;
116
+ const target = event.currentTarget as HTMLElement;
117
+ const point = getDevicePoint(target, event.clientX, event.clientY);
118
+ const session = pointerSession;
119
+ pointerSession = null;
120
+ if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
121
+ if (!point) return;
122
+
123
+ const moved = Math.hypot(
124
+ event.clientX - session.startClientX,
125
+ event.clientY - session.startClientY,
126
+ );
127
+ if (moved < 8) {
128
+ emit('tap', session.start);
129
+ return;
130
+ }
131
+
132
+ emit('swipe', {
133
+ startX: session.start.x,
134
+ startY: session.start.y,
135
+ endX: point.x,
136
+ endY: point.y,
137
+ duration: 120,
138
+ });
139
+ }
140
+
141
+ function handlePointerCancel(event: PointerEvent) {
142
+ if (pointerSession?.pointerId === event.pointerId) pointerSession = null;
143
+ }
144
+ </script>
145
+
146
+ <template>
147
+ <el-card shadow="never" class="automation-card device-preview-card">
148
+ <template #header>
149
+ <div class="panel-header">
150
+ <span>设备预览</span>
151
+ <div class="device-status">
152
+ <el-button
153
+ text
154
+ circle
155
+ :icon="Refresh"
156
+ :disabled="!selectedDeviceId"
157
+ title="刷新画面"
158
+ @click="emit('refreshPreview')"
159
+ />
160
+ <el-tag :type="available ? 'success' : 'info'">
161
+ {{ available ? 'ADB 已连接' : '未检测到设备' }}
162
+ </el-tag>
163
+ </div>
164
+ </div>
165
+ </template>
166
+
167
+ <div class="device-toolbar">
168
+ <el-select
169
+ :model-value="selectedDeviceId"
170
+ placeholder="选择设备"
171
+ class="device-select"
172
+ @change="emit('switchDevice', String($event))"
173
+ >
174
+ <el-option
175
+ v-for="device in devices"
176
+ :key="device.id"
177
+ :label="`${device.id}${device.description ? ` · ${device.description}` : ''}`"
178
+ :value="device.id"
179
+ />
180
+ </el-select>
181
+ </div>
182
+
183
+ <div v-if="actions?.length" class="device-actions">
184
+ <button
185
+ v-for="action in actions"
186
+ :key="action.key"
187
+ type="button"
188
+ class="device-action-button"
189
+ @click="emit('triggerKey', action.keyCode)"
190
+ >
191
+ <img :src="action.icon" :alt="action.label" class="device-action-button__icon" />
192
+ <span class="device-action-button__tooltip">{{ action.label }}</span>
193
+ </button>
194
+ </div>
195
+
196
+ <div
197
+ class="device-preview"
198
+ :class="{ 'device-preview--image': imageUrl && !frameUrl }"
199
+ >
200
+ <div
201
+ v-if="hasPreview"
202
+ class="device-preview__interactive"
203
+ :class="{ 'device-preview__interactive--image': !frameUrl }"
204
+ :style="imageBoxStyle"
205
+ >
206
+ <div
207
+ class="device-preview__surface"
208
+ @pointerdown="handlePointerDown"
209
+ @pointerup="handlePointerUp"
210
+ @pointercancel="handlePointerCancel"
211
+ />
212
+ <iframe
213
+ v-if="frameUrl"
214
+ :key="frameUrl"
215
+ :src="frameUrl"
216
+ title="Android Device Preview"
217
+ sandbox="allow-scripts allow-same-origin allow-forms"
218
+ class="device-preview__frame"
219
+ />
220
+ <img
221
+ v-else
222
+ :src="imageUrl"
223
+ alt="Android Device Preview"
224
+ class="device-preview__frame device-preview__image"
225
+ @load="updateImageSize"
226
+ @error="emit('previewError')"
227
+ />
228
+ <svg
229
+ v-if="overlayViewBox && (overlayBounds?.length || selectedBounds)"
230
+ class="device-preview__overlay"
231
+ :viewBox="overlayViewBox"
232
+ preserveAspectRatio="xMidYMid meet"
233
+ aria-hidden="true"
234
+ >
235
+ <rect
236
+ v-for="bounds in overlayBounds"
237
+ :key="bounds.id"
238
+ class="device-preview__node-bound"
239
+ :x="bounds.left"
240
+ :y="bounds.top"
241
+ :width="Math.max(0, bounds.right - bounds.left)"
242
+ :height="Math.max(0, bounds.bottom - bounds.top)"
243
+ vector-effect="non-scaling-stroke"
244
+ />
245
+ <rect
246
+ v-if="selectedBounds"
247
+ class="device-preview__selected-bound"
248
+ :x="selectedBounds.left"
249
+ :y="selectedBounds.top"
250
+ :width="Math.max(0, selectedBounds.right - selectedBounds.left)"
251
+ :height="Math.max(0, selectedBounds.bottom - selectedBounds.top)"
252
+ vector-effect="non-scaling-stroke"
253
+ />
254
+ </svg>
255
+ </div>
256
+ <div v-else class="device-preview__empty">
257
+ <Cpu class="device-preview__icon" />
258
+ <p v-if="selectedDeviceId" class="device-preview__meta">
259
+ 当前设备:{{ selectedDeviceId }}
260
+ </p>
261
+ <p v-if="previewError">{{ previewError }}</p>
262
+ <p v-else>请选择可用设备</p>
263
+ </div>
264
+ </div>
265
+ </el-card>
266
+ </template>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import { CopyDocument, Edit, RefreshLeft } from '@element-plus/icons-vue';
3
+
4
+ defineProps<{
5
+ code: string;
6
+ showCode: boolean;
7
+ editing: boolean;
8
+ saving: boolean;
9
+ }>();
10
+
11
+ const draftCode = defineModel<string>('draftCode', { required: true });
12
+
13
+ defineEmits<{
14
+ copy: [];
15
+ edit: [];
16
+ undo: [];
17
+ save: [];
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <el-card shadow="never" class="generator-code-card">
23
+ <template #header>
24
+ <div class="panel-header">
25
+ <span>生成代码</span>
26
+ <div class="panel-header__actions">
27
+ <el-button
28
+ type="success"
29
+ :icon="CopyDocument"
30
+ :disabled="!showCode"
31
+ @click="$emit('copy')"
32
+ >
33
+ 复制
34
+ </el-button>
35
+ <el-button
36
+ v-if="!editing"
37
+ type="primary"
38
+ :icon="Edit"
39
+ :disabled="!showCode"
40
+ @click="$emit('edit')"
41
+ >
42
+ 编辑
43
+ </el-button>
44
+ <template v-else>
45
+ <el-button :icon="RefreshLeft" :disabled="saving" @click="$emit('undo')">
46
+ 撤回修改
47
+ </el-button>
48
+ <el-button
49
+ type="primary"
50
+ :loading="saving"
51
+ @click="$emit('save')"
52
+ >
53
+ 保存
54
+ </el-button>
55
+ </template>
56
+ </div>
57
+ </div>
58
+ </template>
59
+
60
+ <el-input
61
+ v-if="editing"
62
+ v-model="draftCode"
63
+ type="textarea"
64
+ resize="none"
65
+ spellcheck="false"
66
+ class="generator-code-editor"
67
+ />
68
+ <pre v-else class="code-block generator-code-block"><code>{{ showCode ? code : '' }}</code></pre>
69
+ </el-card>
70
+ </template>
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import { Document, UploadFilled } from '@element-plus/icons-vue';
3
+ import { ElMessage } from 'element-plus';
4
+ import { useTemplateRef } from 'vue';
5
+
6
+ defineProps<{
7
+ uploading: boolean;
8
+ fileName: string;
9
+ }>();
10
+
11
+ const emit = defineEmits<{
12
+ upload: [file: File];
13
+ }>();
14
+
15
+ const fileInput = useTemplateRef<HTMLInputElement>('fileInput');
16
+ const allowedExtensions = new Set(['txt', 'xls', 'xlsx', 'doc', 'docx']);
17
+ const maxFileSize = 10 * 1024 * 1024;
18
+
19
+ const openFilePicker = () => {
20
+ if (!fileInput.value) return;
21
+ fileInput.value.value = '';
22
+ fileInput.value.click();
23
+ };
24
+
25
+ const handleFileChange = (event: Event) => {
26
+ const input = event.target as HTMLInputElement;
27
+ const file = input.files?.[0];
28
+ if (!file) return;
29
+
30
+ const extension = file.name.split('.').pop()?.toLowerCase() || '';
31
+ if (!allowedExtensions.has(extension)) {
32
+ ElMessage.error('仅支持 txt、xls、xlsx、doc、docx 格式');
33
+ return;
34
+ }
35
+ if (file.size > maxFileSize) {
36
+ ElMessage.error('文件大小不能超过 10MB');
37
+ return;
38
+ }
39
+
40
+ emit('upload', file);
41
+ };
42
+ </script>
43
+
44
+ <template>
45
+ <div class="test-case-upload">
46
+ <el-tooltip v-if="fileName" :content="fileName" placement="top">
47
+ <span class="test-case-upload__file">
48
+ <el-icon><Document /></el-icon>
49
+ <span class="test-case-upload__name">{{ fileName }}</span>
50
+ </span>
51
+ </el-tooltip>
52
+ <el-button
53
+ size="small"
54
+ :icon="UploadFilled"
55
+ :loading="uploading"
56
+ @click="openFilePicker"
57
+ >
58
+ 上传用例
59
+ </el-button>
60
+ <input
61
+ ref="fileInput"
62
+ class="test-case-upload__input"
63
+ type="file"
64
+ accept=".txt,.xls,.xlsx,.doc,.docx"
65
+ @change="handleFileChange"
66
+ />
67
+ </div>
68
+ </template>
69
+
70
+ <style scoped>
71
+ .test-case-upload {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 8px;
75
+ min-width: 0;
76
+ }
77
+
78
+ .test-case-upload__file {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 4px;
82
+ min-width: 0;
83
+ max-width: 132px;
84
+ color: #606266;
85
+ font-size: 12px;
86
+ }
87
+
88
+ .test-case-upload__name {
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
91
+ white-space: nowrap;
92
+ }
93
+
94
+ .test-case-upload__input {
95
+ display: none;
96
+ }
97
+ </style>
@@ -0,0 +1,75 @@
1
+ export type MidsceneModelProvider = 'custom' | 'codex';
2
+ export type MidsceneModelPresetKey = 'gpt' | 'doubao';
3
+
4
+ export type MidsceneModelPreset = {
5
+ key: MidsceneModelPresetKey;
6
+ label: string;
7
+ baseUrl: string;
8
+ modelName: string;
9
+ modelFamily: string;
10
+ };
11
+
12
+ export type MidsceneModelOption = {
13
+ label: string;
14
+ value: string;
15
+ family: string;
16
+ };
17
+
18
+ export const codexMidsceneModel = {
19
+ provider: 'codex' as const,
20
+ baseUrl: 'codex://app-server',
21
+ apiKey: '',
22
+ name: 'gpt-5.5',
23
+ family: 'gpt-5',
24
+ };
25
+
26
+ export const codexMidsceneModelOptions: MidsceneModelOption[] = [
27
+ { label: 'GPT-5.5(推荐)', value: 'gpt-5.5', family: 'gpt-5' },
28
+ { label: 'GPT-5.6 Sol', value: 'gpt-5.6-sol', family: 'gpt-5' },
29
+ { label: 'GPT-5.6 Terra', value: 'gpt-5.6-terra', family: 'gpt-5' },
30
+ { label: 'GPT-5.6 Luna', value: 'gpt-5.6-luna', family: 'gpt-5' },
31
+ { label: 'GPT-5.4', value: 'gpt-5.4', family: 'gpt-5' },
32
+ ];
33
+
34
+ export const midsceneModelOptions: MidsceneModelOption[] = [
35
+ ...codexMidsceneModelOptions,
36
+ { label: 'Doubao Seed 2.1 Pro 260628', value: 'doubao-seed-2-1-pro-260628', family: 'doubao-seed' },
37
+ { label: 'Doubao Seed 2.1 Turbo', value: 'doubao-seed-2.1-turbo', family: 'doubao-seed' },
38
+ { label: 'Doubao Seed 2.0 Lite', value: 'doubao-seed-2.0-lite', family: 'doubao-seed' },
39
+ { label: 'Doubao Seed 1.6 Vision', value: 'doubao-seed-1.6-vision', family: 'doubao-seed' },
40
+ { label: 'Doubao Seed 1.8', value: 'doubao-seed-1.8', family: 'doubao-seed' },
41
+ { label: 'Qwen 3.7 Plus', value: 'qwen3.7-plus', family: 'qwen3' },
42
+ { label: 'Qwen 3.5 Plus', value: 'qwen3.5-plus', family: 'qwen3' },
43
+ { label: 'Qwen 3.6 Plus', value: 'qwen3.6-plus', family: 'qwen3' },
44
+ { label: 'Qwen 3 VL Plus', value: 'qwen3-vl-plus', family: 'qwen3-vl' },
45
+ { label: 'Qwen VL Max Latest', value: 'qwen-vl-max-latest', family: 'qwen2.5-vl' },
46
+ { label: 'Gemini 3.5 Flash', value: 'gemini-3.5-flash', family: 'gemini' },
47
+ { label: 'Gemini 3 Flash Preview', value: 'gemini-3-flash-preview', family: 'gemini' },
48
+ { label: 'Kimi K3', value: 'kimi-k3', family: 'kimi3' },
49
+ { label: 'Kimi K2.5', value: 'kimi-k2.5', family: 'kimi' },
50
+ { label: 'Kimi K2.6', value: 'kimi-k2.6', family: 'kimi' },
51
+ { label: 'MiMo V2.5', value: 'mimo-v2.5', family: 'xiaomi-mimo' },
52
+ { label: 'GLM 5V Turbo', value: 'glm-5v-turbo', family: 'glm-v' },
53
+ { label: 'GLM 4.6V', value: 'glm-4.6v', family: 'glm-v' },
54
+ ];
55
+
56
+ export const midsceneModelFamilyOptions = Array.from(
57
+ new Set(midsceneModelOptions.map((option) => option.family)),
58
+ ).map((family) => ({ label: family, value: family }));
59
+
60
+ export const midsceneModelPresets: MidsceneModelPreset[] = [
61
+ {
62
+ key: 'gpt',
63
+ label: 'GPT',
64
+ baseUrl: 'https://api.openai.com/v1',
65
+ modelName: 'gpt-5.5',
66
+ modelFamily: 'gpt-5',
67
+ },
68
+ {
69
+ key: 'doubao',
70
+ label: 'Doubao',
71
+ baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
72
+ modelName: 'doubao-seed-2-1-pro-260628',
73
+ modelFamily: 'doubao-seed',
74
+ },
75
+ ];