android-midscene-automation 0.1.41 → 0.1.42

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 (58) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +13 -0
  3. package/USAGE.md +2 -0
  4. package/package.json +2 -1
  5. package/remote-agent/index.ts +1 -0
  6. package/server/appium-auto-save.test.ts +60 -0
  7. package/server/appium-config.test.ts +118 -0
  8. package/server/appium-recorder/ai-deduplication.test.ts +80 -0
  9. package/server/appium-recorder/ai-deduplication.ts +74 -0
  10. package/server/appium-recorder/ai-observation.test.ts +125 -0
  11. package/server/appium-recorder/ai-observation.ts +125 -0
  12. package/server/appium-recorder/ai-recognition.ts +118 -22
  13. package/server/appium-recorder/appium-runner.ts +59 -14
  14. package/server/appium-recorder/history-capture.ts +1 -0
  15. package/server/appium-recorder/image-check.ts +1 -1
  16. package/server/appium-recorder/managed-appium.ts +15 -0
  17. package/server/appium-recorder/managed-cleanup.test.ts +82 -0
  18. package/server/appium-recorder/replay-log.ts +13 -0
  19. package/server/appium-recorder/report.ts +14 -12
  20. package/server/appium-recorder/repository.ts +5 -0
  21. package/server/appium-recorder/routes.ts +26 -2
  22. package/server/appium-recorder/screenshot.ts +1 -0
  23. package/server/config.ts +19 -4
  24. package/server/http-api.ts +26 -4
  25. package/server/model-tester.ts +2 -1
  26. package/src/App.vue +41 -29
  27. package/src/api.ts +10 -3
  28. package/src/appium-recorder/AppiumPage.vue +41 -6
  29. package/src/appium-recorder/RunHistoryDialog.vue +1 -1
  30. package/src/appium-recorder/action-descriptions.ts +1 -1
  31. package/src/appium-recorder/ai-deduplication.ts +34 -0
  32. package/src/appium-recorder/ai-prompt-presets.ts +34 -0
  33. package/src/appium-recorder/ai-recognition.ts +27 -2
  34. package/src/appium-recorder/api.ts +14 -0
  35. package/src/appium-recorder/components/AiRecognitionPromptInput.vue +41 -0
  36. package/src/appium-recorder/components/AiRecognitionSettings.vue +143 -0
  37. package/src/appium-recorder/components/AiRecognitionTestDialog.vue +86 -0
  38. package/src/appium-recorder/components/FlowCanvas.vue +3 -2
  39. package/src/appium-recorder/components/FlowNodeCard.vue +2 -2
  40. package/src/appium-recorder/components/FlowStepEditor.vue +8 -2
  41. package/src/appium-recorder/components/NestedConditionBranches.vue +7 -1
  42. package/src/appium-recorder/components/RecordedSteps.vue +7 -1
  43. package/src/appium-recorder/flow-appearance.ts +12 -1
  44. package/src/appium-recorder/flow-graph.ts +2 -2
  45. package/src/appium-recorder/flow-labels.ts +6 -4
  46. package/src/appium-recorder/types.ts +4 -0
  47. package/src/components/config/AiDeduplicationSettings.vue +59 -0
  48. package/src/components/config/AiPromptPresetsSettings.vue +66 -0
  49. package/src/components/config/AppiumConfigPanel.vue +48 -3
  50. package/src/components/config/BaseConfigPanel.vue +20 -8
  51. package/src/components/config/FlowAppearanceSettings.vue +51 -31
  52. package/src/components/config/MidsceneConfigPanel.vue +84 -86
  53. package/src/config/midscene-model-presets.ts +37 -9
  54. package/src/config/use-appium-auto-save.ts +54 -0
  55. package/src/pages/ConfigPage.vue +4 -4
  56. package/src/style.css +5 -10
  57. package/src/types.ts +3 -1
  58. package/src/ui-refresh.css +17 -4
