android-midscene-automation 0.1.42 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/AI/350/257/206/345/210/253/346/223/215/344/275/234/350/257/264/346/230/216.md +442 -0
  2. package/CHANGELOG.md +12 -2
  3. package/FAQ.md +8 -8
  4. package/README.md +156 -19
  5. package/USAGE.md +134 -9
  6. package/package.json +3 -2
  7. package/server/appium-auto-save.test.ts +2 -1
  8. package/server/appium-config.test.ts +48 -1
  9. package/server/appium-recorder/ai-recognition.test.ts +27 -0
  10. package/server/appium-recorder/ai-recognition.ts +16 -4
  11. package/server/appium-recorder/appium-runner.ts +32 -2
  12. package/server/appium-recorder/bounded-loop.test.ts +55 -0
  13. package/server/appium-recorder/bounded-loop.ts +12 -1
  14. package/server/appium-recorder/prompt-optimizer.ts +57 -0
  15. package/server/appium-recorder/report-summary.test.ts +78 -0
  16. package/server/appium-recorder/report-summary.ts +60 -0
  17. package/server/appium-recorder/report.ts +31 -2
  18. package/server/appium-recorder/repository.ts +8 -2
  19. package/server/appium-recorder/routes.ts +53 -3
  20. package/server/config.ts +17 -2
  21. package/server/http-api.ts +8 -4
  22. package/src/App.vue +34 -5
  23. package/src/api.ts +2 -2
  24. package/src/appium-recorder/AppiumPage.vue +126 -62
  25. package/src/appium-recorder/action-descriptions.ts +1 -0
  26. package/src/appium-recorder/ai-recognition.ts +7 -0
  27. package/src/appium-recorder/api.ts +22 -2
  28. package/src/appium-recorder/bounded-loop.ts +9 -1
  29. package/src/appium-recorder/components/AiRecognitionSettings.vue +57 -6
  30. package/src/appium-recorder/components/BreakLoopSettings.vue +19 -3
  31. package/src/appium-recorder/components/ComponentTree.vue +5 -5
  32. package/src/appium-recorder/components/CoordinatePointPicker.vue +23 -0
  33. package/src/appium-recorder/components/FlowActionMenu.vue +50 -20
  34. package/src/appium-recorder/components/FlowStepEditor.vue +10 -2
  35. package/src/appium-recorder/components/NestedConditionBranches.vue +18 -49
  36. package/src/appium-recorder/components/RecordedSteps.vue +16 -5
  37. package/src/appium-recorder/coordinate-preview.ts +37 -0
  38. package/src/appium-recorder/flow-copy.ts +1 -0
  39. package/src/appium-recorder/flow-graph.ts +5 -2
  40. package/src/appium-recorder/flow-labels.ts +4 -2
  41. package/src/appium-recorder/flow-merge.ts +1 -1
  42. package/src/appium-recorder/flow-unmerge.ts +2 -2
  43. package/src/appium-recorder/report-summary.ts +60 -0
  44. package/src/appium-recorder/types.ts +4 -1
  45. package/src/components/config/AiDeduplicationSettings.vue +5 -4
  46. package/src/components/config/AiPromptPresetsSettings.vue +64 -17
  47. package/src/components/config/AppiumConfigPanel.vue +66 -38
  48. package/src/components/config/AppiumModelSettings.vue +45 -0
  49. package/src/components/config/FlowAppearanceSettings.vue +0 -1
  50. package/src/components/config/ReplayReportSummarySettings.vue +118 -0
  51. package/src/components/device/DevicePreviewPanel.vue +18 -3
  52. package/src/components/help/HelpCenterDialog.vue +160 -0
  53. package/src/components/help/MarkdownDocument.vue +139 -0
  54. package/src/config/use-appium-auto-save.ts +12 -4
  55. package/src/pages/ConfigPage.vue +4 -2
  56. package/src/style.css +37 -45
  57. package/src/types.ts +2 -1
  58. 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
package/README.md CHANGED
@@ -52,6 +52,8 @@ npx android-midscene-automation --port 5174
52
52
  ## 功能文档
53
53
 
