android-midscene-automation 0.1.43 → 0.2.2

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 (56) hide show
  1. package/{AI_RECOGNITION_GUIDE.md → AI/350/257/206/345/210/253/346/223/215/344/275/234/350/257/264/346/230/216.md} +78 -6
  2. package/CHANGELOG.md +13 -0
  3. package/FAQ.md +8 -8
  4. package/README.md +143 -17
  5. package/USAGE.md +120 -8
  6. package/package.json +3 -2
  7. package/server/analytics.test.ts +46 -0
  8. package/server/analytics.ts +24 -0
  9. package/server/api-safety-analytics.test.ts +57 -0
  10. package/server/appium-auto-save.test.ts +6 -0
  11. package/server/appium-recorder/ai-recognition.test.ts +27 -0
  12. package/server/appium-recorder/ai-recognition.ts +16 -4
  13. package/server/appium-recorder/appium-runner.ts +11 -1
  14. package/server/appium-recorder/appium-version.test.ts +55 -0
  15. package/server/appium-recorder/managed-appium.ts +37 -1
  16. package/server/appium-recorder/repository.ts +6 -2
  17. package/server/appium-recorder/routes.ts +2 -12
  18. package/server/appium-recorder/run-history.ts +11 -0
  19. package/server/config-access.test.ts +27 -0
  20. package/server/config-access.ts +27 -0
  21. package/server/http-api.ts +63 -54
  22. package/server/operations/repository.ts +8 -0
  23. package/server/remote-agents/routes.ts +2 -12
  24. package/server/request-body.test.ts +40 -0
  25. package/server/request-body.ts +48 -0
  26. package/src/App.vue +98 -742
  27. package/src/analytics/summary.ts +75 -0
  28. package/src/api.ts +11 -0
  29. package/src/appium-recorder/AppiumPage.vue +4 -1
  30. package/src/appium-recorder/ai-recognition.ts +7 -0
  31. package/src/appium-recorder/components/AiRecognitionSettings.vue +27 -0
  32. package/src/appium-recorder/flow-labels.ts +1 -1
  33. package/src/appium-recorder/types.ts +2 -1
  34. package/src/components/AppSidebar.vue +31 -0
  35. package/src/components/analytics/AnalysisCharts.vue +189 -0
  36. package/src/components/analytics/ReportAnalysis.vue +78 -0
  37. package/src/components/config/AiPromptPresetsSettings.vue +55 -5
  38. package/src/components/config/AppiumConfigPanel.vue +4 -1
  39. package/src/components/config/AppiumModelSettings.vue +1 -1
  40. package/src/components/config/BaseConfigPanel.vue +6 -2
  41. package/src/components/config/MidsceneConfigPanel.vue +4 -5
  42. package/src/components/help/HelpCenter.vue +185 -0
  43. package/src/components/help/MarkdownDocument.vue +139 -0
  44. package/src/composables/use-device-preview.ts +279 -0
  45. package/src/composables/use-initial-resource.ts +17 -0
  46. package/src/composables/use-script-execution.ts +277 -0
  47. package/src/config/credentials.ts +2 -0
  48. package/src/config/use-appium-auto-save.ts +22 -11
  49. package/src/config/use-model-config.ts +157 -0
  50. package/src/main.ts +2 -2
  51. package/src/pages/AnalysisPage.vue +60 -0
  52. package/src/pages/ConfigPage.vue +36 -5
  53. package/src/style.css +716 -91
  54. package/src/types.ts +7 -1
  55. package//346/265/201/347/250/213/350/212/202/347/202/271/346/223/215/344/275/234/350/257/264/346/230/216.md +408 -0
  56. package/src/ui-refresh.css +0 -677