@@ -1,9 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue';
3
- import { QuestionFilled } from '@element-plus/icons-vue';
2
+ import { computed, reactive } from 'vue';
3
+ import { ArrowDown, QuestionFilled } from '@element-plus/icons-vue';
4
4
  import {
5
5
  codexMidsceneModelOptions,
6
6
  midsceneModelPresets,
7
+ midsceneModelOptions,
8
+ midsceneModelFamilyOptions,
7
9
  type MidsceneModelProvider,
8
10
  } from '../../config/midscene-model-presets';
9
11
  import type { ConfigForm } from '../../types';
@@ -11,44 +13,52 @@ import type { ConfigForm } from '../../types';
11
13
  const props = defineProps<{
12
14
  configForm: ConfigForm;
13
15
  testingModelKey: string;
14
- isSavingModelConfig: boolean;
15
16
  modelTestStatus: { midscene: string; scriptOptimizer: string };
16
17
  }>();
17
18
 
18
19
  const emit = defineEmits<{
19
20
  testModel: [key: 'midscene' | 'scriptOptimizer'];
20
- saveModelConfig: [];
21
21
  updateMidsceneModelProvider: [provider: MidsceneModelProvider];
22
22
  }>();
23
23
 
24
24
  const activeMidsceneProvider = computed<MidsceneModelProvider>(() =>
25
- props.configForm.midscene.model.provider === 'codex' ? 'codex' : 'custom',
25
+ props.configForm.midscene.model.provider || 'custom',
26
26
  );
27
- const modelConfigGuideUrl = 'https://midscenejs.com/zh/model-common-config.html';
28
-
29
- const normalizeBaseUrl = (value: string) => value.trim().replace(/\/+$/, '');
27
+ const activePreset = computed(() => midsceneModelPresets.find(item => item.key === activeMidsceneProvider.value));
28
+ const modelFamilyOptions = computed(() => {
29
+ if (activeMidsceneProvider.value === 'custom') return midsceneModelFamilyOptions;
30
+ const families = activeMidsceneProvider.value === 'codex'
31
+ ? codexMidsceneModelOptions.map(option => option.family)
32
+ : activePreset.value?.modelFamilies ?? (activePreset.value ? [activePreset.value.modelFamily] : []);
33
+ return [...new Set(families)].map(family => ({ label: family, value: family }));
34
+ });
35
+ const modelNameOptions = computed(() => activeMidsceneProvider.value === 'codex'
36
+ ? codexMidsceneModelOptions
37
+ : midsceneModelOptions.filter(option => modelFamilyOptions.value.some(family => family.value === option.family)));
38
+ const modelSearchActive = reactive({ name: false, family: false });
39
+ const modelSuggestions = (field: 'name' | 'family', query: string) => {
40
+ const options = field === 'name' ? modelNameOptions.value : modelFamilyOptions.value;
41
+ return modelSearchActive[field]
42
+ ? options.filter(option => option.value.toLowerCase().includes(query.toLowerCase()))
43
+ : options;
44
+ };
45
+ const modelConfigGuideUrl = 'https://midscenejs.com/zh/model-common-config.html#glm-v';
30
46
 
31
47
  const updateMidsceneModelName = (value: string) => {
32
48
  props.configForm.midscene.model.name = value;
33
- const option = codexMidsceneModelOptions.find((item) => item.value === value);
49
+ const option = modelNameOptions.value.find((item) => item.value === value);
34
50
  if (option) {
35
51
  props.configForm.midscene.model.family = option.family;
36
52
  }
37
53
  };
38
54
 
39
- const updateCustomMidsceneBaseUrl = (value: string) => {
40
- props.configForm.midscene.model.baseUrl = value;
41
- const preset = midsceneModelPresets.find((item) => normalizeBaseUrl(item.baseUrl) === normalizeBaseUrl(value));
42
- if (preset) {
43
- props.configForm.midscene.model.name = preset.modelName;
44
- props.configForm.midscene.model.family = preset.modelFamily;
45
- }
46
- };
47
-
48
55
  const updateMidsceneProvider = (value: string) => {
49
56
  if (value === 'custom' || value === 'codex') {
50
57
  emit('updateMidsceneModelProvider', value);
58
+ return;
51
59
  }
60
+ const preset = midsceneModelPresets.find(item => item.key === value);
61
+ if (preset) emit('updateMidsceneModelProvider', preset.key);
52
62
  };