54
54
  - [Appium 录制器使用说明](#appium-录制器使用说明)
55
+ - [流程节点操作说明](./流程节点操作说明.md)
56
+ - [AI 识别操作说明](./AI识别操作说明.md)
55
57
  - [常见问题](#常见问题)
56
58
  - [项目更新记录](#项目更新记录)
57
59
 
@@ -171,9 +173,54 @@ midscene_run/ # Midscene 执行报告和运行产物
171
173
 
172
174
  本文档随 npm 包发布,也可以从 README 的“功能文档”直接打开。常见问题请查看 [常见问题](#常见问题)。
173
175
 
174
- AI 识别的场景选择、预设提示词、持续观察、分支、截图去重及超时配置,请查看 [AI 识别详细使用说明](./AI_RECOGNITION_GUIDE.md)。
176
+ Appium 各流程节点的用途和配置方式,请查看 [流程节点操作说明](./流程节点操作说明.md)。AI 识别的场景选择、预设提示词、持续观察、分支、截图去重及超时配置,请查看 [AI 识别操作说明](./AI识别操作说明.md)。
175
177
 
176
- ## 使用前准备
178
+ ## 页面内帮助
179
+
180
+ 点击侧边栏最下方的问号图标,可以在“帮助与版本”窗口中查看:
181
+
182
+ - 当前工具版本和对应的 Midscene 版本。
183
+ - Vue、Element Plus、Appium、Midscene、Vue Flow、OpenCV.js、Vite 和 TypeScript 等主要技术及版本。
184
+ - 本操作说明。
185
+ - 常见问题和各版本更新记录。
186
+
187
+ Midscene 操作说明读取 `USAGE.md`,Appium 操作说明读取 `流程节点操作说明.md`,AI 识别操作说明读取 `AI识别操作说明.md`,常见问题和更新记录分别读取 `FAQ.md` 与 `CHANGELOG.md`;文档更新后页面内容会同步更新。
188
+
189
+ ## Midscene 操作说明
190
+
191
+ ### 配置 Midscene 模型
192
+
193
+ 进入“参数配置”,在 Midscene 模型区域选择模型提供方并填写所需参数。Midscene 执行依赖支持图片输入的多模态模型,填写后应先点击测试,确认模型可以正常响应。
194
+
195
+ ### 生成测试脚本
196
+
197
+ 进入“Midscene > 测试脚本生成”,填写脚本名称、场景标题、目标 App 和测试需求,也可以上传用例文件。点击“AI 生成”后检查生成步骤,确保测试数据、操作目标和成功条件准确。
198
+
199
+ ### 自定义步骤编排
200
+
201
+ 在测试脚本生成页面切换到“自定义步骤编排”,可以逐条添加操作、等待、查询和判断步骤。每条指令只描述一个明确目标,页面跳转后使用可观察的页面元素作为等待条件。
202
+
203
+ ### 编辑并保存脚本
204
+
205
+ 生成脚本后可以查看、编辑或复制代码。确认内容后点击“保存脚本”,保存的脚本会出现在“自动化测试”页面中。
206
+
207
+ ### 执行 Midscene 脚本
208
+
209
+ 进入“Midscene > 自动化测试”,选择脚本和 Android 设备后点击“开始执行”。执行期间保持设备亮屏和解锁,Midscene 会根据当前画面理解目标并执行操作。
210
+
211
+ ### 停止执行
212
+
213
+ 脚本运行期间点击顶部“停止执行”可以中止后续步骤。停止后查看执行输出,确认最后完成的步骤和停止位置。
214
+
215
+ ### 查看执行输出
216
+
217
+ 自动化测试页面会实时显示步骤状态、耗时和错误信息。执行失败时先检查模型配置、ADB 连接、设备画面以及指令中的目标描述是否清晰。
218
+
219
+ ### Midscene 编写建议
220
+
221
+ 一条指令只完成一个目标;输入、点击和等待尽量拆分;等待条件应描述页面中可观察到的稳定元素;不要在同一步骤中同时处理大量弹窗和业务操作。组件 ID 稳定并需要大量重复回放时,可以改用 Appium 流程。
222
+
223
+ ## Appium 使用前准备
177
224
 
178
225
  ### 1. 连接 Android 设备
179
226
 
@@ -282,14 +329,81 @@ App 包名是必填项,只能从“参数配置”中已经添加的“预设
282
329
  - 点击节点右上角删除按钮可移除节点,流程引用会自动修正。
283
330
  - 浏览器刷新后会保持当前功能页面和 Appium 工作区 Tab;存在未保存修改时,刷新或关闭页面前会提示保存。
284
331
 
285
- ### 单 Activity 录制规则
332
+ ### 跨 Activity 录制
286
333
 
287
- 每个脚本只允许录制一个 Activity。录制点击后如果检测到 Activity 发生变化,会强制弹出保存提示;保存完成后清空当前流程,并从新 Activity 开始录制新脚本。
334
+ 录制点击后如果检测到 Activity 发生变化,系统会提示选择继续录制或结束并保存。需要把多个页面保存在同一脚本时选择继续,等待新页面组件树刷新后即可录制后续操作;希望按页面拆分时,结束当前脚本并在新 Activity 中创建脚本,再通过“连接脚本”组合流程。
288
335
 
289
- 加载历史脚本时,如果手机当前 Activity 与脚本入口 Activity 不一致,流程编辑会锁定。此时可执行脚本中的“启动 APP”节点,或插入“系统返回”使页面回到脚本绑定的 Activity;Activity 匹配后自动解除锁定。
336
+ 跨页面流程建议在跳转操作后添加“等待 Activity”,明确目标页面并提高回放稳定性。加载历史脚本时,即使手机当前 Activity 与脚本入口 Activity 不同,也可以查看和编辑流程;回放前应确认启动节点、等待 Activity 或前置操作能把设备带到正确页面。
290
337
 
291
338
  ## 操作说明
292
339
 
340
+ ### 当前操作分类
341
+
342
+ - 组件操作:录制点击、录制输入、提取变量、清空输入、长按、存在则点击、存在则输入、存在则清空。
343
+ - 设备操作:点击坐标、系统返回、Home 键、最近任务、电源键、滑动、双指缩放、启动 APP、停止 APP、启动相册、清除 APP 数据。
344
+ - 判断与等待:判断存在、判断勾选、文字点击、AI 识别、图像判断、等待出现、等待消失、等待 Activity。
345
+ - 检测画面变化:在三级菜单中分别添加“开始”和“结束”,对比指定区域前后的画面。
346
+ - 循环:添加有界循环、进入下一次循环和退出循环。进入下一次循环会跳过本轮剩余节点;退出循环会执行目标循环结束后的流程。
347
+ - 流程控制:连接脚本、终止流程、输出日志和添加延时。
348
+
349
+ 旧脚本中的“断言存在”和“断言文本”仍可回放,但新流程统一使用当前菜单中的判断与等待操作。
350
+
351
+ ### 提取变量
352
+
353
+ 读取选中组件的文字或属性并保存为脚本变量,后续输入、判断、定位和日志节点可以使用 `{{变量名}}` 引用。
354
+
355
+ ### 停止 APP
356
+
357
+ 停止指定包名的 App 进程,不卸载应用,也不清除登录信息和本地数据。节点中可以修改目标包名。
358
+
359
+ ### 启动相册
360
+
361
+ 打开设备可用的系统相册或常见相册应用,只负责进入相册,不自动选择照片或视频。
362
+
363
+ ### 清除 APP 数据
364
+
365
+ 清除目标应用数据并恢复初始状态,会删除登录信息和本地设置。该操作只允许放在流程开始位置,使用前应确认测试数据可以重置。
366
+
367
+ ### 判断勾选
368
+
369
+ 读取原生 Checkbox、RadioButton 或 Switch 的真实 checked 状态,并按勾选或未勾选进入对应分支。普通图片控件应改用图像判断。
370
+
371
+ ### 文字点击
372
+
373
+ 按指定文字精确或模糊查找原生组件,匹配后点击并进入成功分支,未匹配时进入另一分支。它不识别图片或自绘画面中的文字。
374
+
375
+ ### AI 识别
376
+
377
+ 将设备截图发送给视觉模型,可以执行单帧识别、持续观察或检测命中后提前结束。默认按顺序继续执行;启用分支后根据识别结果进入不同路径。
378
+
379
+ ### 图像判断
380
+
381
+ 使用本地 OpenCV 判断模板、图片勾选状态、黑屏、区域颜色或多帧变化,不调用模型。固定图标和稳定视觉特征适合使用该操作。
382
+
383
+ ### 检测画面变化
384
+
385
+ 从三级菜单分别添加“开始”和“结束”。开始节点保存指定区域的基准截图,结束节点与对应基准比较并在日志和报告中记录变化结果。
386
+
387
+ ### 循环
388
+
389
+ 在最大执行次数范围内重复执行循环体,可以固定次数运行,也可以根据元素出现或消失提前退出。最大次数用于防止脚本无限循环。
390
+
391
+ ### 进入下一次循环
392
+
393
+ 跳过当前轮剩余节点并进入指定的当前或外层循环下一轮。达到最大次数后执行循环结束路径。
394
+
395
+ ### 退出循环
396
+
397
+ 立即结束指定的当前或外层循环,随后执行该循环结束后的流程。
398
+
399
+ ### 终止流程
400
+
401
+ 主动结束整次回放,后续节点和连接脚本不再执行。系统仍会保存本次日志和报告。
402
+
403
+ ### 输出日志
404
+
405
+ 输出自定义内容和日志关键字,支持 `{{变量名}}`。适合标记测试阶段、记录读取值或为报告提供检索标识。
406
+
293
407
  ### 录制点击
294
408
 
295
409
  用途:点击某个组件。
@@ -897,9 +1011,9 @@ Home 键
897
1011
 
898
1012
  校验规则:
899
1013
 
900
- - 主流程中连接脚本:插入点 Activity 优先使用前一步录制的 `pageAfter.activity`,只允许选择入口 Activity 与插入点一致的脚本。
901
- - 判断分支中连接脚本:允许选择同一 App 的其他 Activity 脚本,适合登录成功后进入首页等跳转路径。
902
- - 分支连接在回放时会等待手机进入目标脚本的入口 Activity;页面没有真正跳转时会超时失败,不会提前执行其他页面的步骤。
1014
+ - 主流程和判断分支均可选择同一 App 的其他已保存脚本,不要求入口 Activity 与插入点一致。
1015
+ - 目标脚本已绑定入口 Activity 时,回放会等待手机进入该 Activity;页面没有真正跳转时会超时失败。
1016
+ - 目标脚本未绑定入口 Activity 时,直接从当前画面执行。连接脚本不会自动切换手机页面,需在前序步骤中完成点击、返回等操作。
903
1017
  - 目标脚本必须属于当前 App,且不能连接当前脚本自身。
904
1018
  - 脚本互相连接形成循环时,回放会终止并提示循环连接。
905
1019
 
@@ -933,6 +1047,19 @@ Home 键
933
1047
  - 子脚本单独回放时仍会正常执行自己的“启动 App”节点。
934
1048
  - 如果脚本互相连接形成循环,会终止并提示循环连接。
935
1049
 
1050
+ ### 回放报告总结
1051
+
1052
+ 在“参数配置 → Appium配置 → 回放报告总结”中配置,默认关闭,与截图报告独立。
1053
+
1054
+ 1. 在上方“提示词优化模型”填写 Base URL、API Key、Model Name,并点击“测试并保存”。该文本模型同时用于优化提示词和生成回放报告总结。
1055
+ 2. 开启“回放报告总结”。开关和报告提示词修改后自动保存。
1056
+ 3. 在回放按钮的设置中选择报告模板;不选择时使用默认测试报告。默认格式包括测试概览(脚本名称、起止时间、结果、总时长)、执行配置及节点参数、用例描述表格、失败详情和汇总,也可以选择出图时间分析或自定义模板。
1057
+ 4. 回放结束后将脱敏日志、主脚本与连接脚本配置发送给总结模型,生成的 Markdown 使用原回放报告路径,保存在配置的报告输出目录(默认 `output`);完成日志会显示报告路径。
1058
+
1059
+ 开启报告总结后,工具会在回放前验证提示词优化模型。模型不可用时,开关旁会提示“模型不可用,使用默认模板”,回放仍会继续并生成基础报告。总结在设备操作结束后生成,页面会显示生成进度,总结耗时不计入测试总时长。关闭时继续生成原基础报告且不调用总结模型;请求失败、超时或返回空内容时保留基础报告和原始日志,并输出失败原因,不改变测试结果。模型请求最多等待 60 秒。
1060
+
1061
+ 报告不会向模型发送截图和图片模板。为控制输入大小,脚本配置最多保留约 50000 字符,日志最多保留约 100000 字符;超限时保留头尾并标注中段省略,生成报告也会注明信息不完整。模型的原因分析属于辅助判断,应结合原始日志核实。
1062
+
936
1063
  ## 脚本保存与管理
937
1064
 
938
1065
  ### 保存和更新
@@ -940,7 +1067,7 @@ Home 键
940
1067
  - 新脚本需要填写名称并选择预设 App 后保存。
941
1068
  - 从顶部脚本下拉框或“脚本列表”加载历史脚本后,再次点击“保存”会更新原脚本,不会因为名称相同而报错。
942
1069
  - 脚本数据只保存流程图 `flow_json`,其中包含主流程、判断分支、连接关系、节点备注和页面检查点。
943
- - 已保存脚本加载后会校验当前 Activity;页面不匹配时按“单 Activity 录制规则”锁定编辑。
1070
+ - 已保存脚本加载后仍可直接查看和编辑;当前 Activity 不影响脚本编辑。
944
1071
 
945
1072
  ### 脚本列表
946
1073
 
@@ -1161,16 +1288,16 @@ Linux: ~/.local/share/android-midscene-automation
1161
1288
 
1162
1289
  推荐:
1163
1290
 
1164
- - 使用“断言存在”判断下个页面核心元素。
1165
- - 多 Activity 页面可以拆成两个脚本,在判断分支中用“连接脚本”串起来;回放会等待目标 Activity 出现。
1291
+ - 使用“等待出现”判断下个页面核心元素,或添加“等待 Activity”明确目标页面。
1292
+ - 检测到 Activity 变化时可以选择继续在当前脚本录制;也可以拆成两个脚本,再用“连接脚本”串联。
1166
1293
  - 单 Activity App 不要依赖 Activity 变化,应使用页面核心元素判断是否已进入目标状态。
1167
1294
 
1168
- ## 为什么连接脚本提示入口 Activity 不匹配
1295
+ ## 连接脚本后为什么没有进入目标页面
1169
1296
 
1170
- - 主流程连接要求目标脚本入口 Activity 与当前插入点一致,适合复用同一页面上的公共流程。
1171
- - 登录后进入首页等跨 Activity 场景,应先添加“判断存在”,再在“是”或“否”分支中添加“连接脚本”。
1172
- - 分支连接允许选择同一 App 的其他 Activity,但不会主动跳转页面;前面的点击或输入必须真正触发跳转。
1173
- - 如果页面未跳转,回放会在等待目标 Activity 超时后失败,这是为了防止在错误页面执行目标脚本。
1297
+ - 连接脚本列表会显示同一 App 的其他已保存脚本,不受入口 Activity 限制。
1298
+ - 连接操作本身不会点击页面或主动跳转,前面的点击、输入或启动 App 必须先让设备进入目标脚本需要的页面。
1299
+ - 目标脚本绑定了入口 Activity 时,回放会等待该 Activity;未绑定时会直接从当前画面执行。
1300
+ - 跨页面连接前建议添加“等待 Activity”或“等待出现”,便于确认页面已经切换完成。
1174
1301
 
1175
1302
  ## 为什么在桌面点击 App 后回放无效
1176
1303
 
@@ -1197,7 +1324,7 @@ Linux: ~/.local/share/android-midscene-automation
1197
1324
  如果 App 是单 Activity 架构,优先用:
1198
1325
 
1199
1326
  ```text
1200
- 断言存在 页面核心元素
1327
+ 等待出现 页面核心元素
1201
1328
  ```
1202
1329
 
1203
1330
  ## 什么时候用可选步骤
@@ -1231,9 +1358,19 @@ Linux: ~/.local/share/android-midscene-automation
1231
1358
 
1232
1359
  # 项目更新记录
1233
1360
 
1361
+ ## v0.2.1
1362
+ - 侧边栏底部新增“帮助与版本”入口。
1363
+ - AI 识别预设提示词支持编辑测试场景名称和提示词内容,默认预设与自定义预设均可修改并自动保存。
1364
+ - AI 识别启用分支后可勾选“启用兜底”,再选择进入左侧 true 或右侧 false 分支;适用范围改为问号提示,正常识别结果不受影响。
1365
+
1366
+ ## v0.1.43
1367
+ - 新增独立的“提示词优化模型”,统一用于优化 AI 识别提示词、报告提示词和生成回放测试报告总结;AI 识别与回放报告均可输入简要要求后由模型补充提示词,模型测试成功改为弹窗提示。
1368
+ - 回放设置新增报告模板选择,不选择时使用默认测试报告;开启报告总结后会在回放前检测提示词优化模型,模型不可用时弹窗提示并继续生成默认格式的基础报告。总结请求失败、超时或返回空内容时同样保留基础报告,不改变测试结果。
1369
+ - 回放输出弹窗支持同时拖拽调整宽度和高度,并保留最小尺寸与窗口边界限制。
1370
+ - 连接脚本列表显示同一 App 的其他已保存脚本,不再因入口 Activity 不同或未绑定而隐藏;选择时说明等待入口页面或直接从当前画面执行的行为。
1371
+ - 点击坐标操作新增“获取点击坐标”,直接在左侧设备预览点击画面即可填入实际 X、Y 坐标,无需弹窗,支持取消,取点不会执行设备操作。
1372
+
1234
1373
  ## v0.1.42
1235
- - 新增 AI 识别详细使用说明,涵盖预设场景、单帧与持续观察、提前结束、分支、去重、超时及开屏广告等配置示例。
1236
- - 预设提示词新增测试场景名称,管理列表同时展示场景和内容;AI 识别按场景名称选择。
1237
1374
  - 修复回放终止或会话创建失败后设备端 UiAutomator2 残留,导致组件树刷新失败的问题。
1238
1375
  - AI 持续观察新增“检测命中后提前结束”模式,边采样去重边识别,命中后停止观察并继续流程或进入“是”分支;到达最长观察时间后完成剩余关键帧判断,未命中走原顺序或“否”分支,支持命中条件校验、取消和逐次模型请求超时。
1239
1376
  - Appium 参数的AI 识别配置,新增持续观察选项,支持 pixelmatch(默认)、OpenCV(SSIM)和不去重选项。
package/USAGE.md CHANGED
@@ -4,9 +4,54 @@
4
4
 
5
5
  本文档随 npm 包发布,也可以从 README 的“功能文档”直接打开。常见问题请查看 [FAQ.md](./FAQ.md)。
6
6
 
7
- AI 识别的场景选择、预设提示词、持续观察、分支、截图去重及超时配置,请查看 [AI 识别详细使用说明](./AI_RECOGNITION_GUIDE.md)。
7
+ Appium 各流程节点的用途和配置方式,请查看 [流程节点操作说明](./流程节点操作说明.md)。AI 识别的场景选择、预设提示词、持续观察、分支、截图去重及超时配置,请查看 [AI 识别操作说明](./AI识别操作说明.md)。
8
8
 
9
- ## 使用前准备
9
+ ## 页面内帮助
10
+
11
+ 点击侧边栏最下方的问号图标,可以在“帮助与版本”窗口中查看:
12
+
13
+ - 当前工具版本和对应的 Midscene 版本。
14
+ - Vue、Element Plus、Appium、Midscene、Vue Flow、OpenCV.js、Vite 和 TypeScript 等主要技术及版本。
15
+ - 本操作说明。
16
+ - 常见问题和各版本更新记录。
17
+
18
+ Midscene 操作说明读取 `USAGE.md`,Appium 操作说明读取 `流程节点操作说明.md`,AI 识别操作说明读取 `AI识别操作说明.md`,常见问题和更新记录分别读取 `FAQ.md` 与 `CHANGELOG.md`;文档更新后页面内容会同步更新。
19
+
20
+ ## Midscene 操作说明
21
+
22
+ ### 配置 Midscene 模型
23
+
24
+ 进入“参数配置”,在 Midscene 模型区域选择模型提供方并填写所需参数。Midscene 执行依赖支持图片输入的多模态模型,填写后应先点击测试,确认模型可以正常响应。
25
+
26
+ ### 生成测试脚本
27
+
28
+ 进入“Midscene > 测试脚本生成”,填写脚本名称、场景标题、目标 App 和测试需求,也可以上传用例文件。点击“AI 生成”后检查生成步骤,确保测试数据、操作目标和成功条件准确。
29
+
30
+ ### 自定义步骤编排
31
+
32
+ 在测试脚本生成页面切换到“自定义步骤编排”,可以逐条添加操作、等待、查询和判断步骤。每条指令只描述一个明确目标,页面跳转后使用可观察的页面元素作为等待条件。
33
+
34
+ ### 编辑并保存脚本
35
+
36
+ 生成脚本后可以查看、编辑或复制代码。确认内容后点击“保存脚本”,保存的脚本会出现在“自动化测试”页面中。
37
+
38
+ ### 执行 Midscene 脚本
39
+
40
+ 进入“Midscene > 自动化测试”,选择脚本和 Android 设备后点击“开始执行”。执行期间保持设备亮屏和解锁,Midscene 会根据当前画面理解目标并执行操作。
41
+
42
+ ### 停止执行
43
+
44
+ 脚本运行期间点击顶部“停止执行”可以中止后续步骤。停止后查看执行输出,确认最后完成的步骤和停止位置。
45
+
46
+ ### 查看执行输出
47
+
48
+ 自动化测试页面会实时显示步骤状态、耗时和错误信息。执行失败时先检查模型配置、ADB 连接、设备画面以及指令中的目标描述是否清晰。
49
+
50
+ ### Midscene 编写建议
51
+
52
+ 一条指令只完成一个目标;输入、点击和等待尽量拆分;等待条件应描述页面中可观察到的稳定元素;不要在同一步骤中同时处理大量弹窗和业务操作。组件 ID 稳定并需要大量重复回放时,可以改用 Appium 流程。
53
+
54
+ ## Appium 使用前准备
10
55
 
11
56
  ### 1. 连接 Android 设备
12
57
 
@@ -115,14 +160,81 @@ App 包名是必填项,只能从“参数配置”中已经添加的“预设
115
160
  - 点击节点右上角删除按钮可移除节点,流程引用会自动修正。
116
161
  - 浏览器刷新后会保持当前功能页面和 Appium 工作区 Tab;存在未保存修改时,刷新或关闭页面前会提示保存。
117
162
 
118
- ### 单 Activity 录制规则
163
+ ### 跨 Activity 录制
119
164
 
120
- 每个脚本只允许录制一个 Activity。录制点击后如果检测到 Activity 发生变化,会强制弹出保存提示;保存完成后清空当前流程,并从新 Activity 开始录制新脚本。
165
+ 录制点击后如果检测到 Activity 发生变化,系统会提示选择继续录制或结束并保存。需要把多个页面保存在同一脚本时选择继续,等待新页面组件树刷新后即可录制后续操作;希望按页面拆分时,结束当前脚本并在新 Activity 中创建脚本,再通过“连接脚本”组合流程。
121
166
 
122
- 加载历史脚本时,如果手机当前 Activity 与脚本入口 Activity 不一致,流程编辑会锁定。此时可执行脚本中的“启动 APP”节点,或插入“系统返回”使页面回到脚本绑定的 Activity;Activity 匹配后自动解除锁定。
167
+ 跨页面流程建议在跳转操作后添加“等待 Activity”,明确目标页面并提高回放稳定性。加载历史脚本时,即使手机当前 Activity 与脚本入口 Activity 不同,也可以查看和编辑流程;回放前应确认启动节点、等待 Activity 或前置操作能把设备带到正确页面。
123
168
 
124
169
  ## 操作说明
125
170
 
171
+ ### 当前操作分类
172
+
173
+ - 组件操作:录制点击、录制输入、提取变量、清空输入、长按、存在则点击、存在则输入、存在则清空。
174
+ - 设备操作:点击坐标、系统返回、Home 键、最近任务、电源键、滑动、双指缩放、启动 APP、停止 APP、启动相册、清除 APP 数据。
175
+ - 判断与等待:判断存在、判断勾选、文字点击、AI 识别、图像判断、等待出现、等待消失、等待 Activity。
176
+ - 检测画面变化:在三级菜单中分别添加“开始”和“结束”,对比指定区域前后的画面。
177
+ - 循环:添加有界循环、进入下一次循环和退出循环。进入下一次循环会跳过本轮剩余节点;退出循环会执行目标循环结束后的流程。
178
+ - 流程控制:连接脚本、终止流程、输出日志和添加延时。
179
+
180
+ 旧脚本中的“断言存在”和“断言文本”仍可回放,但新流程统一使用当前菜单中的判断与等待操作。
181
+
182
+ ### 提取变量
183
+
184
+ 读取选中组件的文字或属性并保存为脚本变量,后续输入、判断、定位和日志节点可以使用 `{{变量名}}` 引用。
185
+
186
+ ### 停止 APP
187
+
188
+ 停止指定包名的 App 进程,不卸载应用,也不清除登录信息和本地数据。节点中可以修改目标包名。
189
+
190
+ ### 启动相册
191
+
192
+ 打开设备可用的系统相册或常见相册应用,只负责进入相册,不自动选择照片或视频。
193
+
194
+ ### 清除 APP 数据
195
+
196
+ 清除目标应用数据并恢复初始状态,会删除登录信息和本地设置。该操作只允许放在流程开始位置,使用前应确认测试数据可以重置。
197
+
198
+ ### 判断勾选
199
+
200
+ 读取原生 Checkbox、RadioButton 或 Switch 的真实 checked 状态,并按勾选或未勾选进入对应分支。普通图片控件应改用图像判断。
201
+
202
+ ### 文字点击
203
+
204
+ 按指定文字精确或模糊查找原生组件,匹配后点击并进入成功分支,未匹配时进入另一分支。它不识别图片或自绘画面中的文字。
205
+
206
+ ### AI 识别
207
+
208
+ 将设备截图发送给视觉模型,可以执行单帧识别、持续观察或检测命中后提前结束。默认按顺序继续执行;启用分支后根据识别结果进入不同路径。
209
+
210
+ ### 图像判断
211
+
212
+ 使用本地 OpenCV 判断模板、图片勾选状态、黑屏、区域颜色或多帧变化,不调用模型。固定图标和稳定视觉特征适合使用该操作。
213
+
214
+ ### 检测画面变化
215
+
216
+ 从三级菜单分别添加“开始”和“结束”。开始节点保存指定区域的基准截图,结束节点与对应基准比较并在日志和报告中记录变化结果。
217
+
218
+ ### 循环
219
+
220
+ 在最大执行次数范围内重复执行循环体,可以固定次数运行,也可以根据元素出现或消失提前退出。最大次数用于防止脚本无限循环。
221
+
222
+ ### 进入下一次循环
223
+
224
+ 跳过当前轮剩余节点并进入指定的当前或外层循环下一轮。达到最大次数后执行循环结束路径。
225
+
226
+ ### 退出循环
227
+
228
+ 立即结束指定的当前或外层循环,随后执行该循环结束后的流程。
229
+
230
+ ### 终止流程
231
+
232
+ 主动结束整次回放,后续节点和连接脚本不再执行。系统仍会保存本次日志和报告。
233
+
234
+ ### 输出日志
235
+
236
+ 输出自定义内容和日志关键字,支持 `{{变量名}}`。适合标记测试阶段、记录读取值或为报告提供检索标识。
237
+
126
238
  ### 录制点击
127
239
 
128
240
  用途:点击某个组件。
@@ -730,9 +842,9 @@ Home 键
730
842
 
731
843
  校验规则:
732
844
 
733
- - 主流程中连接脚本:插入点 Activity 优先使用前一步录制的 `pageAfter.activity`,只允许选择入口 Activity 与插入点一致的脚本。
734
- - 判断分支中连接脚本:允许选择同一 App 的其他 Activity 脚本,适合登录成功后进入首页等跳转路径。
735
- - 分支连接在回放时会等待手机进入目标脚本的入口 Activity;页面没有真正跳转时会超时失败,不会提前执行其他页面的步骤。
845
+ - 主流程和判断分支均可选择同一 App 的其他已保存脚本,不要求入口 Activity 与插入点一致。
846
+ - 目标脚本已绑定入口 Activity 时,回放会等待手机进入该 Activity;页面没有真正跳转时会超时失败。
847
+ - 目标脚本未绑定入口 Activity 时,直接从当前画面执行。连接脚本不会自动切换手机页面,需在前序步骤中完成点击、返回等操作。
736
848
  - 目标脚本必须属于当前 App,且不能连接当前脚本自身。
737
849
  - 脚本互相连接形成循环时,回放会终止并提示循环连接。
738
850
 
@@ -766,6 +878,19 @@ Home 键
766
878
  - 子脚本单独回放时仍会正常执行自己的“启动 App”节点。
767
879
  - 如果脚本互相连接形成循环,会终止并提示循环连接。
768
880
 
881
+ ### 回放报告总结
882
+
883
+ 在“参数配置 → Appium配置 → 回放报告总结”中配置,默认关闭,与截图报告独立。
884
+
885
+ 1. 在上方“提示词优化模型”填写 Base URL、API Key、Model Name,并点击“测试并保存”。该文本模型同时用于优化提示词和生成回放报告总结。
886
+ 2. 开启“回放报告总结”。开关和报告提示词修改后自动保存。
887
+ 3. 在回放按钮的设置中选择报告模板;不选择时使用默认测试报告。默认格式包括测试概览(脚本名称、起止时间、结果、总时长)、执行配置及节点参数、用例描述表格、失败详情和汇总,也可以选择出图时间分析或自定义模板。
888
+ 4. 回放结束后将脱敏日志、主脚本与连接脚本配置发送给总结模型,生成的 Markdown 使用原回放报告路径,保存在配置的报告输出目录(默认 `output`);完成日志会显示报告路径。
889
+
890
+ 开启报告总结后,工具会在回放前验证提示词优化模型。模型不可用时,开关旁会提示“模型不可用,使用默认模板”,回放仍会继续并生成基础报告。总结在设备操作结束后生成,页面会显示生成进度,总结耗时不计入测试总时长。关闭时继续生成原基础报告且不调用总结模型;请求失败、超时或返回空内容时保留基础报告和原始日志,并输出失败原因,不改变测试结果。模型请求最多等待 60 秒。
891
+
892
+ 报告不会向模型发送截图和图片模板。为控制输入大小,脚本配置最多保留约 50000 字符,日志最多保留约 100000 字符;超限时保留头尾并标注中段省略,生成报告也会注明信息不完整。模型的原因分析属于辅助判断,应结合原始日志核实。
893
+
769
894
  ## 脚本保存与管理
770
895
 
771
896
  ### 保存和更新
@@ -773,7 +898,7 @@ Home 键
773
898
  - 新脚本需要填写名称并选择预设 App 后保存。
774
899
  - 从顶部脚本下拉框或“脚本列表”加载历史脚本后,再次点击“保存”会更新原脚本,不会因为名称相同而报错。
775
900
  - 脚本数据只保存流程图 `flow_json`,其中包含主流程、判断分支、连接关系、节点备注和页面检查点。
776
- - 已保存脚本加载后会校验当前 Activity;页面不匹配时按“单 Activity 录制规则”锁定编辑。
901
+ - 已保存脚本加载后仍可直接查看和编辑;当前 Activity 不影响脚本编辑。
777
902
 
778
903
  ### 脚本列表
779
904
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.1.42",
3
+ "version": "0.2.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oooooooko/android-midscene-automation.git"
@@ -23,7 +23,8 @@
23
23
  "vite.config.ts",
24
24
  "README.md",
25
25
  "USAGE.md",
26
- "AI_RECOGNITION_GUIDE.md",
26
+ "流程节点操作说明.md",
27
+ "AI识别操作说明.md",
27
28
  "FAQ.md",
28
29
  "CHANGELOG.md"
29
30
  ],
@@ -1,3 +1,4 @@
1
+ import { resolveReportSummary } from '../src/appium-recorder/report-summary';
1
2
  // Run: npx tsx --test server/appium-auto-save.test.ts
2
3
  import assert from 'node:assert/strict';
3
4
  import { test } from 'node:test';
@@ -8,7 +9,7 @@ import type { ConfigForm } from '../src/types';
8
9
 
9
10
  test('loads quietly, serializes rapid edits, validates colors and surfaces save errors', async () => {
10
11
  const scope = effectScope();
11
- const form = reactive({ appium: { model: { baseUrl: '', apiKey: '', name: '' } } } as ConfigForm);
12
+ const form = reactive({ appium: { reportSummary: resolveReportSummary(), model: { baseUrl: '', apiKey: '', name: '' } } } as ConfigForm);
12
13
  const submissions: Omit<ConfigForm['appium'], 'model'>[] = [];
13
14
  const errors: unknown[] = [];
14
15
  let release: (() => void) | undefined;
@@ -29,7 +29,7 @@ test('Appium autosave is isolated from manual config, persists and rejects inval
29
29
  if (body.model === 'slow') {
30
30
  await new Promise<void>(resolve => { releaseModel = resolve; modelStarted?.(); });
31
31
  }
32
- res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'OK' } }] }));
32
+ res.end(JSON.stringify({ choices: [{ finish_reason: 'stop', message: { role: 'assistant', content: 'OK' } }] }));
33
33
  });