@@ -0,0 +1,157 @@
1
+ import { reactive, ref, shallowRef } from 'vue';
2
+ import { ElMessage, ElMessageBox } from 'element-plus';
3
+ import { resolveReportSummary } from '../appium-recorder/report-summary';
4
+ import { isAiRecognitionModelConfigured } from '../appium-recorder/ai-recognition';
5
+ import { resolveAiDeduplication } from '../appium-recorder/ai-deduplication';
6
+ import { resolveAiPromptPresets } from '../appium-recorder/ai-prompt-presets';
7
+ import { normalizeFlowBackground, normalizeFlowLineColor } from '../appium-recorder/flow-appearance';
8
+ import { selectMidsceneModelProvider, type MidsceneModelProvider } from './midscene-model-presets';
9
+ import { useAppiumAutoSave } from './use-appium-auto-save';
10
+ import type { ConfigForm } from '../types';
11
+ import * as api from '../api';
12
+
13
+ export function useModelConfig() {
14
+ const isSavingModelConfig = ref(false);
15
+ const testingModelKey = ref('');
16
+ const modelTestStatus = reactive({
17
+ midscene: '',
18
+ scriptOptimizer: '',
19
+ appium: '',
20
+ promptOptimizer: '',
21
+ });
22
+ const aiRecognitionModelConfigured = shallowRef(false);
23
+ const ready = shallowRef(false);
24
+ const configForm = reactive<ConfigForm>({
25
+ appium: { reportSummary: resolveReportSummary(), model: { baseUrl: '', apiKey: '', name: '' }, promptOptimizer: { model: { baseUrl: '', apiKey: '', name: '' } }, aiDeduplication: resolveAiDeduplication(), screenshotReport: false },
26
+ runtime: {
27
+ androidSdkPath: '',
28
+ reportOutputPath: '',
29
+ },
30
+ midscene: {
31
+ model: {
32
+ provider: 'custom',
33
+ baseUrl: '',
34
+ apiKey: '',
35
+ name: '',
36
+ family: '',
37
+ },
38
+ env: {},
39
+ },
40
+ scriptOptimizer: {
41
+ model: {
42
+ baseUrl: '',
43
+ apiKey: '',
44
+ name: '',
45
+ },
46
+ },
47
+ });
48
+ const lastCustomMidsceneModel = reactive({
49
+ baseUrl: '',
50
+ apiKey: '',
51
+ name: '',
52
+ family: '',
53
+ });
54
+
55
+ const appiumAutoSave = useAppiumAutoSave(configForm, async value => {
56
+ await api.saveAppiumConfig(value);
57
+ }, error => {
58
+ ElMessage.error(error instanceof Error ? error.message : 'Appium 配置自动保存失败');
59
+ });
60
+
61
+ const loadConfig = async () => {
62
+ if (ready.value) return;
63
+ const payload = await api.getConfig();
64
+ Object.assign(configForm.runtime, payload.runtime || {});
65
+ Object.assign(configForm.midscene.model, payload.midscene.model);
66
+ Object.keys(configForm.midscene.env).forEach((key) => {
67
+ delete configForm.midscene.env[key];
68
+ });
69
+ Object.assign(configForm.midscene.env, payload.midscene.env || {});
70
+ Object.assign(configForm.scriptOptimizer.model, payload.scriptOptimizer.model);
71
+ Object.assign(configForm.appium.model, payload.appium?.model || { baseUrl: '', apiKey: '', name: '' });
72
+ Object.assign(configForm.appium.promptOptimizer!.model, payload.appium?.promptOptimizer?.model || { baseUrl: '', apiKey: '', name: '' });
73
+ configForm.appium.screenshotReport = payload.appium?.screenshotReport === true;
74
+ configForm.appium.reportSummary = resolveReportSummary(payload.appium?.reportSummary);
75
+ configForm.appium.aiDeduplication = resolveAiDeduplication(payload.appium?.aiDeduplication);
76
+ configForm.appium.aiPromptPresets = resolveAiPromptPresets(payload.appium?.aiPromptPresets);
77
+ configForm.appium.flowBackgroundColor = normalizeFlowBackground(payload.appium?.flowBackgroundColor);
78
+ configForm.appium.flowLineColor = normalizeFlowLineColor(payload.appium?.flowLineColor);
79
+ appiumAutoSave.initialize();
80
+ ready.value = true;
81
+ aiRecognitionModelConfigured.value = isAiRecognitionModelConfigured(configForm.appium.model);
82
+ if (configForm.midscene.model.provider !== 'codex') {
83
+ rememberCustomMidsceneModel();
84
+ }
85
+ };
86
+
87
+ const rememberCustomMidsceneModel = () => {
88
+ if (configForm.midscene.model.provider === 'codex') return;
89
+ lastCustomMidsceneModel.baseUrl = configForm.midscene.model.baseUrl;
90
+ lastCustomMidsceneModel.apiKey = configForm.midscene.model.apiKey;
91
+ lastCustomMidsceneModel.name = configForm.midscene.model.name;
92
+ lastCustomMidsceneModel.family = configForm.midscene.model.family;
93
+ };
94
+
95
+ const updateMidsceneModelProvider = (provider: MidsceneModelProvider) => {
96
+ if (configForm.midscene.model.provider !== 'codex') {
97
+ rememberCustomMidsceneModel();
98
+ }
99
+ Object.assign(configForm.midscene.model, selectMidsceneModelProvider(provider, configForm.midscene.model, lastCustomMidsceneModel));
100
+ modelTestStatus.midscene = '';
101
+ };
102
+
103
+ const getMidsceneModelConfigError = () => {
104
+ const model = configForm.midscene.model;
105
+ const missing = [];
106
+
107
+ if (!model.baseUrl.trim()) missing.push('Base URL');
108
+ if (!model.name.trim()) missing.push('Model Name');
109
+ if (!model.family.trim()) missing.push('Model Family');
110
+ if (model.provider !== 'codex' && !model.apiKey.trim()) missing.push('API Key');
111
+
112
+ if (!missing.length) return '';
113
+ return `Midscene 模型配置不完整:缺少 ${missing.join('、')}。请在“参数配置 > Midscene配置 > Midscene 模型”中选择“使用 Codex”,或补齐自定义提供方后测试并保存。`;
114
+ };
115
+
116
+ const saveModelConfig = async () => {
117
+ if (!ready.value || isSavingModelConfig.value) return;
118
+ isSavingModelConfig.value = true;
119
+ try {
120
+ await api.saveConfig({ runtime: { ...configForm.runtime } });
121
+ ElMessage.success('参数配置已保存');
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : '参数配置保存失败';
124
+ ElMessage.error(message);
125
+ } finally {
126
+ isSavingModelConfig.value = false;
127
+ }
128
+ };
129
+
130
+ const testModel = async (key: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer') => {
131
+ if (!ready.value || testingModelKey.value) return;
132
+ testingModelKey.value = key;
133
+ modelTestStatus[key] = '';
134
+ const model = { ...(key === 'promptOptimizer'
135
+ ? configForm.appium.promptOptimizer!.model
136
+ : configForm[key].model) };
137
+
138
+ try {
139
+ const payload = await api.testModel({ modelKey: key, model, save: true });
140
+ if (!payload.saved) throw new Error('模型未保存,请重试');
141
+ const successMessage = `测试通过,配置已保存${payload.content ? `:${payload.content}` : ''}`;
142
+ modelTestStatus[key] = '';
143
+ if (key === 'appium') aiRecognitionModelConfigured.value = isAiRecognitionModelConfigured(model);
144
+ void ElMessageBox.alert(successMessage, '模型测试成功', {
145
+ type: 'success',
146
+ confirmButtonText: '确定',
147
+ }).catch(() => {});
148
+ } catch (error) {
149
+ modelTestStatus[key] = `未保存:${error instanceof Error ? error.message : '测试失败'}`;
150
+ } finally {
151
+ testingModelKey.value = '';
152
+ }
153
+ };
154
+
155
+
156
+ return { ready, configForm, appiumAutoSave, isSavingModelConfig, testingModelKey, modelTestStatus, aiRecognitionModelConfigured, loadConfig, updateMidsceneModelProvider, getMidsceneModelConfigError, saveModelConfig, testModel };
157
+ }
package/src/main.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createApp } from 'vue';
2
2
  import ElementPlus from 'element-plus';
