android-midscene-automation 0.1.23 → 0.1.25
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 +12 -4
- package/README.md +106 -9
- package/package.json +1 -1
- package/server/config.ts +71 -1
- package/server/http-api.ts +143 -107
- 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 +1 -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 +55 -66
- package/src/pages/GeneratorPage.vue +7 -0
- package/src/style.css +104 -137
- 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
package/server/script-agent.ts
CHANGED
|
@@ -45,32 +45,34 @@ const systemPrompt = `你是一个 Midscene 自动化脚本规划器。
|
|
|
45
45
|
7. 通用测试场景默认按“进入/确认目标 App -> 处理启动阶段遮挡 -> 等待起始页面稳定 -> 执行核心业务动作 -> 等待结果页面或读取结果”的结构拆分。不要只生成一条巨型 act,也不要为同一状态生成重复验证。
|
|
46
46
|
8. 如果需求涉及某个 App,第一条可执行步骤只做轻量进入/确认目标 App:若当前已在目标 App 内则保持当前状态;若不在目标 App 内,使用包名打开目标 App 并等待首屏,不要描述“查找桌面图标/从最近任务选择”等视觉搜索过程。启动后不要再生成额外 boolean 确认。
|
|
47
47
|
9. 如果原始需求里有“遇到弹窗则关闭/允许/跳过”这类策略,生成 1 条启动阶段总括 act 步骤即可,默认 repeat: 1;只有用户明确要求“反复处理/直到没有/所有弹窗”时才设置 repeat: 2。prompt 要写清楚只检查权限、启动协议、广告、更新、活动、通知引导等启动阶段弹窗;如果没有弹窗,不要点击业务内容、表单字段、复选框或提交按钮。
|
|
48
|
-
10.
|
|
49
|
-
11.
|
|
50
|
-
12.
|
|
51
|
-
13.
|
|
52
|
-
14.
|
|
53
|
-
15.
|
|
54
|
-
16.
|
|
55
|
-
17.
|
|
56
|
-
18.
|
|
57
|
-
19.
|
|
58
|
-
20. Midscene
|
|
59
|
-
21.
|
|
60
|
-
22. observePrompt
|
|
61
|
-
23.
|
|
62
|
-
24.
|
|
63
|
-
25.
|
|
64
|
-
26.
|
|
65
|
-
27.
|
|
66
|
-
28.
|
|
67
|
-
29.
|
|
68
|
-
30.
|
|
69
|
-
31.
|
|
70
|
-
32.
|
|
71
|
-
33.
|
|
72
|
-
34.
|
|
73
|
-
35.
|
|
48
|
+
10. 处理任何弹窗、广告、活动、播放器浮层、权限、升级提示前,必须先根据原始需求判断它是“被测目标”还是“干扰遮挡”。如果原始需求正在测试广告、播放器、弹窗、活动页、升级提示、引导页等内容,不要把这些元素当作干扰关闭;应生成 waitFor/query/boolean/tap/act 来等待、读取、断言或操作它们。只有与当前测试目标无关且遮挡后续操作的内容,才生成单独 act 关闭、跳过或允许。
|
|
49
|
+
11. 所有场景都要把输入、点击、等待结果拆成独立步骤。输入使用 input,明确点击使用 tap,条件式处理使用 act,页面稳定使用 waitFor,读取数据使用 query/string/number,真假判断使用 boolean。
|
|
50
|
+
12. 所有定位 prompt 都要具体到页面、区域、附近文本或业务含义,例如“搜索页顶部搜索框”“设备列表中的第一个在线设备”“详情页右上角设置按钮”。不要写“输入框”“按钮”“列表项”等泛化描述。
|
|
51
|
+
13. 所有 input 的 value 必须完全来自原始需求;不得改写、脱敏、复用历史值或编造示例值。aiInput 步骤中 prompt 只表示输入框定位描述,value 表示要输入的内容。
|
|
52
|
+
14. 结果验证默认用 1 条 waitFor 写正向、可观察的成功信号,例如目标页面标题、列表项、详情页核心元素、提交成功页、设备状态变化。不要默认追加长串失败排除条件;只有用户明确要求校验失败原因时,才额外生成错误提示读取或失败断言。
|
|
53
|
+
15. 每个 boolean 步骤只能问一个真假问题;默认不要为了确认当前页面状态而生成 boolean,优先使用 waitFor 作为稳定状态验证。不要生成“waitFor + boolean”重复判断同一结果。
|
|
54
|
+
16. 条件式 no-op 动作会增加模型调用,只有确实存在分支时才生成。如果一个动作步骤的 prompt 包含“如果存在/若出现/如果当前不是/如果未勾选/如果已经/否则跳过/保持当前状态”,不能使用 tap;应使用单次 act,或者先用 boolean 判断后再生成确定目标的 tap,但不要两者都生成。
|
|
55
|
+
17. “处理弹窗/关闭弹窗/允许权限/同意协议”必须使用 act 或 tap,因为 boolean/query 只会判断或读取,不会点击按钮。
|
|
56
|
+
18. 生成步骤时必须参考后续提供的 Midscene 文档内容,尤其是 JavaScript 优化建议、aiAct/aiTap/aiInput/aiWaitFor/aiQuery/aiBoolean/aiString/aiNumber 的职责边界和用法。
|
|
57
|
+
19. 不要生成 Midscene 文档中不存在的 API 名称。当前只输出结构化 JSON steps,由代码生成器映射为对应的 Midscene API。
|
|
58
|
+
20. 优先套用后续 Midscene 脚本生成参考中的拆分策略、步骤标题规范、通用场景模板、登录场景模板、弹窗处理模板和失败验证规则。
|
|
59
|
+
21. Midscene 1.10.4 支持 startObserving/UIObserver。只有原始需求明确需要验证动作过程中一闪而过的 Toast、短暂错误提示、Banner、加载状态或自动消失控件时,才给触发该现象的 act/tap/input 步骤填写 observePrompt;如果不需要验证瞬时现象,必须省略该字段。
|
|
60
|
+
22. observePrompt 必须描述一个肯定、可观察的历史事件,例如“点击保存后,执行过程中出现过保存成功提示”。不要用它判断最终页面是否稳定、当前是否仍有弹窗、是否进入结果页等持续状态,这些情况默认只用 aiWaitFor。
|
|
61
|
+
23. observePrompt 应挂在直接触发现象的动作步骤上,不要挂在启动 App、循环处理弹窗或等待步骤上。即使观察到了成功 Toast,只要用户把“进入结果页/列表更新/状态变化”作为成功标准,仍必须生成最终页面的 waitFor;不要再默认补结果 boolean。
|
|
62
|
+
24. 登录场景默认控制在 8 到 10 个可执行步骤:进入/确认 App、处理启动弹窗、等待登录页、必要时进入账号密码登录、输入账号、输入密码、必要时勾选协议、提交登录、处理登录后遮挡弹窗、等待首页。最终默认用 waitFor 作为成功判定;只有用户明确要求读取布尔结果、错误原因或断言日志时,才额外生成 boolean/string/query。
|
|
63
|
+
25. 登录场景必须先从原始需求中抽取账号、密码、验证码等输入值;所有后续步骤必须复用完全相同的字面量,禁止把邮箱改成手机号、把手机号改成邮箱、使用历史账号、示例账号或自行编造账号。
|
|
64
|
+
26. 登录表单必须拆分:账号输入是独立 input,密码输入是独立 input,登录按钮是独立 tap。登录协议勾选框只有在页面/需求明确需要时才生成独立处理步骤。不要用一条 act 同时输入账号、输入密码、勾选协议和点击登录。
|
|
65
|
+
27. 登录按钮步骤必须使用 tap,prompt 只描述点击当前页面的登录按钮,例如“点击登录页中的登录按钮,提交当前已填写的表单;不要修改账号、密码或其他输入框”。不要在登录按钮 prompt 中重新写账号或密码,也不要追加“如果软键盘遮挡”等条件式子句。
|
|
66
|
+
28. 简单且目标确定可见的点击优先使用 tap。登录按钮适合 tap;只有明确当前不在账号密码登录页时,账号密码登录入口才适合 tap;只有明确协议未勾选时,登录协议勾选框才适合 tap。
|
|
67
|
+
29. 如果步骤语义是“如果当前不是账号密码登录页则点击入口,否则保持当前状态”,必须使用单次 act,不要用 tap。
|
|
68
|
+
30. 如果步骤语义是“如果协议未勾选则勾选,已勾选或没有复选框则保持当前状态”,必须使用单次 act,避免 tap 反选已勾选的协议。
|
|
69
|
+
31. 登录协议勾选框、登录按钮和登录后协议确认弹窗不能设置 repeat;每个步骤最多点击一次,后续用 waitFor 判断是否成功,避免再补重复 boolean。
|
|
70
|
+
32. 如果等待登录页后还可能停留在欢迎页、手机号登录页、微信登录页或其他非账号密码登录页,必须在输入账号前生成单次 act:如果当前不是账号密码登录页,点击账号密码登录入口或切换到账号密码登录方式;如果当前已经显示账号和密码输入框,则保持当前状态。
|
|
71
|
+
33. 登录账号输入框 prompt 必须根据已选择的登录方式精确定位:账号密码登录页中写“账号密码登录页中的账号或邮箱输入框”;手机号登录页中写“手机号登录页中的手机号输入框”。不要写“手机号、账号或邮箱输入框”这种会匹配多个字段的泛化描述。
|
|
72
|
+
34. 登录协议勾选 prompt 必须说明只点击复选框本身,不点击用户协议、隐私政策等文本链接。
|
|
73
|
+
35. 登录按钮提交后必须生成 1 条单次 act 处理登录后遮挡弹窗:若出现版本升级、广告、活动、评分、通知引导、登录后协议或权限弹窗,点击关闭、跳过、稍后再说、取消、同意或允许;如果没有弹窗则保持当前状态。不要点击首页业务内容、添加设备、底部导航或退出登录。若是强制升级且没有关闭/取消/稍后按钮,不要点击立即更新,保持弹窗让最终 waitFor 失败。
|
|
74
|
+
36. 登录成功后的 waitFor 默认只写正向成功信号并带无遮挡要求:等待首页或主界面稳定显示,出现设备列表、添加设备入口、底部导航、用户头像或我的页面入口等首页核心元素之一,并且没有版本升级、广告、活动、评分或通知引导弹窗遮挡首页。不要默认追加长串失败排除条件;只有用户明确要求校验失败原因时,才额外生成错误提示读取或失败断言。
|
|
75
|
+
37. 协议确认弹窗和“再次提交登录”不是登录脚本的默认步骤。只有原始需求明确提到点击登录后会出现服务协议/隐私政策/声明与条款确认弹窗,或上下文明确该 App 存在这个二次确认,才生成这两个条件式 act 步骤。`;
|
|
74
76
|
|
|
75
77
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
78
|
const midsceneDocsDir = path.join(projectRoot, 'docs', 'midscene');
|
|
@@ -228,7 +230,7 @@ function normalizeSteps(steps: Array<Record<string, unknown>>) {
|
|
|
228
230
|
});
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
export async function generatePlan(input: { prompt: string }) {
|
|
233
|
+
export async function generatePlan(input: { prompt: string; signal?: AbortSignal }) {
|
|
232
234
|
const startedAt = Date.now();
|
|
233
235
|
const config = loadConfig();
|
|
234
236
|
|
|
@@ -241,24 +243,27 @@ export async function generatePlan(input: { prompt: string }) {
|
|
|
241
243
|
baseURL: config.scriptOptimizer.model.baseUrl,
|
|
242
244
|
});
|
|
243
245
|
|
|
244
|
-
const completion = await client.chat.completions.create(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
246
|
+
const completion = await client.chat.completions.create(
|
|
247
|
+
{
|
|
248
|
+
model: config.scriptOptimizer.model.name,
|
|
249
|
+
temperature: 0.2,
|
|
250
|
+
messages: [
|
|
251
|
+
{ role: 'system', content: systemPrompt },
|
|
252
|
+
{ role: 'system', content: buildMidsceneDocsContext() },
|
|
253
|
+
{
|
|
254
|
+
role: 'user',
|
|
255
|
+
content: JSON.stringify(
|
|
256
|
+
{
|
|
257
|
+
prompt: input.prompt,
|
|
258
|
+
},
|
|
259
|
+
null,
|
|
260
|
+
2,
|
|
261
|
+
),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
{ signal: input.signal },
|
|
266
|
+
);
|
|
262
267
|
|
|
263
268
|
const content = completion.choices[0]?.message?.content;
|
|
264
269
|
if (!content) {
|
package/src/App.vue
CHANGED
|
@@ -20,15 +20,14 @@ import tasksIcon from './assets/device-actions/tasks.svg';
|
|
|
20
20
|
import volumeDownIcon from './assets/device-actions/volume-down.svg';
|
|
21
21
|
import volumeUpIcon from './assets/device-actions/volume-up.svg';
|
|
22
22
|
import { promptDocument } from './config/prompt-example';
|
|
23
|
+
import { defaultPromptPresetId, promptPresets } from './config/prompt-presets';
|
|
23
24
|
import {
|
|
24
25
|
buildScript,
|
|
25
26
|
type ScriptStep,
|
|
26
27
|
} from './script-generator';
|
|
27
28
|
import {
|
|
28
29
|
codexMidsceneModel,
|
|
29
|
-
midsceneModelPresets,
|
|
30
30
|
type MidsceneModelProvider,
|
|
31
|
-
type MidsceneModelPresetKey,
|
|
32
31
|
} from './config/midscene-model-presets';
|
|
33
32
|
import * as api from './api';
|
|
34
33
|
import AutomationPage from './pages/AutomationPage.vue';
|
|
@@ -42,7 +41,6 @@ import type {
|
|
|
42
41
|
ExecutionStep,
|
|
43
42
|
GeneratorMode,
|
|
44
43
|
MenuKey,
|
|
45
|
-
ModelUsageRecord,
|
|
46
44
|
RunScriptStreamEvent,
|
|
47
45
|
SavedScript,
|
|
48
46
|
} from './types';
|
|
@@ -52,7 +50,9 @@ const menuKeys: MenuKey[] = ['generator', 'automation', 'config', 'appium'];
|
|
|
52
50
|
const storedMenu = window.localStorage.getItem(ACTIVE_MENU_STORAGE_KEY) as MenuKey | null;
|
|
53
51
|
const activeMenu = ref<MenuKey>(storedMenu && menuKeys.includes(storedMenu) ? storedMenu : 'generator');
|
|
54
52
|
const activeGeneratorMode = ref<GeneratorMode>('ai');
|
|
55
|
-
const
|
|
53
|
+
const defaultSourcePrompt = promptPresets.find((item) => item.id === defaultPromptPresetId)?.content || '';
|
|
54
|
+
const sourcePrompt = ref(defaultSourcePrompt);
|
|
55
|
+
const promptPresetId = shallowRef(defaultPromptPresetId);
|
|
56
56
|
const steps = ref<ScriptStep[]>([]);
|
|
57
57
|
const errorMessage = ref('');
|
|
58
58
|
const isGenerating = ref(false);
|
|
@@ -73,6 +73,7 @@ const playgroundPreviewError = ref('');
|
|
|
73
73
|
const playgroundDeviceId = ref('');
|
|
74
74
|
const playgroundFrameUrl = ref('');
|
|
75
75
|
const playgroundFrameSignature = ref('');
|
|
76
|
+
const restartingPlaygroundPreview = shallowRef(false);
|
|
76
77
|
const devicePreviewUrl = ref('');
|
|
77
78
|
const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
|
|
78
79
|
const backendOffline = shallowRef(false);
|
|
@@ -95,7 +96,6 @@ const runStartedAt = shallowRef<number | null>(null);
|
|
|
95
96
|
const runningElapsedNow = shallowRef(0);
|
|
96
97
|
const savedScripts = ref<SavedScript[]>([]);
|
|
97
98
|
const appPresets = ref<AppPreset[]>([]);
|
|
98
|
-
const modelUsageRecords = ref<ModelUsageRecord[]>([]);
|
|
99
99
|
const modelTestStatus = reactive({
|
|
100
100
|
midscene: '',
|
|
101
101
|
scriptOptimizer: '',
|
|
@@ -105,6 +105,10 @@ const actionDialog = reactive({
|
|
|
105
105
|
title: '',
|
|
106
106
|
loading: false,
|
|
107
107
|
error: '',
|
|
108
|
+
canRunInBackground: false,
|
|
109
|
+
canCancel: false,
|
|
110
|
+
cancelText: '终止',
|
|
111
|
+
canceling: false,
|
|
108
112
|
});
|
|
109
113
|
const codeDialog = reactive({
|
|
110
114
|
visible: false,
|
|
@@ -116,8 +120,9 @@ const codeDialog = reactive({
|
|
|
116
120
|
saving: false,
|
|
117
121
|
});
|
|
118
122
|
let playgroundPollTimer: number | null = null;
|
|
119
|
-
let
|
|
123
|
+
let devicePreviewTimer: number | null = null;
|
|
120
124
|
let executionProgressTimer: number | null = null;
|
|
125
|
+
let aiGenerateAbortController: AbortController | null = null;
|
|
121
126
|
let scriptRunAbortController: AbortController | null = null;
|
|
122
127
|
|
|
123
128
|
const isBackendFetchError = (error: unknown) => {
|
|
@@ -265,17 +270,36 @@ async function migrateLegacySavedScripts() {
|
|
|
265
270
|
localStorage.removeItem('midscene-saved-scripts');
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
type ActionDialogOptions = {
|
|
274
|
+
canRunInBackground?: boolean;
|
|
275
|
+
canCancel?: boolean;
|
|
276
|
+
cancelText?: string;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const openActionDialog = (title: string, options: ActionDialogOptions = {}) => {
|
|
269
280
|
actionDialog.visible = true;
|
|
270
281
|
actionDialog.title = title;
|
|
271
282
|
actionDialog.loading = true;
|
|
272
283
|
actionDialog.error = '';
|
|
284
|
+
actionDialog.canRunInBackground = !!options.canRunInBackground;
|
|
285
|
+
actionDialog.canCancel = !!options.canCancel;
|
|
286
|
+
actionDialog.cancelText = options.cancelText || '终止';
|
|
287
|
+
actionDialog.canceling = false;
|
|
273
288
|
};
|
|
274
289
|
|
|
275
290
|
const closeActionDialog = () => {
|
|
276
291
|
actionDialog.visible = false;
|
|
277
292
|
actionDialog.loading = false;
|
|
278
293
|
actionDialog.error = '';
|
|
294
|
+
actionDialog.canRunInBackground = false;
|
|
295
|
+
actionDialog.canCancel = false;
|
|
296
|
+
actionDialog.cancelText = '终止';
|
|
297
|
+
actionDialog.canceling = false;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const sendActionDialogToBackground = () => {
|
|
301
|
+
if (!actionDialog.loading || !actionDialog.canRunInBackground) return;
|
|
302
|
+
actionDialog.visible = false;
|
|
279
303
|
};
|
|
280
304
|
|
|
281
305
|
const openCodeDialog = (script: SavedScript) => {
|
|
@@ -328,6 +352,7 @@ const saveCodeDialog = async () => {
|
|
|
328
352
|
const failActionDialog = (message: string) => {
|
|
329
353
|
actionDialog.loading = false;
|
|
330
354
|
actionDialog.error = message;
|
|
355
|
+
actionDialog.canceling = false;
|
|
331
356
|
};
|
|
332
357
|
|
|
333
358
|
const addStep = () => {
|
|
@@ -403,7 +428,8 @@ const clearGeneratorPage = () => {
|
|
|
403
428
|
form.testName = '';
|
|
404
429
|
form.promptTitle = '';
|
|
405
430
|
form.appPresetId = '';
|
|
406
|
-
|
|
431
|
+
promptPresetId.value = defaultPromptPresetId;
|
|
432
|
+
sourcePrompt.value = defaultSourcePrompt;
|
|
407
433
|
steps.value = [];
|
|
408
434
|
showGeneratedCode.value = false;
|
|
409
435
|
importedTestCaseFileName.value = '';
|
|
@@ -534,6 +560,15 @@ const saveCurrentScript = async () => {
|
|
|
534
560
|
}
|
|
535
561
|
};
|
|
536
562
|
|
|
563
|
+
const isAbortError = (error: unknown) =>
|
|
564
|
+
error instanceof DOMException && error.name === 'AbortError';
|
|
565
|
+
|
|
566
|
+
const stopGenerateWithModel = () => {
|
|
567
|
+
if (!aiGenerateAbortController || aiGenerateAbortController.signal.aborted) return;
|
|
568
|
+
actionDialog.canceling = true;
|
|
569
|
+
aiGenerateAbortController.abort();
|
|
570
|
+
};
|
|
571
|
+
|
|
537
572
|
const removeSavedScript = async (id: string) => {
|
|
538
573
|
const script = savedScripts.value.find((item) => item.id === id);
|
|
539
574
|
try {
|
|
@@ -559,6 +594,11 @@ const removeSavedScript = async (id: string) => {
|
|
|
559
594
|
};
|
|
560
595
|
|
|
561
596
|
const generateWithModel = async () => {
|
|
597
|
+
if (isGenerating.value) {
|
|
598
|
+
actionDialog.visible = true;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
562
602
|
if (!validateCurrentGeneratorInputs()) return;
|
|
563
603
|
|
|
564
604
|
if (activeGeneratorMode.value === 'manual') {
|
|
@@ -570,11 +610,18 @@ const generateWithModel = async () => {
|
|
|
570
610
|
|
|
571
611
|
isGenerating.value = true;
|
|
572
612
|
errorMessage.value = '';
|
|
573
|
-
|
|
613
|
+
const controller = new AbortController();
|
|
614
|
+
aiGenerateAbortController = controller;
|
|
615
|
+
openActionDialog('AI生成', {
|
|
616
|
+
canRunInBackground: true,
|
|
617
|
+
canCancel: true,
|
|
618
|
+
cancelText: '终止',
|
|
619
|
+
});
|
|
574
620
|
|
|
575
621
|
try {
|
|
576
622
|
const payload = await api.generatePlan({
|
|
577
623
|
prompt: effectiveSourcePrompt.value,
|
|
624
|
+
signal: controller.signal,
|
|
578
625
|
});
|
|
579
626
|
|
|
580
627
|
form.promptTitle = payload.promptTitle || form.promptTitle;
|
|
@@ -583,11 +630,19 @@ const generateWithModel = async () => {
|
|
|
583
630
|
showGeneratedCode.value = true;
|
|
584
631
|
closeActionDialog();
|
|
585
632
|
} catch (error) {
|
|
633
|
+
if (isAbortError(error)) {
|
|
634
|
+
closeActionDialog();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
586
638
|
const message = error instanceof Error ? error.message : 'AI 生成失败';
|
|
587
639
|
errorMessage.value = message;
|
|
588
640
|
failActionDialog(message);
|
|
589
641
|
} finally {
|
|
590
|
-
|
|
642
|
+
if (aiGenerateAbortController === controller) {
|
|
643
|
+
aiGenerateAbortController = null;
|
|
644
|
+
}
|
|
645
|
+
actionDialog.canceling = false;
|
|
591
646
|
isGenerating.value = false;
|
|
592
647
|
}
|
|
593
648
|
};
|
|
@@ -627,18 +682,6 @@ const updateMidsceneModelProvider = (provider: MidsceneModelProvider) => {
|
|
|
627
682
|
modelTestStatus.midscene = '';
|
|
628
683
|
};
|
|
629
684
|
|
|
630
|
-
const applyMidsceneModelPreset = (key: MidsceneModelPresetKey) => {
|
|
631
|
-
const preset = midsceneModelPresets.find((item) => item.key === key);
|
|
632
|
-
if (!preset) return;
|
|
633
|
-
|
|
634
|
-
configForm.midscene.model.provider = 'custom';
|
|
635
|
-
configForm.midscene.model.baseUrl = preset.baseUrl;
|
|
636
|
-
configForm.midscene.model.name = preset.modelName;
|
|
637
|
-
configForm.midscene.model.family = preset.modelFamily;
|
|
638
|
-
rememberCustomMidsceneModel();
|
|
639
|
-
modelTestStatus.midscene = '';
|
|
640
|
-
};
|
|
641
|
-
|
|
642
685
|
const getMidsceneModelConfigError = () => {
|
|
643
686
|
const model = configForm.midscene.model;
|
|
644
687
|
const missing = [];
|
|
@@ -733,11 +776,6 @@ const removeAppPreset = async (id: string) => {
|
|
|
733
776
|
}
|
|
734
777
|
};
|
|
735
778
|
|
|
736
|
-
const loadModelUsageRecords = async () => {
|
|
737
|
-
const payload = await api.getModelUsageRecords(50);
|
|
738
|
-
modelUsageRecords.value = payload.records || [];
|
|
739
|
-
};
|
|
740
|
-
|
|
741
779
|
const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
742
780
|
testingModelKey.value = key;
|
|
743
781
|
errorMessage.value = '';
|
|
@@ -753,7 +791,6 @@ const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
|
|
|
753
791
|
} catch (error) {
|
|
754
792
|
modelTestStatus[key] = error instanceof Error ? error.message : '测试失败';
|
|
755
793
|
} finally {
|
|
756
|
-
await loadModelUsageRecords().catch(() => undefined);
|
|
757
794
|
testingModelKey.value = '';
|
|
758
795
|
}
|
|
759
796
|
};
|
|
@@ -789,11 +826,13 @@ const loadDeviceInterface = async () => {
|
|
|
789
826
|
const loadAdbPreview = () => {
|
|
790
827
|
if (backendOffline.value) return;
|
|
791
828
|
if (!playgroundDeviceId.value) {
|
|
829
|
+
playgroundFrameUrl.value = '';
|
|
792
830
|
devicePreviewUrl.value = '';
|
|
793
831
|
devicePreviewMode.value = '';
|
|
794
832
|
return;
|
|
795
833
|
}
|
|
796
834
|
|
|
835
|
+
playgroundFrameUrl.value = '';
|
|
797
836
|
devicePreviewUrl.value = `${api.APP_BASE}/api/android-preview?deviceId=${encodeURIComponent(playgroundDeviceId.value)}&t=${Date.now()}`;
|
|
798
837
|
devicePreviewMode.value = 'screenshot';
|
|
799
838
|
};
|
|
@@ -804,25 +843,37 @@ const refreshAdbPreviewAfterInput = () => {
|
|
|
804
843
|
}
|
|
805
844
|
};
|
|
806
845
|
|
|
807
|
-
const refreshDevicePreview = () => {
|
|
808
|
-
if (devicePreviewMode.value === 'stream' &&
|
|
809
|
-
|
|
846
|
+
const refreshDevicePreview = async () => {
|
|
847
|
+
if (devicePreviewMode.value === 'stream' && playgroundDeviceId.value) {
|
|
848
|
+
if (restartingPlaygroundPreview.value) return;
|
|
849
|
+
restartingPlaygroundPreview.value = true;
|
|
850
|
+
playgroundFrameUrl.value = '';
|
|
851
|
+
playgroundFrameSignature.value = '';
|
|
852
|
+
try {
|
|
853
|
+
await api.restartPlaygroundPreview({ deviceId: playgroundDeviceId.value });
|
|
854
|
+
restartingPlaygroundPreview.value = false;
|
|
855
|
+
await loadPlaygroundStatus();
|
|
856
|
+
} catch (error) {
|
|
857
|
+
playgroundPreviewError.value = error instanceof Error ? error.message : '重启实时预览失败';
|
|
858
|
+
} finally {
|
|
859
|
+
restartingPlaygroundPreview.value = false;
|
|
860
|
+
}
|
|
810
861
|
return;
|
|
811
862
|
}
|
|
812
863
|
loadAdbPreview();
|
|
813
864
|
};
|
|
814
865
|
|
|
815
|
-
const
|
|
866
|
+
const startDevicePreviewTimer = () => {
|
|
816
867
|
if (backendOffline.value) return;
|
|
817
|
-
if (
|
|
868
|
+
if (devicePreviewTimer) return;
|
|
818
869
|
loadAdbPreview();
|
|
819
|
-
|
|
870
|
+
devicePreviewTimer = window.setInterval(loadAdbPreview, 1000);
|
|
820
871
|
};
|
|
821
872
|
|
|
822
|
-
const
|
|
823
|
-
if (!
|
|
824
|
-
window.clearInterval(
|
|
825
|
-
|
|
873
|
+
const stopDevicePreviewTimer = () => {
|
|
874
|
+
if (!devicePreviewTimer) return;
|
|
875
|
+
window.clearInterval(devicePreviewTimer);
|
|
876
|
+
devicePreviewTimer = null;
|
|
826
877
|
};
|
|
827
878
|
|
|
828
879
|
const markBackendOffline = () => {
|
|
@@ -836,13 +887,13 @@ const markBackendOffline = () => {
|
|
|
836
887
|
playgroundPreviewError.value = '本地服务已断开,请重新运行启动命令后刷新页面。';
|
|
837
888
|
errorMessage.value = playgroundPreviewError.value;
|
|
838
889
|
stopPlaygroundPollTimer();
|
|
839
|
-
|
|
890
|
+
stopDevicePreviewTimer();
|
|
840
891
|
};
|
|
841
892
|
|
|
842
893
|
const loadPlaygroundStatus = async () => {
|
|
843
|
-
if (backendOffline.value) return;
|
|
894
|
+
if (backendOffline.value || restartingPlaygroundPreview.value) return;
|
|
844
895
|
if (!playgroundDeviceId.value) {
|
|
845
|
-
|
|
896
|
+
stopDevicePreviewTimer();
|
|
846
897
|
playgroundAvailable.value = false;
|
|
847
898
|
playgroundPreviewError.value = '';
|
|
848
899
|
playgroundFrameUrl.value = '';
|
|
@@ -854,7 +905,9 @@ const loadPlaygroundStatus = async () => {
|
|
|
854
905
|
|
|
855
906
|
try {
|
|
856
907
|
await loadDeviceInterface();
|
|
908
|
+
if (restartingPlaygroundPreview.value) return;
|
|
857
909
|
const payload = await api.getPlaygroundStatus();
|
|
910
|
+
if (restartingPlaygroundPreview.value) return;
|
|
858
911
|
|
|
859
912
|
const matchedDevice = !payload.deviceId || payload.deviceId === playgroundDeviceId.value;
|
|
860
913
|
const hasRealtimeStream = payload.previewKind === 'scrcpy' && payload.sessionConnected === true;
|
|
@@ -867,7 +920,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
867
920
|
|
|
868
921
|
const nextSignature = `${payload.url || ''}::${playgroundDeviceId.value}`;
|
|
869
922
|
if (hasPlayground) {
|
|
870
|
-
|
|
923
|
+
stopDevicePreviewTimer();
|
|
871
924
|
if (nextSignature !== playgroundFrameSignature.value || !playgroundFrameUrl.value) {
|
|
872
925
|
playgroundFrameSignature.value = nextSignature;
|
|
873
926
|
playgroundFrameUrl.value = `${api.APP_BASE}/__android_playground__/?ts=${Date.now()}`;
|
|
@@ -881,7 +934,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
881
934
|
playgroundFrameUrl.value = '';
|
|
882
935
|
playgroundFrameSignature.value = '';
|
|
883
936
|
loadAdbPreview();
|
|
884
|
-
if (activeMenu.value === 'appium')
|
|
937
|
+
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
885
938
|
}
|
|
886
939
|
} catch (error) {
|
|
887
940
|
if (isBackendFetchError(error)) {
|
|
@@ -892,6 +945,7 @@ const loadPlaygroundStatus = async () => {
|
|
|
892
945
|
playgroundFrameUrl.value = '';
|
|
893
946
|
playgroundFrameSignature.value = '';
|
|
894
947
|
loadAdbPreview();
|
|
948
|
+
if (activeMenu.value === 'appium' || activeMenu.value === 'automation') startDevicePreviewTimer();
|
|
895
949
|
playgroundPreviewError.value = error instanceof Error ? error.message : '设备预览不可用';
|
|
896
950
|
}
|
|
897
951
|
};
|
|
@@ -1183,7 +1237,6 @@ const runSelectedScript = async () => {
|
|
|
1183
1237
|
lastRunStatus.value = isAbortError ? '已停止' : '执行失败';
|
|
1184
1238
|
} finally {
|
|
1185
1239
|
stopExecutionProgressTimer();
|
|
1186
|
-
await loadModelUsageRecords().catch(() => undefined);
|
|
1187
1240
|
isRunningScript.value = false;
|
|
1188
1241
|
isStoppingScript.value = false;
|
|
1189
1242
|
runStartedAt.value = null;
|
|
@@ -1233,10 +1286,10 @@ watch(
|
|
|
1233
1286
|
watch(activeMenu, (menu) => {
|
|
1234
1287
|
window.localStorage.setItem(ACTIVE_MENU_STORAGE_KEY, menu);
|
|
1235
1288
|
if (menu === 'appium' || menu === 'automation') {
|
|
1236
|
-
|
|
1289
|
+
stopDevicePreviewTimer();
|
|
1237
1290
|
void loadPlaygroundStatus();
|
|
1238
1291
|
} else {
|
|
1239
|
-
|
|
1292
|
+
stopDevicePreviewTimer();
|
|
1240
1293
|
}
|
|
1241
1294
|
}, { immediate: true });
|
|
1242
1295
|
|
|
@@ -1247,7 +1300,7 @@ onMounted(async () => {
|
|
|
1247
1300
|
console.warn('脚本缓存迁移失败', error);
|
|
1248
1301
|
}
|
|
1249
1302
|
|
|
1250
|
-
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts()
|
|
1303
|
+
await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts()]);
|
|
1251
1304
|
await loadPlaygroundStatus();
|
|
1252
1305
|
playgroundPollTimer = window.setInterval(() => {
|
|
1253
1306
|
if (backendOffline.value) {
|
|
@@ -1261,8 +1314,9 @@ onMounted(async () => {
|
|
|
1261
1314
|
|
|
1262
1315
|
onUnmounted(() => {
|
|
1263
1316
|
stopPlaygroundPollTimer();
|
|
1264
|
-
|
|
1317
|
+
stopDevicePreviewTimer();
|
|
1265
1318
|
stopExecutionProgressTimer();
|
|
1319
|
+
aiGenerateAbortController?.abort();
|
|
1266
1320
|
scriptRunAbortController?.abort();
|
|
1267
1321
|
});
|
|
1268
1322
|
</script>
|
|
@@ -1286,7 +1340,31 @@ onUnmounted(() => {
|
|
|
1286
1340
|
{{ actionDialog.error }}
|
|
1287
1341
|
</div>
|
|
1288
1342
|
<template #footer>
|
|
1289
|
-
<
|
|
1343
|
+
<div v-if="actionDialog.loading" class="action-dialog__footer-actions">
|
|
1344
|
+
<el-button
|
|
1345
|
+
v-if="actionDialog.canCancel"
|
|
1346
|
+
class="action-dialog__footer-button action-dialog__footer-button--secondary"
|
|
1347
|
+
:icon="VideoPause"
|
|
1348
|
+
:loading="actionDialog.canceling"
|
|
1349
|
+
@click="stopGenerateWithModel"
|
|
1350
|
+
>
|
|
1351
|
+
{{ actionDialog.cancelText === '终止' ? '终止生成' : actionDialog.cancelText }}
|
|
1352
|
+
</el-button>
|
|
1353
|
+
<el-button
|
|
1354
|
+
v-if="actionDialog.canRunInBackground"
|
|
1355
|
+
class="action-dialog__footer-button action-dialog__footer-button--primary"
|
|
1356
|
+
@click="sendActionDialogToBackground"
|
|
1357
|
+
>
|
|
1358
|
+
后台处理
|
|
1359
|
+
</el-button>
|
|
1360
|
+
</div>
|
|
1361
|
+
<el-button
|
|
1362
|
+
v-else
|
|
1363
|
+
class="action-dialog__footer-button action-dialog__footer-button--primary"
|
|
1364
|
+
@click="closeActionDialog"
|
|
1365
|
+
>
|
|
1366
|
+
关闭
|
|
1367
|
+
</el-button>
|
|
1290
1368
|
</template>
|
|
1291
1369
|
</el-dialog>
|
|
1292
1370
|
|
|
@@ -1355,11 +1433,11 @@ onUnmounted(() => {
|
|
|
1355
1433
|
<el-button
|
|
1356
1434
|
v-if="activeMenu === 'generator'"
|
|
1357
1435
|
type="primary"
|
|
1358
|
-
:icon="Check"
|
|
1359
|
-
:
|
|
1436
|
+
:icon="isGenerating ? Loading : Check"
|
|
1437
|
+
:class="{ 'layout-header__button--loading': isGenerating }"
|
|
1360
1438
|
@click="generateWithModel"
|
|
1361
1439
|
>
|
|
1362
|
-
AI 生成
|
|
1440
|
+
{{ isGenerating ? 'AI 生成中' : 'AI 生成' }}
|
|
1363
1441
|
</el-button>
|
|
1364
1442
|
<el-button
|
|
1365
1443
|
v-if="activeMenu === 'generator'"
|
|
@@ -1404,15 +1482,18 @@ onUnmounted(() => {
|
|
|
1404
1482
|
v-show="activeMenu === 'generator'"
|
|
1405
1483
|
v-model:mode="activeGeneratorMode"
|
|
1406
1484
|
v-model:source-prompt="sourcePrompt"
|
|
1485
|
+
v-model:prompt-preset-id="promptPresetId"
|
|
1407
1486
|
v-model:generated-code-draft="generatedCodeDraft"
|
|
1408
1487
|
:form="form"
|
|
1409
1488
|
:app-presets="appPresets"
|
|
1489
|
+
:prompt-presets="promptPresets"
|
|
1410
1490
|
:prompt-example="promptDocument.trim()"
|
|
1411
1491
|
:steps="steps"
|
|
1412
1492
|
:generated-code="generatedCode"
|
|
1413
1493
|
:show-generated-code="showGeneratedCode"
|
|
1414
1494
|
:generated-code-editing="generatedCodeEditing"
|
|
1415
1495
|
:generated-code-edit-saving="generatedCodeEditSaving"
|
|
1496
|
+
:is-generating="isGenerating"
|
|
1416
1497
|
:importing-test-case="importingTestCase"
|
|
1417
1498
|
:imported-test-case-file-name="importedTestCaseFileName"
|
|
1418
1499
|
@add-step="addStep"
|
|
@@ -1434,14 +1515,12 @@ onUnmounted(() => {
|
|
|
1434
1515
|
:is-saving-model-config="isSavingModelConfig"
|
|
1435
1516
|
:is-saving-app-preset="isSavingAppPreset"
|
|
1436
1517
|
:model-test-status="modelTestStatus"
|
|
1437
|
-
:model-usage-records="modelUsageRecords"
|
|
1438
1518
|
@test-model="testModel"
|
|
1439
1519
|
@save-model-config="saveModelConfig"
|
|
1440
1520
|
@save-app-preset="saveAppPreset"
|
|
1441
1521
|
@edit-app-preset="editAppPreset"
|
|
1442
1522
|
@delete-app-preset="removeAppPreset"
|
|
1443
1523
|
@update-midscene-model-provider="updateMidsceneModelProvider"
|
|
1444
|
-
@apply-midscene-model-preset="applyMidsceneModelPreset"
|
|
1445
1524
|
/>
|
|
1446
1525
|
|
|
1447
1526
|
<AutomationPage
|