android-midscene-automation 0.1.24 → 0.1.26
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/CHANGELOG.md +15 -5
- package/README.md +15 -5
- package/package.json +1 -1
- package/server/http-api.ts +141 -105
- package/server/script-agent.ts +50 -45
- package/src/App.vue +133 -54
- package/src/api.ts +15 -10
- package/src/appium-recorder/AppiumPage.vue +156 -55
- package/src/appium-recorder/components/FlowCanvas.vue +2 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +93 -3
- package/src/appium-recorder/components/NestedConditionBranches.vue +6 -0
- package/src/appium-recorder/flow-copy.ts +49 -18
- package/src/appium-recorder/flow-graph.ts +3 -2
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/config/prompt-example.ts +4 -0
- package/src/config/prompt-presets.ts +172 -0
- package/src/pages/AiGeneratorPage.vue +188 -5
- package/src/pages/AutomationPage.vue +3 -3
- package/src/pages/ConfigPage.vue +13 -35
- package/src/pages/GeneratorPage.vue +7 -0
- package/src/style.css +108 -139
- package/src/types.ts +0 -14
- package/server/model-call-usage-importer.ts +0 -108
- package/server/model-usage-repository.ts +0 -131
- package/src/components/config/ModelUsageChart.vue +0 -188
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { promptDocument } from './prompt-example';
|
|
2
|
+
|
|
3
|
+
export type PromptPreset = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const defaultPromptPresetId = 'general';
|
|
11
|
+
|
|
12
|
+
export const promptPresets: PromptPreset[] = [
|
|
13
|
+
{
|
|
14
|
+
id: defaultPromptPresetId,
|
|
15
|
+
name: '通用提示词',
|
|
16
|
+
description: '当前默认提示词,适合启动、登录和常规流程',
|
|
17
|
+
content: promptDocument.trim(),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'ads',
|
|
21
|
+
name: '广告测试',
|
|
22
|
+
description: '开屏、插屏、Banner、激励视频等广告链路',
|
|
23
|
+
content: `# 广告相关测试
|
|
24
|
+
|
|
25
|
+
目标:测试 App 中的广告展示、跳过、关闭、点击跳转或奖励发放链路,包括开屏广告、插屏广告、Banner 广告、信息流广告和激励视频广告。
|
|
26
|
+
|
|
27
|
+
生成脚本时请遵循:
|
|
28
|
+
1. 广告素材、广告标识、倒计时、跳过按钮、关闭按钮、领取奖励按钮、奖励到账提示都是本用例的被测目标,不要把它们当作干扰弹窗自动关闭。
|
|
29
|
+
2. 只有权限申请、升级提示、登录态失效、与广告无关的活动弹窗遮挡广告入口或断言时,才生成单独步骤处理。
|
|
30
|
+
3. 每个 Midscene 指令只做一件事:进入页面、触发广告、等待广告出现、读取广告信息、点击跳过/关闭/领取奖励、等待结果分别拆成独立步骤。
|
|
31
|
+
4. 对可见且确定的按钮使用 aiTap;需要等待广告加载、倒计时结束或奖励提示时使用 aiWaitFor;需要读取广告标题、跳转文案、倒计时数字或奖励数量时使用 aiQuery、aiString 或 aiNumber。
|
|
32
|
+
5. 如果测试开屏广告,先启动 App 并等待开屏广告或首页稳定出现;如果没有广告且需求要求必须展示广告,最终步骤应失败,不要自动改为首页成功。
|
|
33
|
+
6. 如果测试激励广告,播放期间不要跳过视频;等待视频播放完成后点击领取、关闭或确认奖励,并等待奖励到账、余额变化或业务页面返回。
|
|
34
|
+
7. 最终成功判定必须是正向、可观察的界面结果,例如“出现广告标识和素材”“广告关闭后回到原页面”“奖励到账提示出现”“点击广告后进入落地页”。`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'form',
|
|
38
|
+
name: '表单填写',
|
|
39
|
+
description: '注册、登录、资料、地址、反馈等表单提交',
|
|
40
|
+
content: `# 表单填写测试
|
|
41
|
+
|
|
42
|
+
目标:测试 App 中表单页面的输入、选择、提交和结果验证流程。
|
|
43
|
+
|
|
44
|
+
生成脚本时请遵循:
|
|
45
|
+
1. 先进入目标 App 和目标表单页面,等待页面稳定显示字段、标题或提交按钮。
|
|
46
|
+
2. 输入、选择、勾选、上传、提交必须拆成独立步骤,不要用一条 aiAct 同时填写所有字段并提交。
|
|
47
|
+
3. aiInput 的 value 必须完全来自用户原始需求;不要改写、脱敏、补全或编造示例值。
|
|
48
|
+
4. 每个输入框定位要写清楚页面、字段名、附近文本或业务含义,例如“收货地址页中的联系人姓名输入框”。
|
|
49
|
+
5. 下拉选择、日期选择、单选、多选、开关等稳定可见控件优先用 aiTap;如果需要“未勾选才勾选”或“若有默认值则保持”,用单次 aiAct。
|
|
50
|
+
6. 提交后等待明确结果:成功提示、详情页、列表新增项、状态变更或错误提示;不要只判断按钮被点击。
|
|
51
|
+
7. 如果本用例测试校验失败,保留错误状态并读取或等待错误提示,不要自动修正输入继续提交。`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'chat',
|
|
55
|
+
name: '聊天对话',
|
|
56
|
+
description: 'IM、客服、群聊、发送文本/图片/语音',
|
|
57
|
+
content: `# 聊天对话测试
|
|
58
|
+
|
|
59
|
+
目标:测试 App 的聊天、客服或消息会话流程。
|
|
60
|
+
|
|
61
|
+
生成脚本时请遵循:
|
|
62
|
+
1. 先进入目标会话页面,等待会话标题、消息列表、输入框或发送按钮稳定显示。
|
|
63
|
+
2. 发送文本时使用独立 aiInput 定位消息输入框,再用 aiTap 点击发送按钮;不要把输入和发送合成一条泛化 aiAct。
|
|
64
|
+
3. 发送图片、表情、语音、文件等内容时,先打开对应入口,再选择素材或执行录制,最后等待消息气泡出现。
|
|
65
|
+
4. 聊天页中的表情面板、更多面板、语音录制浮层、图片预览、客服欢迎语可能是被测目标,不要当作干扰弹窗关闭。
|
|
66
|
+
5. 如果需要验证回复,使用 aiWaitFor 等待对方消息、客服回复、发送成功状态或已读状态;需要提取回复内容时使用 aiQuery 或 aiString。
|
|
67
|
+
6. 失败场景要保留失败提示,例如发送失败、网络异常、撤回提示,不要自动重试到成功,除非原始需求明确要求重试。
|
|
68
|
+
7. 最终成功判定必须落在可见消息、状态标签、会话列表更新或未读数变化上。`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'player',
|
|
72
|
+
name: '播放器测试',
|
|
73
|
+
description: '播放、暂停、进度、横竖屏、清晰度、弹幕',
|
|
74
|
+
content: `# 播放器相关测试
|
|
75
|
+
|
|
76
|
+
目标:测试 App 中视频、音频或直播播放器的播放控制和状态验证。
|
|
77
|
+
|
|
78
|
+
生成脚本时请遵循:
|
|
79
|
+
1. 播放器控件、播放/暂停按钮、进度条、全屏按钮、清晰度菜单、倍速菜单、弹幕开关、试看提示和广告贴片可能是被测目标,不要默认关闭。
|
|
80
|
+
2. 进入播放页后先等待播放器区域、标题或首帧稳定显示,再执行播放、暂停、拖动进度、切换清晰度或全屏等动作。
|
|
81
|
+
3. 明确按钮点击使用 aiTap;需要等待缓冲完成、播放开始、暂停状态、横屏完成或进度变化时使用 aiWaitFor。
|
|
82
|
+
4. 如果测试贴片广告或会员试看提示,保留广告或提示并验证展示、倒计时、跳过或购买入口;不要自动跳过。
|
|
83
|
+
5. 如果测试普通播放链路,只有与播放目标无关且遮挡操作的权限、升级、活动弹窗才可以单独处理。
|
|
84
|
+
6. 读取播放时长、当前进度、清晰度、倍速、标题或直播状态时使用 aiQuery、aiString 或 aiNumber,并描述它们在播放器中的位置。
|
|
85
|
+
7. 最终成功判定应是可观察状态,例如视频正在播放、按钮变为暂停、进度已变化、进入全屏、清晰度已切换或出现播放完成提示。`,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'search-list',
|
|
89
|
+
name: '搜索列表',
|
|
90
|
+
description: '搜索、筛选、排序、列表结果和详情页',
|
|
91
|
+
content: `# 搜索和列表测试
|
|
92
|
+
|
|
93
|
+
目标:测试 App 中搜索、筛选、排序、列表加载和详情页进入流程。
|
|
94
|
+
|
|
95
|
+
生成脚本时请遵循:
|
|
96
|
+
1. 先进入搜索页或列表页,等待搜索框、筛选入口、列表区域或空状态稳定显示。
|
|
97
|
+
2. 输入搜索词使用 aiInput,提交搜索使用 aiTap 或键盘确认;筛选、排序、分类切换用独立步骤。
|
|
98
|
+
3. 搜索词、筛选条件、排序方式必须来自原始需求,不要自行替换成示例值。
|
|
99
|
+
4. 列表结果需要等待明确的可见信号,例如结果卡片、标题、价格、状态、空结果文案或加载完成提示。
|
|
100
|
+
5. 需要读取多个结果时使用 aiQuery,并指定返回字段;需要点击结果时明确“第一个/包含某文本/某状态”的目标。
|
|
101
|
+
6. 滚动加载只在需求明确需要更多结果时生成,且每次滚动后等待新内容出现。
|
|
102
|
+
7. 最终成功判定应是结果存在、空状态正确、筛选结果符合条件、排序生效或详情页核心信息可见。`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'payment-order',
|
|
106
|
+
name: '订单支付',
|
|
107
|
+
description: '下单、确认订单、支付方式、订单状态',
|
|
108
|
+
content: `# 订单和支付测试
|
|
109
|
+
|
|
110
|
+
目标:测试商品/服务选择、提交订单、支付方式选择和订单结果验证。
|
|
111
|
+
|
|
112
|
+
生成脚本时请遵循:
|
|
113
|
+
1. 进入商品、购物车、服务页或订单页后,等待核心信息稳定显示。
|
|
114
|
+
2. 选择规格、数量、地址、优惠券、支付方式、提交订单、确认支付分别拆成独立步骤。
|
|
115
|
+
3. 对真实支付、扣费、转账、提现、购买等高风险动作,只有原始需求明确要求执行时才生成点击确认支付;否则停在支付确认页并验证页面状态。
|
|
116
|
+
4. 金额、商品名、地址、优惠信息、支付方式等关键数据优先使用 aiQuery 或 aiString 读取后再进行可见结果验证。
|
|
117
|
+
5. 支付密码、验证码、短信码等敏感输入必须完全来自原始需求,不能编造。
|
|
118
|
+
6. 如果测试失败场景,等待并读取失败提示、库存不足、余额不足、风控提示或支付取消状态,不要自动换支付方式继续。
|
|
119
|
+
7. 最终成功判定应是订单提交成功、支付成功、订单状态变化、订单号出现或支付确认页信息正确。`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'permission-popup',
|
|
123
|
+
name: '权限弹窗',
|
|
124
|
+
description: '系统权限、协议、升级、活动、引导页处理',
|
|
125
|
+
content: `# 权限和弹窗测试
|
|
126
|
+
|
|
127
|
+
目标:测试 App 中权限申请、协议确认、升级提示、活动弹窗、引导页或业务弹窗的展示和处理。
|
|
128
|
+
|
|
129
|
+
生成脚本时请遵循:
|
|
130
|
+
1. 先判断弹窗是被测目标还是干扰遮挡:如果需求正在测试该弹窗,不要关闭;如果弹窗只是遮挡后续操作,才生成处理步骤。
|
|
131
|
+
2. 权限、协议、升级、活动、评分、通知引导等弹窗的按钮定位要写清楚,例如“权限弹窗中的允许按钮”“升级弹窗中的稍后再说按钮”。
|
|
132
|
+
3. 如果测试权限允许链路,点击允许后等待业务页面继续加载;如果测试拒绝链路,点击拒绝后等待错误提示、降级入口或权限引导页。
|
|
133
|
+
4. 如果测试强制升级,不能点击立即更新下载;等待并断言强制升级弹窗内容和按钮状态。
|
|
134
|
+
5. 如果测试活动或营销弹窗,保留弹窗并读取标题、活动文案、按钮或关闭入口;不要默认把它当广告关闭。
|
|
135
|
+
6. 条件式处理使用单次 aiAct,例如“如果出现无关弹窗则关闭,否则保持当前状态”;不要用 boolean 只判断不操作。
|
|
136
|
+
7. 最终成功判定应是弹窗内容正确、弹窗被正确关闭、权限结果生效或后续页面无遮挡。`,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'profile-settings',
|
|
140
|
+
name: '个人设置',
|
|
141
|
+
description: '个人资料、设置开关、账号安全、退出登录',
|
|
142
|
+
content: `# 个人资料和设置测试
|
|
143
|
+
|
|
144
|
+
目标:测试 App 的个人中心、资料编辑、设置项、账号安全或退出登录流程。
|
|
145
|
+
|
|
146
|
+
生成脚本时请遵循:
|
|
147
|
+
1. 先进入个人中心或设置页面,等待页面标题、头像、昵称、设置列表或账号信息稳定显示。
|
|
148
|
+
2. 点击入口、编辑字段、切换开关、保存、返回验证分别拆成独立步骤。
|
|
149
|
+
3. 修改昵称、手机号、邮箱、签名等输入值必须来自原始需求,不要自行补全示例数据。
|
|
150
|
+
4. 开关类控件如果要求“打开/关闭到某状态”,使用条件式 aiAct 避免反向切换;如果只是点击明确开关,使用 aiTap。
|
|
151
|
+
5. 退出登录、注销账号、清除数据等高风险动作,只有原始需求明确要求时才执行;否则停在确认弹窗并验证。
|
|
152
|
+
6. 保存后等待正向结果,例如保存成功提示、资料页显示新值、设置状态变化或回到登录页。
|
|
153
|
+
7. 需要读取当前资料或设置状态时使用 aiQuery、aiString 或 aiBoolean,并要求返回可见文本或明确真假。`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'webview-h5',
|
|
157
|
+
name: 'H5/WebView',
|
|
158
|
+
description: '内嵌网页、混合页、活动页和外链跳转',
|
|
159
|
+
content: `# H5 和 WebView 测试
|
|
160
|
+
|
|
161
|
+
目标:测试 App 内嵌 H5、WebView、活动页、帮助页或外链跳转流程。
|
|
162
|
+
|
|
163
|
+
生成脚本时请遵循:
|
|
164
|
+
1. 先进入包含 H5/WebView 的页面,等待页面标题、加载完成标识、主要按钮或内容区域稳定显示。
|
|
165
|
+
2. WebView 内的页面加载、弹窗、登录态提示、活动浮层可能是被测目标,不要默认关闭;只有无关遮挡才处理。
|
|
166
|
+
3. 点击、输入、筛选、提交、返回 App 原生页面分别拆成独立步骤。
|
|
167
|
+
4. 如果页面内容异步加载,使用 aiWaitFor 等待可见内容,不要用固定等待替代明确状态。
|
|
168
|
+
5. 如果需要读取网页内容、订单号、协议文本、活动规则或按钮状态,使用 aiQuery、aiString 或 aiBoolean,并描述屏幕上可见的位置。
|
|
169
|
+
6. 如果测试外链跳转,等待目标页面标题、域名提示、浏览器选择页或返回入口;不要把跳转后的页面误判为失败。
|
|
170
|
+
7. 最终成功判定应是 H5 内容正确、交互结果正确、跳转成功或返回原 App 页面成功。`,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { shallowRef } from 'vue';
|
|
3
|
-
import {
|
|
2
|
+
import { shallowRef, useTemplateRef } from 'vue';
|
|
3
|
+
import { ElMessage } from 'element-plus';
|
|
4
|
+
import { Delete, Edit, QuestionFilled, Upload } from '@element-plus/icons-vue';
|
|
4
5
|
import TestCaseFileUpload from '../components/generator/TestCaseFileUpload.vue';
|
|
5
6
|
import type { AppPreset, GeneratorForm } from '../types';
|
|
7
|
+
import type { PromptPreset } from '../config/prompt-presets';
|
|
6
8
|
|
|
7
|
-
defineProps<{
|
|
9
|
+
const props = defineProps<{
|
|
8
10
|
form: GeneratorForm;
|
|
9
11
|
appPresets: AppPreset[];
|
|
12
|
+
promptPresets: PromptPreset[];
|
|
10
13
|
promptExample: string;
|
|
14
|
+
generating: boolean;
|
|
11
15
|
importingTestCase: boolean;
|
|
12
16
|
importedFileName: string;
|
|
13
17
|
}>();
|
|
@@ -18,7 +22,51 @@ defineEmits<{
|
|
|
18
22
|
}>();
|
|
19
23
|
|
|
20
24
|
const sourcePrompt = defineModel<string>('sourcePrompt', { required: true });
|
|
25
|
+
const promptPresetId = defineModel<string>('promptPresetId', { required: true });
|
|
21
26
|
const promptExampleVisible = shallowRef(false);
|
|
27
|
+
const promptEditorVisible = shallowRef(false);
|
|
28
|
+
const promptEditorDraft = shallowRef('');
|
|
29
|
+
const promptFileInput = useTemplateRef<HTMLInputElement>('promptFileInput');
|
|
30
|
+
|
|
31
|
+
const applyPromptPreset = (id: string) => {
|
|
32
|
+
const preset = props.promptPresets.find((item) => item.id === id);
|
|
33
|
+
if (!preset) return;
|
|
34
|
+
sourcePrompt.value = preset.content;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const openPromptEditor = () => {
|
|
38
|
+
promptEditorDraft.value = sourcePrompt.value;
|
|
39
|
+
promptEditorVisible.value = true;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const savePromptEditor = () => {
|
|
43
|
+
sourcePrompt.value = promptEditorDraft.value.trim();
|
|
44
|
+
promptEditorVisible.value = false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const openPromptFilePicker = () => {
|
|
48
|
+
promptFileInput.value?.click();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handlePromptFileChange = async (event: Event) => {
|
|
52
|
+
const input = event.target as HTMLInputElement;
|
|
53
|
+
const file = input.files?.[0];
|
|
54
|
+
if (!file) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const content = (await file.text()).trim();
|
|
58
|
+
if (!content) {
|
|
59
|
+
ElMessage.warning('上传的提示词为空');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
sourcePrompt.value = content;
|
|
63
|
+
ElMessage.success('提示词已上传');
|
|
64
|
+
} catch {
|
|
65
|
+
ElMessage.error('提示词文件读取失败');
|
|
66
|
+
} finally {
|
|
67
|
+
input.value = '';
|
|
68
|
+
}
|
|
69
|
+
};
|
|
22
70
|
</script>
|
|
23
71
|
|
|
24
72
|
<template>
|
|
@@ -42,6 +90,44 @@ const promptExampleVisible = shallowRef(false);
|
|
|
42
90
|
/>
|
|
43
91
|
</el-select>
|
|
44
92
|
</el-form-item>
|
|
93
|
+
<el-form-item label="提示词模板">
|
|
94
|
+
<div class="prompt-template-row">
|
|
95
|
+
<el-select
|
|
96
|
+
v-model="promptPresetId"
|
|
97
|
+
placeholder="选择提示词模板"
|
|
98
|
+
popper-class="prompt-template-dropdown"
|
|
99
|
+
:disabled="generating"
|
|
100
|
+
@change="applyPromptPreset"
|
|
101
|
+
>
|
|
102
|
+
<el-option
|
|
103
|
+
v-for="preset in promptPresets"
|
|
104
|
+
:key="preset.id"
|
|
105
|
+
:label="preset.name"
|
|
106
|
+
:value="preset.id"
|
|
107
|
+
>
|
|
108
|
+
<div class="prompt-template-option">
|
|
109
|
+
<span class="prompt-template-option__name">{{ preset.name }}</span>
|
|
110
|
+
<span class="prompt-template-option__description">{{ preset.description }}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</el-option>
|
|
113
|
+
</el-select>
|
|
114
|
+
<div class="prompt-template-actions">
|
|
115
|
+
<el-button size="small" :icon="Edit" :disabled="generating" @click="openPromptEditor">
|
|
116
|
+
编辑
|
|
117
|
+
</el-button>
|
|
118
|
+
<el-button size="small" :icon="Upload" :disabled="generating" @click="openPromptFilePicker">
|
|
119
|
+
上传
|
|
120
|
+
</el-button>
|
|
121
|
+
</div>
|
|
122
|
+
<input
|
|
123
|
+
ref="promptFileInput"
|
|
124
|
+
class="prompt-file-input"
|
|
125
|
+
type="file"
|
|
126
|
+
accept=".txt,.md,.markdown,.text"
|
|
127
|
+
@change="handlePromptFileChange"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</el-form-item>
|
|
45
131
|
</el-form>
|
|
46
132
|
</el-card>
|
|
47
133
|
|
|
@@ -51,11 +137,16 @@ const promptExampleVisible = shallowRef(false);
|
|
|
51
137
|
<span>原始 Prompt</span>
|
|
52
138
|
<div class="panel-header__actions prompt-card__actions">
|
|
53
139
|
<TestCaseFileUpload
|
|
54
|
-
:uploading="importingTestCase"
|
|
140
|
+
:uploading="importingTestCase || generating"
|
|
55
141
|
:file-name="importedFileName"
|
|
56
142
|
@upload="$emit('importTestCase', $event)"
|
|
57
143
|
/>
|
|
58
|
-
<el-button
|
|
144
|
+
<el-button
|
|
145
|
+
size="small"
|
|
146
|
+
:icon="Delete"
|
|
147
|
+
:disabled="importingTestCase || generating"
|
|
148
|
+
@click="$emit('clearAll')"
|
|
149
|
+
>
|
|
59
150
|
清除
|
|
60
151
|
</el-button>
|
|
61
152
|
<el-button
|
|
@@ -72,6 +163,7 @@ const promptExampleVisible = shallowRef(false);
|
|
|
72
163
|
class="source-prompt-input"
|
|
73
164
|
type="textarea"
|
|
74
165
|
:rows="14"
|
|
166
|
+
:disabled="generating"
|
|
75
167
|
placeholder="请输入原始 Prompt,或点击右上角问号查看示例"
|
|
76
168
|
/>
|
|
77
169
|
</el-card>
|
|
@@ -80,11 +172,102 @@ const promptExampleVisible = shallowRef(false);
|
|
|
80
172
|
<el-dialog v-model="promptExampleVisible" title="原始 Prompt 示例" width="720px">
|
|
81
173
|
<pre class="prompt-example"><code>{{ promptExample }}</code></pre>
|
|
82
174
|
</el-dialog>
|
|
175
|
+
|
|
176
|
+
<el-dialog v-model="promptEditorVisible" title="编辑提示词" width="760px">
|
|
177
|
+
<el-input
|
|
178
|
+
v-model="promptEditorDraft"
|
|
179
|
+
class="prompt-editor-input"
|
|
180
|
+
type="textarea"
|
|
181
|
+
:rows="18"
|
|
182
|
+
:disabled="generating"
|
|
183
|
+
placeholder="请输入提示词"
|
|
184
|
+
/>
|
|
185
|
+
<template #footer>
|
|
186
|
+
<el-button @click="promptEditorVisible = false">取消</el-button>
|
|
187
|
+
<el-button type="primary" :disabled="generating" @click="savePromptEditor">保存</el-button>
|
|
188
|
+
</template>
|
|
189
|
+
</el-dialog>
|
|
83
190
|
</template>
|
|
84
191
|
|
|
85
192
|
<style scoped>
|
|
193
|
+
.prompt-template-row {
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
gap: 8px;
|
|
197
|
+
width: 100%;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.prompt-template-row .el-select {
|
|
201
|
+
flex: 1 1 auto;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.prompt-template-actions {
|
|
206
|
+
display: flex;
|
|
207
|
+
flex: 0 0 auto;
|
|
208
|
+
gap: 8px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.prompt-template-option {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 4px;
|
|
215
|
+
min-width: 0;
|
|
216
|
+
line-height: 1.35;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.prompt-template-option__name {
|
|
220
|
+
color: #1f2937;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.prompt-template-option__description {
|
|
225
|
+
color: #8a97a8;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
text-overflow: ellipsis;
|
|
229
|
+
white-space: nowrap;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.prompt-file-input {
|
|
233
|
+
display: none;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
:global(.prompt-template-dropdown .el-select-dropdown__list) {
|
|
237
|
+
padding: 6px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
:global(.prompt-template-dropdown .el-select-dropdown__item) {
|
|
241
|
+
height: auto;
|
|
242
|
+
min-height: 64px;
|
|
243
|
+
padding: 11px 12px;
|
|
244
|
+
border-radius: 6px;
|
|
245
|
+
line-height: 1.35;
|
|
246
|
+
white-space: normal;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
:global(.prompt-template-dropdown .el-select-dropdown__item + .el-select-dropdown__item) {
|
|
250
|
+
margin-top: 4px;
|
|
251
|
+
}
|
|
252
|
+
|
|
86
253
|
.prompt-card__actions {
|
|
87
254
|
align-items: center;
|
|
88
255
|
min-width: 0;
|
|
89
256
|
}
|
|
257
|
+
|
|
258
|
+
.prompt-editor-input :deep(.el-textarea__inner) {
|
|
259
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
|
|
260
|
+
line-height: 1.6;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@media (max-width: 640px) {
|
|
264
|
+
.prompt-template-row {
|
|
265
|
+
align-items: stretch;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.prompt-template-actions {
|
|
270
|
+
justify-content: flex-end;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
90
273
|
</style>
|
|
@@ -20,7 +20,7 @@ defineProps<{
|
|
|
20
20
|
formatScriptTime: (value: string) => string;
|
|
21
21
|
switchAndroidDevice: (deviceId: string) => void;
|
|
22
22
|
triggerDeviceKey: (keyCode: number) => void;
|
|
23
|
-
refreshDevicePreview: () => void
|
|
23
|
+
refreshDevicePreview: () => void | Promise<void>;
|
|
24
24
|
openCodeDialog: (script: SavedScript) => void;
|
|
25
25
|
removeSavedScript: (id: string) => void;
|
|
26
26
|
deviceWidth: number;
|
|
@@ -110,7 +110,7 @@ const getRunStatusTagText = (status: string) => {
|
|
|
110
110
|
<el-card shadow="never" class="automation-card execution-card">
|
|
111
111
|
<template #header>执行面板</template>
|
|
112
112
|
<el-form label-position="top">
|
|
113
|
-
<el-form-item label="当前脚本">
|
|
113
|
+
<el-form-item class="execution-script-item" label="当前脚本">
|
|
114
114
|
<el-input :model-value="selectedScript ? `${selectedScript.promptTitle || selectedScript.name} · ${selectedScript.name}` : ''" readonly />
|
|
115
115
|
</el-form-item>
|
|
116
116
|
<el-form-item class="execution-output-item">
|
|
@@ -148,7 +148,7 @@ const getRunStatusTagText = (status: string) => {
|
|
|
148
148
|
class="execution-log__output"
|
|
149
149
|
:value="executionLog"
|
|
150
150
|
readonly
|
|
151
|
-
wrap="
|
|
151
|
+
wrap="soft"
|
|
152
152
|
spellcheck="false"
|
|
153
153
|
/>
|
|
154
154
|
</div>
|
package/src/pages/ConfigPage.vue
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
3
|
import { Delete, Edit, QuestionFilled } from '@element-plus/icons-vue';
|
|
4
|
-
import ModelUsageChart from '../components/config/ModelUsageChart.vue';
|
|
5
4
|
import {
|
|
6
5
|
codexMidsceneModelOptions,
|
|
7
6
|
midsceneModelPresets,
|
|
8
7
|
type MidsceneModelProvider,
|
|
9
|
-
type MidsceneModelPresetKey,
|
|
10
8
|
} from '../config/midscene-model-presets';
|
|
11
|
-
import type { AppPreset, ConfigForm
|
|
9
|
+
import type { AppPreset, ConfigForm } from '../types';
|
|
12
10
|
|
|
13
11
|
type AppPresetForm = {
|
|
14
12
|
id: string;
|
|
@@ -27,7 +25,6 @@ const props = defineProps<{
|
|
|
27
25
|
midscene: string;
|
|
28
26
|
scriptOptimizer: string;
|
|
29
27
|
};
|
|
30
|
-
modelUsageRecords: ModelUsageRecord[];
|
|
31
28
|
}>();
|
|
32
29
|
|
|
33
30
|
const emit = defineEmits<{
|
|
@@ -37,7 +34,6 @@ const emit = defineEmits<{
|
|
|
37
34
|
editAppPreset: [app: AppPreset];
|
|
38
35
|
deleteAppPreset: [id: string];
|
|
39
36
|
updateMidsceneModelProvider: [provider: MidsceneModelProvider];
|
|
40
|
-
applyMidsceneModelPreset: [key: MidsceneModelPresetKey];
|
|
41
37
|
}>();
|
|
42
38
|
|
|
43
39
|
const activeMidsceneProvider = computed<MidsceneModelProvider>(() =>
|
|
@@ -45,15 +41,7 @@ const activeMidsceneProvider = computed<MidsceneModelProvider>(() =>
|
|
|
45
41
|
);
|
|
46
42
|
const modelConfigGuideUrl = 'https://midscenejs.com/zh/model-common-config.html';
|
|
47
43
|
|
|
48
|
-
const
|
|
49
|
-
const model = props.configForm.midscene.model;
|
|
50
|
-
return midsceneModelPresets.find(
|
|
51
|
-
(preset) =>
|
|
52
|
-
preset.baseUrl === model.baseUrl &&
|
|
53
|
-
preset.modelName === model.name &&
|
|
54
|
-
preset.modelFamily === model.family,
|
|
55
|
-
)?.key || '';
|
|
56
|
-
});
|
|
44
|
+
const normalizeBaseUrl = (value: string) => value.trim().replace(/\/+$/, '');
|
|
57
45
|
|
|
58
46
|
const updateMidsceneModelName = (value: string) => {
|
|
59
47
|
props.configForm.midscene.model.name = value;
|
|
@@ -63,9 +51,12 @@ const updateMidsceneModelName = (value: string) => {
|
|
|
63
51
|
}
|
|
64
52
|
};
|
|
65
53
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
const updateCustomMidsceneBaseUrl = (value: string) => {
|
|
55
|
+
props.configForm.midscene.model.baseUrl = value;
|
|
56
|
+
const preset = midsceneModelPresets.find((item) => normalizeBaseUrl(item.baseUrl) === normalizeBaseUrl(value));
|
|
57
|
+
if (preset) {
|
|
58
|
+
props.configForm.midscene.model.name = preset.modelName;
|
|
59
|
+
props.configForm.midscene.model.family = preset.modelFamily;
|
|
69
60
|
}
|
|
70
61
|
};
|
|
71
62
|
|
|
@@ -183,22 +174,12 @@ const openModelConfigGuide = () => {
|
|
|
183
174
|
</el-form-item>
|
|
184
175
|
|
|
185
176
|
<template v-if="activeMidsceneProvider === 'custom'">
|
|
186
|
-
<el-form-item label="模型预设参数">
|
|
187
|
-
<el-select
|
|
188
|
-
:model-value="activeMidscenePresetKey"
|
|
189
|
-
placeholder="请选择模型预设"
|
|
190
|
-
@change="applyMidscenePreset"
|
|
191
|
-
>
|
|
192
|
-
<el-option
|
|
193
|
-
v-for="preset in midsceneModelPresets"
|
|
194
|
-
:key="preset.key"
|
|
195
|
-
:label="preset.label"
|
|
196
|
-
:value="preset.key"
|
|
197
|
-
/>
|
|
198
|
-
</el-select>
|
|
199
|
-
</el-form-item>
|
|
200
177
|
<el-form-item label="Base URL">
|
|
201
|
-
<el-input
|
|
178
|
+
<el-input
|
|
179
|
+
:model-value="configForm.midscene.model.baseUrl"
|
|
180
|
+
placeholder="输入 Base URL,匹配已知提供方时自动填充模型"
|
|
181
|
+
@input="updateCustomMidsceneBaseUrl"
|
|
182
|
+
/>
|
|
202
183
|
</el-form-item>
|
|
203
184
|
<el-form-item label="API Key">
|
|
204
185
|
<el-input v-model="configForm.midscene.model.apiKey" show-password />
|
|
@@ -303,9 +284,6 @@ const openModelConfigGuide = () => {
|
|
|
303
284
|
</section>
|
|
304
285
|
</div>
|
|
305
286
|
|
|
306
|
-
<ModelUsageChart
|
|
307
|
-
:records="modelUsageRecords"
|
|
308
|
-
/>
|
|
309
287
|
</el-card>
|
|
310
288
|
|
|
311
289
|
</section>
|
|
@@ -3,23 +3,27 @@ import AiGeneratorPage from './AiGeneratorPage.vue';
|
|
|
3
3
|
import ManualStepsPage from './ManualStepsPage.vue';
|
|
4
4
|
import GeneratedCodePanel from '../components/generator/GeneratedCodePanel.vue';
|
|
5
5
|
import type { AppPreset, GeneratorForm, GeneratorMode } from '../types';
|
|
6
|
+
import type { PromptPreset } from '../config/prompt-presets';
|
|
6
7
|
import type { ScriptStep } from '../script-generator';
|
|
7
8
|
|
|
8
9
|
defineProps<{
|
|
9
10
|
form: GeneratorForm;
|
|
10
11
|
appPresets: AppPreset[];
|
|
12
|
+
promptPresets: PromptPreset[];
|
|
11
13
|
promptExample: string;
|
|
12
14
|
steps: ScriptStep[];
|
|
13
15
|
generatedCode: string;
|
|
14
16
|
showGeneratedCode: boolean;
|
|
15
17
|
generatedCodeEditing: boolean;
|
|
16
18
|
generatedCodeEditSaving: boolean;
|
|
19
|
+
isGenerating: boolean;
|
|
17
20
|
importingTestCase: boolean;
|
|
18
21
|
importedTestCaseFileName: string;
|
|
19
22
|
}>();
|
|
20
23
|
|
|
21
24
|
const mode = defineModel<GeneratorMode>('mode', { required: true });
|
|
22
25
|
const sourcePrompt = defineModel<string>('sourcePrompt', { required: true });
|
|
26
|
+
const promptPresetId = defineModel<string>('promptPresetId', { required: true });
|
|
23
27
|
const generatedCodeDraft = defineModel<string>('generatedCodeDraft', { required: true });
|
|
24
28
|
|
|
25
29
|
defineEmits<{
|
|
@@ -64,9 +68,12 @@ defineEmits<{
|
|
|
64
68
|
<AiGeneratorPage
|
|
65
69
|
v-if="mode === 'ai'"
|
|
66
70
|
v-model:source-prompt="sourcePrompt"
|
|
71
|
+
v-model:prompt-preset-id="promptPresetId"
|
|
67
72
|
:form="form"
|
|
68
73
|
:app-presets="appPresets"
|
|
74
|
+
:prompt-presets="promptPresets"
|
|
69
75
|
:prompt-example="promptExample"
|
|
76
|
+
:generating="isGenerating"
|
|
70
77
|
:importing-test-case="importingTestCase"
|
|
71
78
|
:imported-file-name="importedTestCaseFileName"
|
|
72
79
|
@import-test-case="$emit('importTestCase', $event)"
|