3
+ import zhCn from 'element-plus/es/locale/lang/zh-cn';
3
4
  import 'element-plus/dist/index.css';
4
5
  import '@vue-flow/core/dist/style.css';
5
6
  import '@vue-flow/core/dist/theme-default.css';
6
7
  import App from './App.vue';
7
8
  import './style.css';
8
- import './ui-refresh.css';
9
9
 
10
- createApp(App).use(ElementPlus).mount('#app');
10
+ createApp(App).use(ElementPlus, { locale: zhCn }).mount('#app');
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef, watch } from 'vue';
3
+ import { Refresh } from '@element-plus/icons-vue';
4
+ import { getTestAnalytics } from '../api';
5
+ import type { AnalysisResponse } from '../analytics/summary';
6
+ import AnalysisCharts from '../components/analytics/AnalysisCharts.vue';
7
+ import ReportAnalysis from '../components/analytics/ReportAnalysis.vue';
8
+ const props = defineProps<{ active: boolean }>();
9
+ const days = shallowRef(0);
10
+ const data = shallowRef<AnalysisResponse | null>(null);
11
+ const loading = shallowRef(false);
12
+ const error = shallowRef('');
13
+ let requestId = 0;
14
+ async function refresh() {
15
+ const id = ++requestId;
16
+ loading.value = true;
17
+ error.value = '';
18
+ try { const result = await getTestAnalytics(days.value); if (id === requestId) data.value = result; }
19
+ catch (reason) { if (id === requestId) error.value = reason instanceof Error ? reason.message : '统计数据加载失败'; }
20
+ finally { if (id === requestId) loading.value = false; }
21
+ }
22
+ watch(() => props.active, active => { if (active) void refresh(); }, { immediate: true });
23
+ watch(days, () => { void refresh(); });
24
+ </script>
25
+
26
+ <template>
27
+ <div class="analysis-page" :aria-busy="loading">
28
+ <div class="analysis-page__heading">
29
+ <div><h1>测试分析</h1><p>基于已保存脚本与测试执行记录,查看两种引擎的运行情况。</p></div>
30
+ <div class="analysis-page__actions">
31
+ <el-select v-model="days" aria-label="统计时间范围" style="width: 150px">
32
+ <el-option label="全部时间" :value="0" /><el-option label="最近 7 天" :value="7" /><el-option label="最近 30 天" :value="30" /><el-option label="最近 90 天" :value="90" />
33
+ </el-select>
34
+ <el-button :icon="Refresh" :loading="loading" @click="refresh">刷新统计</el-button>
35
+ </div>
36
+ </div>
37
+ <el-alert v-if="error" type="error" :closable="false" show-icon :title="error"><el-button link type="primary" @click="refresh">重新加载</el-button></el-alert>
38
+ <div v-if="loading && !data" class="page-loading" role="status">正在汇总测试记录…</div>
39
+ <template v-if="data">
40
+ <div class="analysis-page__note">
41
+ <span>{{ data.days ? `最近 ${data.days} 天` : '全部时间' }} · 通过率 = 通过 ÷(通过 + 失败);平均耗时仅统计已完成测试,停止和运行中单独列出。</span>
42
+ <span>更新于 {{ new Date(data.generatedAt).toLocaleString('zh-CN', { hour12: false }) }}{{ loading ? ' · 更新中…' : '' }}</span>
43
+ </div>
44
+ <AnalysisCharts :data="data" />
45
+ <ReportAnalysis title="Midscene" :summary="data.midscene" />
46
+ <ReportAnalysis title="Appium" :summary="data.appium" />
47
+ <p class="analysis-page__footnote">统计来自数据库中的执行记录,不依赖 HTML 报告开关。Midscene 按历史脚本名称关联,Appium 按脚本 ID 关联;已删除或无法匹配的记录标记为“历史脚本”。未保存执行记录的测试不计入统计。</p>
48
+ </template>
49
+ </div>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .analysis-page { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
54
+ .analysis-page__heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
55
+ h1 { margin: 0; font-size: 22px; font-weight: 600; }
56
+ .analysis-page__heading p { margin: 8px 0 0; color: var(--ui-text-secondary); font-size: 13px; }
57
+ .analysis-page__actions { display: flex; gap: 12px; }
58
+ .analysis-page__note { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--ui-text-secondary); }
59
+ .analysis-page__footnote { margin: 0; color: var(--ui-text-secondary); font-size: 12px; line-height: 1.8; }
60
+ </style>
@@ -14,12 +14,14 @@ defineProps<{
14
14
  isSavingModelConfig: boolean;
15
15
  isSavingAppPreset: boolean;
16
16
  appiumSaveStatus?: string;
17
+ appiumSaveFailed?: boolean;
17
18
  modelTestStatus: { midscene: string; scriptOptimizer: string; appium?: string; promptOptimizer?: string };
18
19
  }>();