34
34
  try {
35
35
  await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve));
@@ -73,6 +73,52 @@ test('Appium autosave is isolated from manual config, persists and rejects inval
73
73
  if (other !== key) assert.deepEqual(after[other], before[other]);
74
74
  }
75
75
  }
76
+ assert.equal((await post('/api/appium-recorder/prompts/optimize', {
77
+ kind: 'aiRecognition', prompt: '检查有没有广告', condition: true,
78
+ })).status, 500, 'prompt optimization requires its own tested model');
79
+ const unavailableSummaryModel = await post('/api/appium-recorder/report-summary/check', {});
80
+ assert.equal(unavailableSummaryModel.status, 200);
81
+ const unavailableSummaryStatus = await unavailableSummaryModel.json() as { available: boolean; message?: string };
82
+ assert.equal(unavailableSummaryStatus.available, false);
83
+ assert.equal(unavailableSummaryStatus.message, '模型配置不完整');
84
+ const promptOptimizerBefore = await (await fetch(base + '/api/config')).json() as AppConfig;
85
+ assert.equal((await post('/api/test-model', { modelKey: 'promptOptimizer', model: { ...model, name: 'fail' }, save: true })).status, 500);
86
+ assert.deepEqual(await (await fetch(base + '/api/config')).json(), promptOptimizerBefore);
87
+ assert.equal((await post('/api/test-model', { modelKey: 'promptOptimizer', model, save: true })).status, 200);
88
+ stored = await (await fetch(base + '/api/config')).json() as AppConfig;
89
+ assert.equal(stored.appium.promptOptimizer?.model.name, 'pass');
90
+ const optimizedRecognition = await post('/api/appium-recorder/prompts/optimize', {
91
+ kind: 'aiRecognition', prompt: '检查有没有广告', condition: true,
92
+ });
93
+ assert.equal(optimizedRecognition.status, 200);
94
+ assert.deepEqual(await optimizedRecognition.json(), { prompt: 'OK' });
95
+ const optimizedSummary = await post('/api/appium-recorder/prompts/optimize', {
96
+ kind: 'reportSummary', prompt: '总结出图时间',
97
+ });
98
+ assert.equal(optimizedSummary.status, 200);
99
+ assert.deepEqual(await optimizedSummary.json(), { prompt: 'OK' });
100
+ // Summary settings auto-save independently and reuse the prompt optimizer model.
101
+ const summaryBefore = await (await fetch(base + '/api/config')).json() as AppConfig;
102
+ assert.equal(summaryBefore.appium.reportSummary?.enabled, false);
103
+ const customReportPreset = { name: '支付流程', prompt: '输出支付流程的步骤和结果' };
104
+ assert.equal((await post('/api/config/appium', { appium: { reportSummary: {
105
+ enabled: true, prompt: '输出用例表格', customPresets: [customReportPreset], model,
106
+ } } })).status, 200);
107
+ let summaryConfig = await (await fetch(base + '/api/config')).json() as AppConfig;
108
+ assert.equal(summaryConfig.appium.reportSummary?.prompt, '输出用例表格');
109
+ assert.deepEqual(summaryConfig.appium.reportSummary?.customPresets, [customReportPreset]);
110
+ assert.equal('model' in summaryConfig.appium.reportSummary!, false);
111
+ assert.equal(summaryConfig.appium.promptOptimizer?.model.name, 'pass');
112
+ const summaryModelStatus = await post('/api/appium-recorder/report-summary/check', {});
113
+ assert.equal(summaryModelStatus.status, 200);
114
+ assert.deepEqual(await summaryModelStatus.json(), { enabled: true, available: true });
115
+ await post('/api/config/appium', { appium: { reportSummary: { enabled: false, prompt: '恢复测试格式', customPresets: [customReportPreset], model: { name: 'stale' } } } });
116
+ summaryConfig = await (await fetch(base + '/api/config')).json() as AppConfig;
117
+ assert.equal('model' in summaryConfig.appium.reportSummary!, false);
118
+ assert.deepEqual(summaryConfig.appium.reportSummary?.customPresets, [customReportPreset]);
119
+ assert.equal(summaryConfig.appium.promptOptimizer?.model.name, 'pass');
120
+ assert.deepEqual(summaryConfig.appium.model, summaryBefore.appium.model);
121
+ assert.equal((await post('/api/config/appium', { appium: { reportSummary: { prompt: '' } } })).status, 400);
76
122
  // A slow test must not revert an appearance edit saved while it was running.