53
63
 
54
64
  const openModelConfigGuide = () => {
@@ -57,13 +67,21 @@ const openModelConfigGuide = () => {
57
67
  </script>
58
68
 
59
69
  <template>
60
- <el-card shadow="never" class="config-module-card">
70
+ <el-card shadow="never" class="config-module-card config-midscene-card">
61
71
  <template #header>
62
72
  <div class="panel-header">
63
- <span>模型配置</span>
64
- <el-button type="primary" :loading="isSavingModelConfig" @click="$emit('saveModelConfig')">
65
- 保存模型配置
66
- </el-button>
73
+ <span class="panel-header__title">
74
+ <span>模型配置</span>
75
+ <el-tooltip content="模型参考配置" placement="top" :show-after="200">
76
+ <el-button
77
+ class="config-model-help"
78
+ text
79
+ :icon="QuestionFilled"
80
+ aria-label="模型参考配置"
81
+ @click="openModelConfigGuide"
82
+ />
83
+ </el-tooltip>
84
+ </span>
67
85
  </div>
68
86
  </template>
69
87
 
@@ -73,71 +91,36 @@ const openModelConfigGuide = () => {
73
91
  <span>Midscene 模型</span>
74
92
  <el-button
75
93
  :loading="testingModelKey === 'midscene'"
94
+ :disabled="!!testingModelKey"
76
95
  @click="$emit('testModel', 'midscene')"
77
96
  >
78
- 测试模型
97
+ 测试并保存
79
98
  </el-button>
80
99
  </div>
81
- <el-form label-position="top">
100
+ <el-form label-position="top" :disabled="!!testingModelKey">
82
101
  <el-form-item label="接入方式">
83
102
  <el-select
84
103
  :model-value="activeMidsceneProvider"
85
104
  @change="updateMidsceneProvider"
86
105
  >
87
106
  <el-option label="自定义提供方" value="custom" />
107
+ <el-option v-for="preset in midsceneModelPresets" :key="preset.key" :label="preset.label" :value="preset.key" />
88
108
  <el-option label="使用 Codex" value="codex" />
89
109
  </el-select>
90
110
  </el-form-item>
91
111
 
92
- <template v-if="activeMidsceneProvider === 'custom'">
112
+ <template v-if="activeMidsceneProvider !== 'codex'">
113
+ <el-alert v-if="activePreset?.hint" class="config-form-alert" type="info" :closable="false" :title="activePreset.hint" />
93
114
  <el-form-item label="Base URL">
94
115
  <el-input
95
- :model-value="configForm.midscene.model.baseUrl"
96
- placeholder="输入 Base URL,匹配已知提供方时自动填充模型"
97
- @input="updateCustomMidsceneBaseUrl"
116
+ v-model="configForm.midscene.model.baseUrl"
117
+ placeholder="选择接入方式自动填充,也可手动修改"
98
118
  />
99
119
  </el-form-item>
100
120
  <el-form-item label="API Key">
101
121
  <el-input v-model="configForm.midscene.model.apiKey" show-password />
102
122
  </el-form-item>
103
- <el-form-item>
104
- <template #label>
105
- <span class="config-field-label">
106
- <span>Model Name</span>
107
- <el-tooltip content="查看模型填写参考" placement="top">
108
- <el-button
109
- class="config-field-help"
110
- text
111
- size="small"
112
- :icon="QuestionFilled"
113
- aria-label="查看 Model Name 填写参考"
114
- @click.stop="openModelConfigGuide"
115
- />
116
- </el-tooltip>
117
- </span>
118
- </template>
119
- <el-input v-model="configForm.midscene.model.name" placeholder="例如:gpt-5.5" />
120
- </el-form-item>
121
- <el-form-item>
122
- <template #label>
123
- <span class="config-field-label">
124
- <span>Model Family</span>
125
- <el-tooltip content="查看模型填写参考" placement="top">
126
- <el-button
127
- class="config-field-help"
128
- text
129
- size="small"
130
- :icon="QuestionFilled"
131
- aria-label="查看 Model Family 填写参考"
132
- @click.stop="openModelConfigGuide"
133
- />
134
- </el-tooltip>
135
- </span>
136
- </template>
137
- <el-input v-model="configForm.midscene.model.family" placeholder="例如:gpt-5" />
138
- </el-form-item>
139
123
  </template>
140
-
141
124
  <template v-else>
142
125
  <el-alert
143
126
  class="config-form-alert"
@@ -148,25 +131,34 @@ const openModelConfigGuide = () => {
148
131
  <el-form-item label="Base URL">
149
132
  <el-input :model-value="configForm.midscene.model.baseUrl" readonly />
150
133
  </el-form-item>
151
- <el-form-item label="Model Name">
152
- <el-select
153
- :model-value="configForm.midscene.model.name"
154
- @change="updateMidsceneModelName"
155
- >
156
- <el-option
157
- v-for="option in codexMidsceneModelOptions"
158
- :key="option.value"
159
- :label="option.label"
160
- :value="option.value"
161
- />
162
- </el-select>
163
- </el-form-item>
164
- <el-form-item label="Model Family">
165
- <el-select v-model="configForm.midscene.model.family" disabled>
166
- <el-option label="gpt-5" value="gpt-5" />
167
- </el-select>
168
- </el-form-item>
169
134
  </template>
135
+ <el-form-item label="Model Name">
136
+ <el-autocomplete
137
+ class="config-model-input"
138
+ :model-value="configForm.midscene.model.name"
139
+ :fetch-suggestions="(query: string) => modelSuggestions('name', query)"
140
+ :debounce="0"
141
+ :suffix-icon="ArrowDown"
142
+ fit-input-width
143
+ placeholder="选择或输入模型名称"
144
+ @focus="modelSearchActive.name = false"
145
+ @input="modelSearchActive.name = true"
146
+ @update:model-value="updateMidsceneModelName"
147
+ />
148
+ </el-form-item>
149
+ <el-form-item label="Model Family">
150
+ <el-autocomplete
151
+ v-model="configForm.midscene.model.family"
152
+ class="config-model-input"
153
+ :fetch-suggestions="(query: string) => modelSuggestions('family', query)"
154
+ :debounce="0"
155
+ :suffix-icon="ArrowDown"
156
+ fit-input-width
157
+ placeholder="选择或输入模型系列"
158
+ @focus="modelSearchActive.family = false"
159
+ @input="modelSearchActive.family = true"
160
+ />
161
+ </el-form-item>
170
162
  <el-form-item v-if="modelTestStatus.midscene" label="测试结果">
171
163
  <el-input :model-value="modelTestStatus.midscene" readonly />
172
164
  </el-form-item>
@@ -178,12 +170,13 @@ const openModelConfigGuide = () => {
178
170
  <span>脚本优化模型</span>
179
171
  <el-button
180
172
  :loading="testingModelKey === 'scriptOptimizer'"
173
+ :disabled="!!testingModelKey"
181
174
  @click="$emit('testModel', 'scriptOptimizer')"
182
175
  >
183
- 测试模型
176
+ 测试并保存
184
177
  </el-button>
185
178
  </div>
186
- <el-form label-position="top">
179
+ <el-form label-position="top" :disabled="!!testingModelKey">
187
180
  <el-form-item label="Base URL">
188
181
  <el-input v-model="configForm.scriptOptimizer.model.baseUrl" />
189
182
  </el-form-item>
@@ -202,3 +195,8 @@ const openModelConfigGuide = () => {
202
195
 
203
196
  </el-card>
204
197
  </template>
198
+
199
+ <style scoped>
200
+ .config-midscene-card :deep(.el-card__body) { background: var(--ui-bg-soft, #f8f9fb); }
201
+ .config-model-input { width: 100%; }
202
+ </style>
@@ -1,5 +1,5 @@
1
- export type MidsceneModelProvider = 'custom' | 'codex';
2
- export type MidsceneModelPresetKey = 'gpt' | 'doubao';
1
+ export type MidsceneModelPresetKey = 'gpt' | 'doubao' | 'qwen' | 'deepseek' | 'gemini' | 'kimi' | 'mimo' | 'glm' | 'glm-global' | 'autoglm' | 'autoglm-global' | 'ui-tars';
2
+ export type MidsceneModelProvider = 'custom' | 'codex' | MidsceneModelPresetKey;
3
3
 
4
4
  export type MidsceneModelPreset = {
5
5
  key: MidsceneModelPresetKey;
@@ -7,6 +7,8 @@ export type MidsceneModelPreset = {
7
7
  baseUrl: string;
8
8
  modelName: string;
9
9
  modelFamily: string;
10
+ modelFamilies?: string[];
11
+ hint?: string;
10
12
  };
11
13
 
12
14
  export type MidsceneModelOption = {
@@ -33,8 +35,11 @@ export const codexMidsceneModelOptions: MidsceneModelOption[] = [
33
35
 
34
36
  export const midsceneModelOptions: MidsceneModelOption[] = [
35
37
  ...codexMidsceneModelOptions,
38
+ { label: 'GPT-6 Sol', value: 'gpt-6-sol', family: 'gpt-6' },
39
+ { label: 'DeepSeek Flash', value: 'deepseek-flash', family: 'deepseek' },
40
+ { label: 'AutoGLM Phone', value: 'autoglm-phone', family: 'auto-glm' },
36
41
  { 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' },
42
+ { label: 'Doubao Seed 2.1 Turbo', value: 'doubao-seed-2-1-turbo-260628', family: 'doubao-seed' },
38
43
  { label: 'Doubao Seed 2.0 Lite', value: 'doubao-seed-2.0-lite', family: 'doubao-seed' },
39
44
  { label: 'Doubao Seed 1.6 Vision', value: 'doubao-seed-1.6-vision', family: 'doubao-seed' },
40
45
  { label: 'Doubao Seed 1.8', value: 'doubao-seed-1.8', family: 'doubao-seed' },
@@ -48,28 +53,51 @@ export const midsceneModelOptions: MidsceneModelOption[] = [
48
53
  { label: 'Kimi K3', value: 'kimi-k3', family: 'kimi3' },
49
54
  { label: 'Kimi K2.5', value: 'kimi-k2.5', family: 'kimi' },
50
55
  { label: 'Kimi K2.6', value: 'kimi-k2.6', family: 'kimi' },
56
+ { label: 'MiMo V2.6 Pro', value: 'mimo-v2.6-pro', family: 'xiaomi-mimo' },
51
57
  { label: 'MiMo V2.5', value: 'mimo-v2.5', family: 'xiaomi-mimo' },
52
58
  { label: 'GLM 5V Turbo', value: 'glm-5v-turbo', family: 'glm-v' },
53
59
  { label: 'GLM 4.6V', value: 'glm-4.6v', family: 'glm-v' },
54
60
  ];
55
61
 
56
62
  export const midsceneModelFamilyOptions = Array.from(
57
- new Set(midsceneModelOptions.map((option) => option.family)),
63
+ new Set([...midsceneModelOptions.map((option) => option.family), 'auto-glm-multilingual', 'vlm-ui-tars', 'vlm-ui-tars-doubao', 'vlm-ui-tars-doubao-1.5']),
58
64
  ).map((family) => ({ label: family, value: family }));
59
65
 
66
+ // 官方配置示例:https://midscenejs.com/zh/model-common-config.html(2026-09-23)。
60
67
  export const midsceneModelPresets: MidsceneModelPreset[] = [
61
68
  {
62
69
  key: 'gpt',
63
- label: 'GPT',
70
+ label: 'OpenAI GPT',
64
71
  baseUrl: 'https://api.openai.com/v1',
65
- modelName: 'gpt-5.5',
66
- modelFamily: 'gpt-5',
72
+ modelName: 'gpt-6-sol',
73
+ modelFamily: 'gpt-6',
74
+ modelFamilies: ['gpt-5', 'gpt-6'],
67
75
  },
68
76
  {
69
77
  key: 'doubao',
70
- label: 'Doubao',
78
+ label: '火山引擎 · 豆包 Seed',
71
79
  baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
72
- modelName: 'doubao-seed-2-1-pro-260628',
80
+ modelName: 'doubao-seed-2-1-turbo-260628',
73
81
  modelFamily: 'doubao-seed',
74
82
  },
83
+ { key: 'qwen', label: '阿里云 · 千问 Qwen', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', modelName: 'qwen3.7-plus', modelFamily: 'qwen3', modelFamilies: ['qwen3', 'qwen3-vl', 'qwen2.5-vl'] },
84
+ { key: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com', modelName: 'deepseek-flash', modelFamily: 'deepseek' },
85
+ { key: 'gemini', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/', modelName: 'gemini-3.5-flash', modelFamily: 'gemini' },
86
+ { key: 'kimi', label: '月之暗面 · Kimi', baseUrl: 'https://api.moonshot.cn/v1', modelName: 'kimi-k3', modelFamily: 'kimi3', modelFamilies: ['kimi3', 'kimi'] },
87
+ { key: 'mimo', label: '小米 · MiMo', baseUrl: 'https://api.xiaomimimo.com/v1', modelName: 'mimo-v2.6-pro', modelFamily: 'xiaomi-mimo' },
88
+ { key: 'glm', label: '智谱 BigModel · GLM-V(国内)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', modelName: 'glm-5v-turbo', modelFamily: 'glm-v' },
89
+ { key: 'glm-global', label: '智谱 Z.AI · GLM-V(国际)', baseUrl: 'https://api.z.ai/api/paas/v4', modelName: 'glm-5v-turbo', modelFamily: 'glm-v' },
90
+ { key: 'autoglm', label: '智谱 BigModel · AutoGLM(国内)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', modelName: 'autoglm-phone', modelFamily: 'auto-glm', modelFamilies: ['auto-glm', 'auto-glm-multilingual'], hint: '模型名称以平台实际名称为准;aiAssert / aiQuery 等页面理解任务需另外配置 Insight 模型。' },
91
+ { key: 'autoglm-global', label: '智谱 Z.AI · AutoGLM(国际)', baseUrl: 'https://api.z.ai/api/paas/v4', modelName: 'autoglm-phone', modelFamily: 'auto-glm', modelFamilies: ['auto-glm', 'auto-glm-multilingual'], hint: '模型名称以平台实际名称为准;aiAssert / aiQuery 等页面理解任务需另外配置 Insight 模型。' },
92
+ { key: 'ui-tars', label: '火山引擎 · UI-TARS', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', modelName: '', modelFamily: 'vlm-ui-tars-doubao-1.5', modelFamilies: ['vlm-ui-tars', 'vlm-ui-tars-doubao', 'vlm-ui-tars-doubao-1.5'], hint: '请在 Model Name 中填写已部署 UI-TARS 的推理接入点 ID(ep-…)或实际模型名称。' },
75
93
  ];
94
+
95
+ type MidsceneModelConfig = { provider: MidsceneModelProvider; baseUrl: string; apiKey: string; name: string; family: string };
96
+
97
+ export function selectMidsceneModelProvider(provider: MidsceneModelProvider, current: MidsceneModelConfig, custom: Omit<MidsceneModelConfig, 'provider'>): MidsceneModelConfig {
98
+ if (provider === 'codex') return { ...codexMidsceneModel };
99
+ if (provider === 'custom') return { ...custom, provider };
100
+ const preset = midsceneModelPresets.find(item => item.key === provider);
101
+ if (!preset) return current;
102
+ return { ...current, provider, baseUrl: preset.baseUrl, name: preset.modelName, family: preset.modelFamily };
103
+ }
@@ -0,0 +1,54 @@
1
+ import { onScopeDispose, shallowRef, watch } from 'vue';
2
+ import type { ConfigForm } from '../types';
3
+ import { isHexColor } from '../appium-recorder/flow-appearance';
4
+ import { resolveAiDeduplication } from '../appium-recorder/ai-deduplication';
5
+
6
+ // 串行提交最新快照,较慢的旧请求不会在新请求后覆盖配置。
7
+ export function useAppiumAutoSave(config: ConfigForm, save: (value: Omit<ConfigForm['appium'], 'model'>) => Promise<unknown>, onError: (error: unknown) => void) {
8
+ const saving = shallowRef(false);
9
+ const status = shallowRef('');
10
+ let initialized = false, saved = '', pending: string | undefined;
11
+ let timer: ReturnType<typeof setTimeout> | undefined;
12
+ // 模型只能通过“测试并保存”提交,自动保存仅观察其余设置。
13
+ function snapshotSettings() { const { model: _model, ...settings } = config.appium; return JSON.stringify(settings); }
14
+ function initialize() { saved = snapshotSettings(); initialized = true; }
15
+ async function flush() {
16
+ if (saving.value || pending === undefined) return;
17
+ saving.value = true;
18
+ try {
19
+ while (pending !== undefined) {
20
+ const snapshot = pending;
21
+ pending = undefined;
22
+ if (snapshot === saved) { status.value = '已自动保存'; continue; }
23
+ status.value = '正在自动保存…';
24
+ try {
25
+ await save(JSON.parse(snapshot));
26
+ saved = snapshot;
27
+ if (snapshotSettings() === snapshot) status.value = '已自动保存';
28
+ } catch (error) {
29
+ status.value = '自动保存失败,请重新修改后重试';
30
+ onError(error);
31
+ // 不无限重试失败请求;若用户期间有新修改,继续保存最新快照。
32
+ }
33
+ }
34
+ } finally { saving.value = false; }
35
+ }
36
+ watch(snapshotSettings, snapshot => {
37
+ if (!initialized) return;
38
+ clearTimeout(timer);
39
+ pending = undefined;
40
+ const value = config.appium;
41
+ if ((value.flowBackgroundColor !== undefined && !isHexColor(value.flowBackgroundColor))
42
+ || (value.flowLineColor !== undefined && !isHexColor(value.flowLineColor))) {
43
+ status.value = '颜色格式有误,尚未保存';
44
+ return;
45
+ }
46
+ try { resolveAiDeduplication(value.aiDeduplication); }
47
+ catch { status.value = '去重参数有误,尚未保存'; return; }
48
+ pending = snapshot;
49
+ status.value = '等待自动保存…';
50
+ timer = setTimeout(() => { void flush(); }, 350);
51
+ }, { flush: 'sync' });
52
+ onScopeDispose(() => { clearTimeout(timer); pending = undefined; });
53
+ return { initialize, saving, status };
54
+ }
@@ -13,6 +13,7 @@ defineProps<{
13
13
  testingModelKey: string;
14
14
  isSavingModelConfig: boolean;
15
15
  isSavingAppPreset: boolean;
16
+ appiumSaveStatus?: string;
16
17
  modelTestStatus: { midscene: string; scriptOptimizer: string; appium?: string };
17
18
  }>();
18
19
 
@@ -22,6 +23,7 @@ defineEmits<{
22
23
  saveAppPreset: [];
23
24
  editAppPreset: [app: AppPreset];
24
25
  deleteAppPreset: [id: string];
26
+ cancelAppPresetEdit: [];
25
27
  updateMidsceneModelProvider: [provider: MidsceneModelProvider];
26
28
  }>();
27
29
 
@@ -81,6 +83,7 @@ function handleTabKeydown(event: KeyboardEvent, index: number) {
81
83
  @save-app-preset="$emit('saveAppPreset')"
82
84
  @edit-app-preset="$emit('editAppPreset', $event)"
83
85
  @delete-app-preset="$emit('deleteAppPreset', $event)"
86
+ @cancel-app-preset-edit="$emit('cancelAppPresetEdit')"
84
87
  />
85
88
  </section>
86
89
 
@@ -93,10 +96,8 @@ function handleTabKeydown(event: KeyboardEvent, index: number) {
93
96
  <MidsceneConfigPanel
94
97
  :config-form="configForm"
95
98
  :testing-model-key="testingModelKey"
96
- :is-saving-model-config="isSavingModelConfig"
97
99
  :model-test-status="modelTestStatus"
98
100
  @test-model="$emit('testModel', $event)"
99
- @save-model-config="$emit('saveModelConfig')"
100
101
  @update-midscene-model-provider="$emit('updateMidsceneModelProvider', $event)"
101
102
  />
102
103
  </section>
@@ -110,10 +111,9 @@ function handleTabKeydown(event: KeyboardEvent, index: number) {
110
111
  <AppiumConfigPanel
111
112
  :config-form="configForm"
112
113
  :testing-model-key="testingModelKey"
113
- :is-saving-model-config="isSavingModelConfig"
114
114
  :test-status="modelTestStatus.appium"
115
+ :save-status="appiumSaveStatus"
115
116
  @test-model="$emit('testModel', 'appium')"
116
- @save-model-config="$emit('saveModelConfig')"
117
117
  />
118
118
  </section>
119
119
  </div>
package/src/style.css CHANGED
@@ -1684,16 +1684,11 @@ select {
1684
1684
  gap: 8px;
1685
1685
  }
1686
1686
 
1687
- .config-field-label {
1688
- display: inline-flex;
1689
- align-items: center;
1690
- gap: 4px;
1691
- }
1692
-
1693
- .config-field-help.el-button {
1694
- height: 18px;
1687
+ .config-model-help.el-button.is-text {
1688
+ width: 28px;
1689
+ height: 28px;
1695
1690
  padding: 0;
1696
- color: var(--el-text-color-secondary);
1691
+ font-size: 16px;
1697
1692
  }
1698
1693
 
1699
1694
  .config-module-card {
@@ -2869,7 +2864,7 @@ select {
2869
2864
 
2870
2865
  .appium-vue-flow .vue-flow__edge-path,
2871
2866
  .appium-vue-flow .vue-flow__connection-path {
2872
- stroke: #719985;
2867
+ stroke: var(--appium-flow-line-color, #719985);
2873
2868
  stroke-width: 2.5;
2874
2869
  stroke-linecap: round;
2875
2870
  stroke-linejoin: round;
package/src/types.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import type { AiPromptPreset } from './appium-recorder/ai-prompt-presets';
1
2
  import type { ScriptStep } from './script-generator';
2
3
  import type { MidsceneModelProvider } from './config/midscene-model-presets';
3
4
  import type { AiRecognitionModel } from './appium-recorder/ai-recognition';
5
+ import type { AiDeduplicationConfig } from './appium-recorder/ai-deduplication';
4
6
 
5
7
  export type MenuKey = 'generator' | 'automation' | 'config' | 'appium';
6
8
  export type GeneratorMode = 'ai' | 'manual';
@@ -12,7 +14,7 @@ export type GeneratorForm = {
12
14
  };
13
15
 
14
16
  export type ConfigForm = {
15
- appium: { model: AiRecognitionModel; flowBackgroundColor?: string };
17
+ appium: { model: AiRecognitionModel; aiPromptPresets?: AiPromptPreset[]; aiDeduplication?: AiDeduplicationConfig; flowBackgroundColor?: string; flowLineColor?: string; screenshotReport?: boolean };
16
18
  runtime: {
17
19
  androidSdkPath: string;
18
20
  reportOutputPath: string;
@@ -334,14 +334,27 @@ body {
334
334
  }
335
335
 
336
336
  .config-model-grid {
337
- gap: 20px;
337
+ gap: 32px;
338
338
  }
339
339
 
340
340
  .config-model-grid > section {
341
341
  min-width: 0;
342
- padding: 16px;
343
- border: 1px solid var(--ui-border);
344
- border-radius: var(--ui-radius-control);
342
+ padding: 0;
343
+ border: 0;
344
+ border-radius: 0;
345
+ }
346
+
347
+ .config-model-grid > section + section {
348
+ padding-left: 32px;
349
+ border-left: 1px solid var(--ui-border);
350
+ }
351
+
352
+ @media (max-width: 640px) {
353
+ .config-model-grid > section + section {
354
+ padding: 24px 0 0;
355
+ border-left: 0;
356
+ border-top: 1px solid var(--ui-border);
357
+ }
345
358
  }
346
359
 
347
360
  .config-model-grid > section,