19
20
 
20
21
  defineEmits<{
21
22
  testModel: [key: 'midscene' | 'scriptOptimizer' | 'appium' | 'promptOptimizer'];
22
23
  saveModelConfig: [];
24
+ retryAppiumSave: [];
23
25
  saveAppPreset: [];
24
26
  editAppPreset: [app: AppPreset];
25
27
  deleteAppPreset: [id: string];
@@ -115,6 +117,8 @@ function handleTabKeydown(event: KeyboardEvent, index: number) {
115
117
  :prompt-optimizer-test-status="modelTestStatus.promptOptimizer"
116
118
  @test-prompt-optimizer-model="$emit('testModel', 'promptOptimizer')"
117
119
  :save-status="appiumSaveStatus"
120
+ :save-failed="appiumSaveFailed"
121
+ @retry-save="$emit('retryAppiumSave')"
118
122
  @test-model="$emit('testModel', 'appium')"
119
123
  />
120
124
  </section>
@@ -123,13 +127,40 @@ function handleTabKeydown(event: KeyboardEvent, index: number) {
123
127
 
124
128
  <style scoped>
125
129
  .config-tabs {
130
+ display: flex;
131
+ width: 100%;
126
132
  max-width: 100%;
127
133
  flex-wrap: wrap;
134
+ gap: 28px;
135
+ margin: 0;
136
+ padding: 0 28px;
137
+ border: 0;
138
+ border-bottom: 1px solid var(--ui-border);
139
+ border-radius: 0;
140
+ background: var(--ui-surface);
128
141
  }
129
-
130
- @media (max-width: 640px) {
131
- :deep(.config-model-grid) {
132
- grid-template-columns: minmax(0, 1fr);
133
- }
142
+ .config-tabs .subnav__item {
143
+ min-height: 46px;
144
+ padding: 10px 2px;
145
+ border-bottom: 2px solid transparent;
146
+ border-radius: 0;
147
+ font-size: 14px;
148
+ }
149
+ .config-tabs .subnav__item--active {
150
+ border-bottom-color: var(--ui-primary);
151
+ background: transparent;
152
+ color: var(--ui-primary);
153
+ box-shadow: none;
154
+ }
155
+ .config-page > [role="tabpanel"] { padding: 24px; }
156
+ :deep(.config-module-card > .el-card__header) { padding: 18px 24px; font-size: 16px; font-weight: 500; }
157
+ :deep(.config-module-card > .el-card__body) { padding: 24px; background: var(--ui-surface); }
158
+ :deep(.el-form-item) { margin-bottom: 22px; }
159
+ :deep(.el-form-item__label) { margin-bottom: 8px; font-size: 14px; }
160
+ :deep(.el-input__wrapper), :deep(.el-select__wrapper) { min-height: 36px; }
161
+ :deep(.config-grid) { margin-bottom: 0; gap: 24px; }
162
+ @media (max-width: 1000px) {
163
+ :deep(.config-model-grid) { grid-template-columns: minmax(0, 1fr); }
164
+ :deep(.config-model-grid > section + section) { padding: 24px 0 0; border-left: 0; border-top: 1px solid var(--ui-border); }
134
165
  }
135
166
  </style>