77
123
  const started = new Promise<void>(resolve => { modelStarted = resolve; });
78
124
  const slowTest = post('/api/test-model', { modelKey: 'appium', model: { ...model, name: 'slow' }, save: true });
@@ -85,6 +131,7 @@ test('Appium autosave is isolated from manual config, persists and rejects inval
85
131
  await post('/api/config', { runtime: initial.runtime });
86
132
  stored = await (await fetch(base + '/api/config')).json() as AppConfig;
87
133
  assert.equal(stored.appium.model.name, 'slow');
134
+ assert.equal(stored.appium.promptOptimizer?.model.name, 'pass');
88
135
  assert.equal(stored.midscene.model.name, 'pass');
89
136
  assert.equal(stored.appium.flowLineColor, '#123456');
90
137
  assert.equal(loadModelConfigFromDb()?.appium?.model?.name, 'slow');
@@ -0,0 +1,27 @@
1
+ // Run: npx tsx --test server/appium-recorder/ai-recognition.test.ts
2
+ import assert from 'node:assert/strict';
3
+ import { test } from 'node:test';
4
+ import {
5
+ AiInvalidResultError,
6
+ parseAiRecognitionResult,
7
+ resolveAiInvalidResultFallback,
8
+ } from './ai-recognition';
9
+
10
+ test('valid AI boolean results keep their original branch', () => {
11
+ assert.deepEqual(parseAiRecognitionResult('{"result":true,"reason":"已出图"}'), { result: true, reason: '已出图' });
12
+ assert.deepEqual(parseAiRecognitionResult('{"result":false,"reason":"仍是黑屏"}'), { result: false, reason: '仍是黑屏' });
13
+ });
14
+
15
+ test('empty, invalid and indeterminate AI answers are classified as invalid results', () => {
16
+ for (const content of ['', 'not json', '{"result":"true"}', '{"result":null,"reason":"画面模糊"}']) {
17
+ assert.throws(() => parseAiRecognitionResult(content), AiInvalidResultError);
18
+ }
19
+ });
20
+
21
+ test('configured fallback applies only to invalid AI results', () => {
22
+ const invalid = new AiInvalidResultError('没有 true/false');
23
+ assert.equal(resolveAiInvalidResultFallback(invalid, 'yes'), true);
24
+ assert.equal(resolveAiInvalidResultFallback(invalid, 'no'), false);
25
+ assert.equal(resolveAiInvalidResultFallback(invalid), null);
26
+ assert.equal(resolveAiInvalidResultFallback(new Error('network failed'), 'yes'), null);
27
+ });
@@ -17,16 +17,28 @@ export function aiAnswerText(content: string | null | undefined) {
17
17
  return (content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().replace(/^```(?:json)?\s*([\s\S]*?)\s*```$/i, '$1').trim();
18
18
  }
19
19
 
20
+ export class AiInvalidResultError extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = 'AiInvalidResultError';
24
+ }
25
+ }
26
+
27
+ export function resolveAiInvalidResultFallback(error: unknown, branch?: 'yes' | 'no'): boolean | null {
28
+ if (!(error instanceof AiInvalidResultError) || (branch !== 'yes' && branch !== 'no')) return null;
29
+ return branch === 'yes';
30
+ }
31
+
20
32
  export function parseAiRecognitionResult(content: string | null | undefined): { result: boolean; reason: string } {
21
33
  // 只接受 JSON 布尔值;字符串 "false"、不确定或格式错误不能误走 false 分支。
22
34
  const text = aiAnswerText(content);
23
35
  let value: unknown;
24
- try { value = JSON.parse(text); } catch { throw new Error('AI 识别未返回有效 JSON 结果'); }
36
+ try { value = JSON.parse(text); } catch { throw new AiInvalidResultError('AI 识别未返回有效 JSON 结果'); }
25
37
  if (!value || typeof value !== 'object' || !('result' in value) || typeof value.result !== 'boolean') {
26
38
  if (value && typeof value === 'object' && 'result' in value && value.result === null) {
27
- throw new Error(`AI 无法判断,无法进入分支:${'reason' in value && typeof value.reason === 'string' ? value.reason.slice(0, 2000) : '未返回确定的判断结果'}`);
39
+ throw new AiInvalidResultError(`AI 无法判断:${'reason' in value && typeof value.reason === 'string' ? value.reason.slice(0, 2000) : '未返回确定的判断结果'}`);
28
40
  }
29
- throw new Error('AI 识别结果必须包含布尔值 result(true/false)');
41
+ throw new AiInvalidResultError('AI 识别结果必须包含布尔值 result(true/false)');
30
42
  }
31
43
  const reason = 'reason' in value && typeof value.reason === 'string' ? value.reason.trim() : '';
32
44
  return { result: value.result, reason: reason.slice(0, 2000) };
@@ -96,7 +108,7 @@ export async function recognizeDeviceScreen(input: {
96
108
  // 先记录正文再解析,格式错误或被截断的回答也可排查;不记录请求与鉴权信息。
97
109
  const content = (choice?.message.content || '').split(model.apiKey).join('[REDACTED]');
98
110
  input.onModelOutput?.(content || '(空响应)');
99
- if (choice?.finish_reason !== 'stop') throw new Error('AI 识别响应未完整返回');
111
+ if (choice?.finish_reason !== 'stop') throw new AiInvalidResultError('AI 识别响应未完整返回');
100
112
  if (input.aiBranchEnabled === false && observationConfig?.mode !== 'untilMatch') {
101
113
  const reason = aiAnswerText(content);
102
114
  if (!reason) throw new Error('AI 识别未返回回答内容');