android-midscene-automation 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_RECOGNITION_GUIDE.md +370 -0
- package/CHANGELOG.md +16 -0
- package/README.md +35 -3
- package/USAGE.md +18 -3
- package/package.json +2 -1
- package/remote-agent/index.ts +1 -0
- package/server/appium-auto-save.test.ts +61 -0
- package/server/appium-config.test.ts +165 -0
- package/server/appium-recorder/ai-deduplication.test.ts +80 -0
- package/server/appium-recorder/ai-deduplication.ts +74 -0
- package/server/appium-recorder/ai-observation.test.ts +125 -0
- package/server/appium-recorder/ai-observation.ts +125 -0
- package/server/appium-recorder/ai-recognition.ts +118 -22
- package/server/appium-recorder/appium-runner.ts +79 -14
- package/server/appium-recorder/bounded-loop.test.ts +55 -0
- package/server/appium-recorder/bounded-loop.ts +12 -1
- package/server/appium-recorder/history-capture.ts +1 -0
- package/server/appium-recorder/image-check.ts +1 -1
- package/server/appium-recorder/managed-appium.ts +15 -0
- package/server/appium-recorder/managed-cleanup.test.ts +82 -0
- package/server/appium-recorder/prompt-optimizer.ts +57 -0
- package/server/appium-recorder/replay-log.ts +13 -0
- package/server/appium-recorder/report-summary.test.ts +78 -0
- package/server/appium-recorder/report-summary.ts +60 -0
- package/server/appium-recorder/report.ts +45 -14
- package/server/appium-recorder/repository.ts +7 -0
- package/server/appium-recorder/routes.ts +78 -4
- package/server/appium-recorder/screenshot.ts +1 -0
- package/server/config.ts +34 -4
- package/server/http-api.ts +31 -5
- package/server/model-tester.ts +2 -1
- package/src/App.vue +53 -30
- package/src/api.ts +11 -4
- package/src/appium-recorder/AppiumPage.vue +167 -68
- package/src/appium-recorder/RunHistoryDialog.vue +1 -1
- package/src/appium-recorder/action-descriptions.ts +2 -1
- package/src/appium-recorder/ai-deduplication.ts +34 -0
- package/src/appium-recorder/ai-prompt-presets.ts +34 -0
- package/src/appium-recorder/ai-recognition.ts +27 -2
- package/src/appium-recorder/api.ts +36 -2
- package/src/appium-recorder/bounded-loop.ts +9 -1
- package/src/appium-recorder/components/AiRecognitionPromptInput.vue +41 -0
- package/src/appium-recorder/components/AiRecognitionSettings.vue +167 -0
- package/src/appium-recorder/components/AiRecognitionTestDialog.vue +86 -0
- package/src/appium-recorder/components/BreakLoopSettings.vue +19 -3
- package/src/appium-recorder/components/ComponentTree.vue +5 -5
- package/src/appium-recorder/components/CoordinatePointPicker.vue +23 -0
- package/src/appium-recorder/components/FlowActionMenu.vue +50 -20
- package/src/appium-recorder/components/FlowCanvas.vue +3 -2
- package/src/appium-recorder/components/FlowNodeCard.vue +2 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +17 -3
- package/src/appium-recorder/components/NestedConditionBranches.vue +25 -50
- package/src/appium-recorder/components/RecordedSteps.vue +23 -6
- package/src/appium-recorder/coordinate-preview.ts +37 -0
- package/src/appium-recorder/flow-appearance.ts +12 -1
- package/src/appium-recorder/flow-copy.ts +1 -0
- package/src/appium-recorder/flow-graph.ts +7 -4
- package/src/appium-recorder/flow-labels.ts +9 -5
- package/src/appium-recorder/flow-merge.ts +1 -1
- package/src/appium-recorder/flow-unmerge.ts +2 -2
- package/src/appium-recorder/report-summary.ts +60 -0
- package/src/appium-recorder/types.ts +6 -0
- package/src/components/config/AiDeduplicationSettings.vue +60 -0
- package/src/components/config/AiPromptPresetsSettings.vue +63 -0
- package/src/components/config/AppiumConfigPanel.vue +76 -3
- package/src/components/config/AppiumModelSettings.vue +45 -0
- package/src/components/config/BaseConfigPanel.vue +20 -8
- package/src/components/config/FlowAppearanceSettings.vue +51 -32
- package/src/components/config/MidsceneConfigPanel.vue +84 -86
- package/src/components/config/ReplayReportSummarySettings.vue +118 -0
- package/src/components/device/DevicePreviewPanel.vue +18 -3
- package/src/config/midscene-model-presets.ts +37 -9
- package/src/config/use-appium-auto-save.ts +62 -0
- package/src/pages/ConfigPage.vue +8 -6
- package/src/style.css +5 -55
- package/src/types.ts +4 -1
- package/src/ui-refresh.css +17 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { Delete, Plus } from '@element-plus/icons-vue';
|
|
4
|
+
import { ElMessage } from 'element-plus';
|
|
5
|
+
import { resolveAiPromptPresets, type AiPromptPreset } from '../../appium-recorder/ai-prompt-presets';
|
|
6
|
+
import { validateAiRecognitionPrompt } from '../../appium-recorder/ai-recognition';
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ modelValue?: AiPromptPreset[] }>();
|
|
9
|
+
const emit = defineEmits<{ 'update:modelValue': [presets: AiPromptPreset[]] }>();
|
|
10
|
+
const presets = computed(() => resolveAiPromptPresets(props.modelValue));
|
|
11
|
+
const draft = ref('');
|
|
12
|
+
const sceneName = ref('');
|
|
13
|
+
|
|
14
|
+
function addPreset() {
|
|
15
|
+
try {
|
|
16
|
+
const prompt = validateAiRecognitionPrompt(draft.value);
|
|
17
|
+
const next = resolveAiPromptPresets([...presets.value, { name: sceneName.value, prompt }]);
|
|
18
|
+
emit('update:modelValue', next);
|
|
19
|
+
sceneName.value = '';
|
|
20
|
+
draft.value = '';
|
|
21
|
+
} catch (error) { ElMessage.warning(error instanceof Error ? error.message : '添加提示词失败'); }
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<section class="ai-prompt-settings" aria-labelledby="ai-prompt-presets-title">
|
|
27
|
+
<h3 id="ai-prompt-presets-title" class="ai-prompt-visually-hidden">AI 识别预设提示词</h3>
|
|
28
|
+
<p class="ai-prompt-description">添加常用测试场景,录制 AI 识别节点时可以直接选择。删除预设不会修改已有节点。</p>
|
|
29
|
+
<div class="ai-prompt-layout">
|
|
30
|
+
<el-form label-position="top" @submit.prevent="addPreset">
|
|
31
|
+
<el-form-item label="测试场景">
|
|
32
|
+
<el-input v-model="sceneName" maxlength="80" placeholder="例如:开屏广告检测" />
|
|
33
|
+
</el-form-item>
|
|
34
|
+
<el-form-item label="新增提示词">
|
|
35
|
+
<el-input v-model="draft" type="textarea" :rows="4" maxlength="4000" show-word-limit placeholder="输入常用的识别内容,例如:当前画面是否出现广告弹窗?" />
|
|
36
|
+
</el-form-item>
|
|
37
|
+
<div class="ai-prompt-actions"><el-button native-type="submit" :icon="Plus">添加提示词</el-button></div>
|
|
38
|
+
</el-form>
|
|
39
|
+
<div class="ai-prompt-list" aria-label="已添加的提示词">
|
|
40
|
+
<div v-for="(preset, index) in presets" :key="preset.name" class="ai-prompt-row">
|
|
41
|
+
<div class="ai-prompt-content"><strong>{{ preset.name }}</strong><p>{{ preset.prompt }}</p></div>
|
|
42
|
+
<el-button text :icon="Delete" :aria-label="`删除提示词 ${preset.name}`" @click="emit('update:modelValue', presets.filter((_, i) => i !== index))" />
|
|
43
|
+
</div>
|
|
44
|
+
<el-empty v-if="!presets.length" description="暂无预设提示词" :image-size="60" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
.ai-prompt-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
52
|
+
.ai-prompt-description { margin: 0 0 20px; color: var(--el-text-color-secondary); font-size: 12px; }
|
|
53
|
+
.ai-prompt-layout { display: grid; gap: 20px; }
|
|
54
|
+
.ai-prompt-actions { display: flex; justify-content: flex-end; }
|
|
55
|
+
.ai-prompt-list { --ai-prompt-row-height: 76px; min-width: 0; height: calc(var(--ai-prompt-row-height) * 3 + 1px); overflow-y: auto; scrollbar-gutter: stable; border-top: 1px solid var(--ui-border); }
|
|
56
|
+
.ai-prompt-row { display: flex; align-items: flex-start; gap: 12px; height: var(--ai-prompt-row-height); box-sizing: border-box; padding: 10px 0; }
|
|
57
|
+
.ai-prompt-row + .ai-prompt-row { border-top: 1px solid var(--ui-border); }
|
|
58
|
+
.ai-prompt-content { flex: 1; min-width: 0; font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
|
|
59
|
+
.ai-prompt-content strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
60
|
+
.ai-prompt-content p { display: -webkit-box; margin: 4px 0 0; overflow: hidden; color: var(--el-text-color-secondary); -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
|
61
|
+
.ai-prompt-row .el-button { flex-shrink: 0; }
|
|
62
|
+
.ai-prompt-list :deep(.el-empty) { height: 100%; padding: 0; }
|
|
63
|
+
</style>
|
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import ReplayReportSummarySettings from './ReplayReportSummarySettings.vue';
|
|
2
3
|
import type { ConfigForm } from '../../types';
|
|
3
4
|
import FlowAppearanceSettings from './FlowAppearanceSettings.vue';
|
|
5
|
+
import AiDeduplicationSettings from './AiDeduplicationSettings.vue';
|
|
6
|
+
import AiPromptPresetsSettings from './AiPromptPresetsSettings.vue';
|
|
7
|
+
import AppiumModelSettings from './AppiumModelSettings.vue';
|
|
4
8
|
|
|
5
9
|
defineProps<{
|
|
6
10
|
configForm: ConfigForm;
|
|
7
11
|
testingModelKey: string;
|
|
8
|
-
isSavingModelConfig: boolean;
|
|
9
12
|
testStatus?: string;
|
|
13
|
+
promptOptimizerTestStatus?: string;
|
|
14
|
+
saveStatus?: string;
|
|
10
15
|
}>();
|
|
11
|
-
defineEmits<{ testModel: [];
|
|
16
|
+
defineEmits<{ testModel: []; testPromptOptimizerModel: [] }>();
|
|
12
17
|
</script>
|
|
13
18
|
|
|
14
19
|
<template>
|
|
15
|
-
<
|
|
20
|
+
<div class="appium-config-stack">
|
|
21
|
+
<section class="appium-config-section" aria-labelledby="appium-models-title">
|
|
22
|
+
<header class="appium-section-heading">
|
|
23
|
+
<div><h2 id="appium-models-title">模型服务</h2><p>不同任务使用独立模型,配置修改需测试通过后保存。</p></div>
|
|
24
|
+
<span v-if="saveStatus" class="config-save-status" role="status">{{ saveStatus }}</span>
|
|
25
|
+
</header>
|
|
26
|
+
<div class="appium-model-grid">
|
|
27
|
+
<AppiumModelSettings v-model="configForm.appium.model" title="AI 识别模型" badge="视觉"
|
|
28
|
+
description="用于识别设备截图和持续观察,需要选择支持图片输入的模型。" name-placeholder="支持图片输入的模型"
|
|
29
|
+
model-key="appium" :testing-model-key="testingModelKey" :test-status="testStatus" @test="$emit('testModel')" />
|
|
30
|
+
<AppiumModelSettings v-if="configForm.appium.promptOptimizer" v-model="configForm.appium.promptOptimizer.model"
|
|
31
|
+
title="提示词优化模型" badge="文本" description="用于优化 AI 识别和回放报告提示词,也用于生成回放测试报告总结,只需支持文本输入。"
|
|
32
|
+
name-placeholder="用于优化提示词的文本模型" model-key="promptOptimizer" :testing-model-key="testingModelKey"
|
|
33
|
+
:test-status="promptOptimizerTestStatus" @test="$emit('testPromptOptimizerModel')" />
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<section class="appium-config-section" aria-labelledby="appium-recognition-title">
|
|
38
|
+
<header class="appium-section-heading"><div><h2 id="appium-recognition-title">AI 识别设置</h2><p>控制持续观察的截图筛选,并管理录制节点可选的常用提示词。</p></div></header>
|
|
39
|
+
<div class="appium-settings-grid">
|
|
40
|
+
<el-card shadow="never" class="config-module-card config-appium-card">
|
|
41
|
+
<template #header><div class="panel-header"><span>持续观察截图去重</span></div></template>
|
|
42
|
+
<el-form label-position="top"><AiDeduplicationSettings v-model="configForm.appium.aiDeduplication" /></el-form>
|
|
43
|
+
</el-card>
|
|
44
|
+
<el-card shadow="never" class="config-module-card config-appium-card">
|
|
45
|
+
<template #header><div class="panel-header"><span>AI 识别预设提示词</span></div></template>
|
|
46
|
+
<AiPromptPresetsSettings v-model="configForm.appium.aiPromptPresets" />
|
|
47
|
+
</el-card>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section class="appium-config-section" aria-labelledby="appium-replay-title">
|
|
52
|
+
<header class="appium-section-heading"><div><h2 id="appium-replay-title">回放与报告</h2><p>配置回放证据、基础报告和 AI 总结。</p></div></header>
|
|
53
|
+
<el-card shadow="never" class="config-module-card config-appium-card">
|
|
54
|
+
<template #header>
|
|
55
|
+
<div class="panel-header">
|
|
56
|
+
<span class="panel-header__title">
|
|
57
|
+
<span>回放截图</span>
|
|
58
|
+
<el-switch v-model="configForm.appium.screenshotReport" aria-label="截图与 HTML 报告" />
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
<el-alert title="开启后会在节点执行前后截图并生成 HTML 报告。截图会增加回放耗时,实际操作间隔可能超过设置的延时时间。" type="info" :closable="false" show-icon />
|
|
63
|
+
</el-card>
|
|
64
|
+
<ReplayReportSummarySettings v-if="configForm.appium.reportSummary" :config="configForm.appium.reportSummary" :testing-model-key="testingModelKey" />
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
<section class="appium-config-section" aria-labelledby="appium-editor-title">
|
|
68
|
+
<header class="appium-section-heading"><div><h2 id="appium-editor-title">流程编辑器</h2><p>调整流程画布和连接线的显示颜色。</p></div></header>
|
|
69
|
+
<FlowAppearanceSettings v-model="configForm.appium.flowBackgroundColor" v-model:line-color="configForm.appium.flowLineColor" />
|
|
70
|
+
</section>
|
|
71
|
+
</div>
|
|
16
72
|
</template>
|
|
73
|
+
|
|
74
|
+
<style scoped>
|
|
75
|
+
.config-save-status { font-size: 12px; font-weight: 400; color: var(--el-text-color-secondary); }
|
|
76
|
+
.appium-config-stack { display: grid; gap: 28px; }
|
|
77
|
+
.appium-config-section { display: grid; gap: 14px; min-width: 0; }
|
|
78
|
+
.appium-section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; padding: 0 4px; }
|
|
79
|
+
.appium-section-heading h2 { margin: 0; color: var(--el-text-color-primary); font-size: 16px; line-height: 1.4; }
|
|
80
|
+
.appium-section-heading p { margin: 4px 0 0; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.5; }
|
|
81
|
+
.appium-model-grid, .appium-settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
|
|
82
|
+
.appium-settings-grid > .config-appium-card { display: flex; flex-direction: column; }
|
|
83
|
+
.appium-settings-grid > .config-appium-card :deep(.el-card__body) { flex: 1; }
|
|
84
|
+
.appium-config-section > .config-appium-card + .config-appium-card { margin-top: 2px; }
|
|
85
|
+
@media (max-width: 1000px) {
|
|
86
|
+
.appium-model-grid, .appium-settings-grid { grid-template-columns: minmax(0, 1fr); }
|
|
87
|
+
.appium-section-heading { align-items: flex-start; flex-direction: column; }
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AiRecognitionModel } from '../../appium-recorder/ai-recognition';
|
|
3
|
+
|
|
4
|
+
const model = defineModel<AiRecognitionModel>({ required: true });
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
title: string;
|
|
7
|
+
badge: string;
|
|
8
|
+
description: string;
|
|
9
|
+
namePlaceholder: string;
|
|
10
|
+
modelKey: 'appium' | 'promptOptimizer';
|
|
11
|
+
testingModelKey: string;
|
|
12
|
+
testStatus?: string;
|
|
13
|
+
}>();
|
|
14
|
+
defineEmits<{ test: [] }>();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<el-card shadow="never" class="config-module-card config-appium-card appium-model-card">
|
|
19
|
+
<template #header>
|
|
20
|
+
<div class="panel-header">
|
|
21
|
+
<span class="panel-header__title">
|
|
22
|
+
<span>{{ title }}</span>
|
|
23
|
+
<el-tag size="small" effect="plain">{{ badge }}</el-tag>
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
<p class="appium-model-description">{{ description }}</p>
|
|
28
|
+
<el-form label-position="top" :disabled="!!testingModelKey">
|
|
29
|
+
<el-form-item label="Base URL"><el-input v-model="model.baseUrl" placeholder="兼容 OpenAI 的 API 地址" /></el-form-item>
|
|
30
|
+
<el-form-item label="API Key"><el-input v-model="model.apiKey" type="password" show-password /></el-form-item>
|
|
31
|
+
<el-form-item label="Model Name"><el-input v-model="model.name" :placeholder="namePlaceholder" /></el-form-item>
|
|
32
|
+
<el-alert v-if="testStatus" class="appium-model-status" :title="testStatus"
|
|
33
|
+
:type="testStatus.startsWith('测试通过') ? 'success' : 'warning'" :closable="false" show-icon />
|
|
34
|
+
<div class="appium-model-actions">
|
|
35
|
+
<el-button type="primary" plain :loading="testingModelKey === modelKey" :disabled="!!testingModelKey && testingModelKey !== modelKey" @click="$emit('test')">测试并保存</el-button>
|
|
36
|
+
</div>
|
|
37
|
+
</el-form>
|
|
38
|
+
</el-card>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<style scoped>
|
|
42
|
+
.appium-model-description { min-height: 40px; margin: 0 0 18px; color: var(--el-text-color-secondary); font-size: 13px; line-height: 1.6; }
|
|
43
|
+
.appium-model-status { margin-bottom: 16px; }
|
|
44
|
+
.appium-model-actions { display: flex; justify-content: flex-end; }
|
|
45
|
+
</style>
|
|
@@ -15,6 +15,7 @@ defineEmits<{
|
|
|
15
15
|
saveAppPreset: [];
|
|
16
16
|
editAppPreset: [app: AppPreset];
|
|
17
17
|
deleteAppPreset: [id: string];
|
|
18
|
+
cancelAppPresetEdit: [];
|
|
18
19
|
}>();
|
|
19
20
|
</script>
|
|
20
21
|
|
|
@@ -55,21 +56,21 @@ defineEmits<{
|
|
|
55
56
|
<span>预设 App 参数</span>
|
|
56
57
|
<el-tag v-if="appPresetForm.id" size="small" type="warning">编辑中</el-tag>
|
|
57
58
|
</div>
|
|
58
|
-
<div class="panel-header__actions">
|
|
59
|
-
<el-button type="primary" :loading="isSavingAppPreset" @click="$emit('saveAppPreset')">
|
|
60
|
-
{{ appPresetForm.id ? '保存修改' : '保存 App' }}
|
|
61
|
-
</el-button>
|
|
62
|
-
</div>
|
|
63
59
|
</div>
|
|
64
60
|
</template>
|
|
65
61
|
|
|
66
|
-
<
|
|
62
|
+
<div class="app-preset-layout">
|
|
63
|
+
<el-form label-position="top" :disabled="isSavingAppPreset" @submit.prevent="$emit('saveAppPreset')">
|
|
67
64
|
<el-form-item label="App 名称">
|
|
68
65
|
<el-input v-model="appPresetForm.name" placeholder="例如:示例 App" />
|
|
69
66
|
</el-form-item>
|
|
70
67
|
<el-form-item label="App 包名">
|
|
71
68
|
<el-input v-model="appPresetForm.packageName" placeholder="例如:com.example.app" />
|
|
72
69
|
</el-form-item>
|
|
70
|
+
<div class="app-preset-actions">
|
|
71
|
+
<el-button v-if="appPresetForm.id" :disabled="isSavingAppPreset" @click="$emit('cancelAppPresetEdit')">取消编辑</el-button>
|
|
72
|
+
<el-button native-type="submit" :loading="isSavingAppPreset">{{ appPresetForm.id ? '保存修改' : '添加' }}</el-button>
|
|
73
|
+
</div>
|
|
73
74
|
</el-form>
|
|
74
75
|
|
|
75
76
|
<div class="app-preset-list">
|
|
@@ -79,12 +80,23 @@ defineEmits<{
|
|
|
79
80
|
<span>{{ app.packageName }}</span>
|
|
80
81
|
</div>
|
|
81
82
|
<div class="script-row__actions">
|
|
82
|
-
<el-button text size="small" :icon="Edit" @click="$emit('editAppPreset', app)" />
|
|
83
|
-
<el-button text size="small" :icon="Delete" @click="$emit('deleteAppPreset', app.id)" />
|
|
83
|
+
<el-button text size="small" :icon="Edit" :disabled="isSavingAppPreset" :aria-label="`编辑 ${app.name}`" @click="$emit('editAppPreset', app)" />
|
|
84
|
+
<el-button text size="small" :icon="Delete" :disabled="isSavingAppPreset" :aria-label="`删除 ${app.name}`" @click="$emit('deleteAppPreset', app.id)" />
|
|
84
85
|
</div>
|
|
85
86
|
</div>
|
|
86
87
|
<el-empty v-if="!appPresets.length" description="暂无预设 App" />
|
|
87
88
|
</div>
|
|
89
|
+
</div>
|
|
88
90
|
</el-card>
|
|
89
91
|
</div>
|
|
90
92
|
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
.app-preset-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start; }
|
|
96
|
+
.app-preset-actions { display: flex; justify-content: flex-end; margin-top: 4px; }
|
|
97
|
+
.app-preset-layout .app-preset-list { margin-top: 0; padding-left: 24px; border-left: 1px solid var(--ui-border); }
|
|
98
|
+
@media (max-width: 800px) {
|
|
99
|
+
.app-preset-layout { grid-template-columns: minmax(0, 1fr); }
|
|
100
|
+
.app-preset-layout .app-preset-list { padding: 24px 4px 0 0; border-left: 0; border-top: 1px solid var(--ui-border); }
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -1,51 +1,70 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
3
|
import { RefreshLeft } from '@element-plus/icons-vue';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_FLOW_BACKGROUND, FLOW_BACKGROUND_PRESETS, normalizeFlowBackground,
|
|
6
|
+
DEFAULT_FLOW_LINE_COLOR, FLOW_LINE_COLOR_PRESETS, normalizeFlowLineColor, isHexColor,
|
|
7
|
+
} from '../../appium-recorder/flow-appearance';
|
|
5
8
|
|
|
6
|
-
const props = defineProps<{ modelValue?: string;
|
|
7
|
-
const emit = defineEmits<{ 'update:modelValue': [color: string];
|
|
8
|
-
const
|
|
9
|
-
|
|
9
|
+
const props = defineProps<{ modelValue?: string; lineColor?: string }>();
|
|
10
|
+
const emit = defineEmits<{ 'update:modelValue': [color: string]; 'update:lineColor': [color: string] }>();
|
|
11
|
+
const panels = computed(() => [
|
|
12
|
+
{ key: 'background' as const, title: '流程背景色', color: props.modelValue ?? DEFAULT_FLOW_BACKGROUND,
|
|
13
|
+
fallback: DEFAULT_FLOW_BACKGROUND, presets: FLOW_BACKGROUND_PRESETS, normalize: normalizeFlowBackground,
|
|
14
|
+
inputLabel: '十六进制背景色', pickerLabel: '选择流程背景色', resetLabel: '恢复默认颜色', presetLabel: '选择颜色' },
|
|
15
|
+
{ key: 'line' as const, title: '连接线条颜色', color: props.lineColor ?? DEFAULT_FLOW_LINE_COLOR,
|
|
16
|
+
fallback: DEFAULT_FLOW_LINE_COLOR, presets: FLOW_LINE_COLOR_PRESETS, normalize: normalizeFlowLineColor,
|
|
17
|
+
inputLabel: '十六进制线条颜色', pickerLabel: '选择连接线条颜色', resetLabel: '恢复默认线条颜色', presetLabel: '选择线条颜色' },
|
|
18
|
+
].map(panel => ({ ...panel, error: isHexColor(panel.color) ? '' : '请输入有效的十六进制颜色,如 #ABC 或 #AABBCC' })));
|
|
19
|
+
function update(key: 'background' | 'line', color: string) {
|
|
20
|
+
if (key === 'background') emit('update:modelValue', color);
|
|
21
|
+
else emit('update:lineColor', color);
|
|
22
|
+
}
|
|
10
23
|
</script>
|
|
11
24
|
|
|
12
25
|
<template>
|
|
13
26
|
<el-card shadow="never" class="config-module-card config-appium-card flow-appearance-settings">
|
|
14
|
-
<template #header>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</
|
|
37
|
-
</
|
|
38
|
-
</
|
|
27
|
+
<template #header><div class="panel-header"><span>流程外观</span></div></template>
|
|
28
|
+
<div class="flow-appearance-columns">
|
|
29
|
+
<section v-for="panel in panels" :key="panel.key" class="flow-appearance-section" :aria-label="panel.title">
|
|
30
|
+
<h3>{{ panel.title }}</h3>
|
|
31
|
+
<el-form label-position="top">
|
|
32
|
+
<el-form-item label="预选颜色">
|
|
33
|
+
<div class="flow-color-presets">
|
|
34
|
+
<button v-for="preset in panel.presets" :key="preset" type="button"
|
|
35
|
+
:style="{ backgroundColor: preset }" :aria-label="`${panel.presetLabel} ${preset}`" :title="preset"
|
|
36
|
+
:aria-pressed="panel.normalize(panel.color) === preset && !panel.error"
|
|
37
|
+
@click="update(panel.key, preset)" />
|
|
38
|
+
</div>
|
|
39
|
+
</el-form-item>
|
|
40
|
+
<el-form-item label="自定义颜色" :error="panel.error">
|
|
41
|
+
<div class="flow-color-input">
|
|
42
|
+
<el-color-picker :model-value="panel.normalize(panel.color)" color-format="hex" :predefine="panel.presets"
|
|
43
|
+
:aria-label="panel.pickerLabel" @update:model-value="update(panel.key, $event || panel.fallback)" />
|
|
44
|
+
<el-input :model-value="panel.color" :aria-label="panel.inputLabel" :placeholder="panel.fallback"
|
|
45
|
+
@update:model-value="update(panel.key, String($event))" />
|
|
46
|
+
<el-button :icon="RefreshLeft" :title="panel.resetLabel" :aria-label="panel.resetLabel" @click="update(panel.key, panel.fallback)" />
|
|
47
|
+
</div>
|
|
48
|
+
</el-form-item>
|
|
49
|
+
</el-form>
|
|
50
|
+
</section>
|
|
51
|
+
</div>
|
|
39
52
|
</el-card>
|
|
40
53
|
</template>
|
|
41
54
|
|
|
42
55
|
<style scoped>
|
|
43
|
-
.flow-appearance-
|
|
44
|
-
.flow-appearance-
|
|
56
|
+
.flow-appearance-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; }
|
|
57
|
+
.flow-appearance-section { min-width: 0; }
|
|
58
|
+
.flow-appearance-section + .flow-appearance-section { padding-left: 32px; border-left: 1px solid var(--ui-border); }
|
|
59
|
+
.flow-appearance-section h3 { margin: 0 0 20px; font-size: 15px; font-weight: 600; color: var(--el-text-color-primary); }
|
|
45
60
|
.flow-color-presets, .flow-color-input { display: flex; align-items: center; gap: 10px; }
|
|
46
61
|
.flow-color-presets { flex-wrap: wrap; }
|
|
47
62
|
.flow-color-presets button { width: 32px; height: 32px; border: 1px solid var(--el-border-color-darker); border-radius: 4px; cursor: pointer; }
|
|
48
63
|
.flow-color-presets button[aria-pressed="true"] { outline: 2px solid var(--el-color-primary); outline-offset: 2px; }
|
|
49
64
|
.flow-color-input { width: 100%; }
|
|
50
65
|
.flow-color-input .el-input { flex: 1; min-width: 0; }
|
|
66
|
+
@media (max-width: 800px) {
|
|
67
|
+
.flow-appearance-columns { grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
|
68
|
+
.flow-appearance-section + .flow-appearance-section { padding: 24px 0 0; border-left: 0; border-top: 1px solid var(--ui-border); }
|
|
69
|
+
}
|
|
51
70
|
</style>
|
|
@@ -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
|
|
25
|
+
props.configForm.midscene.model.provider || 'custom',
|
|
26
26
|
);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
placeholder="
|
|
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>
|