android-midscene-automation 0.1.22 → 0.1.24
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 +10 -2
- package/FAQ.md +91 -0
- package/README.md +33 -23
- package/USAGE.md +2 -90
- package/package.json +2 -1
- package/server/config.ts +71 -1
- package/server/http-api.ts +2 -2
- package/src/pages/ConfigPage.vue +42 -31
- package/src/style.css +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## v0.1.24
|
|
4
|
+
|
|
5
|
+
- 参数配置的运行配置新增保存校验,Android SDK 路径必须包含 `platform-tools/adb`,回放报告目录必须填写已存在且可写入的绝对路径。
|
|
6
|
+
- Midscene 自定义提供方的 `Model Name` 和 `Model Family` 改为输入框,方便填写官方支持列表之外的模型名称和模型系列。
|
|
7
|
+
|
|
8
|
+
## v0.1.23
|
|
9
|
+
|
|
10
|
+
- 常见问题拆分为独立 `FAQ.md` 文档,并在 README 的“功能文档”中增加跳转入口。
|
|
11
|
+
|
|
3
12
|
## v0.1.22
|
|
4
13
|
|
|
5
|
-
- 升级 Midscene 相关依赖到 `v1.12.0
|
|
6
|
-
- 跟进 Midscene `v1.12.0` 的 Test Runner Beta、报告 wall/model call time 汇总、`deepseek-v4-flash-vision-exp` 支持,以及 Android ASAR 外部二进制路径修复。
|
|
14
|
+
- 升级 Midscene 相关依赖到 `v1.12.0`
|
|
7
15
|
|
|
8
16
|
## v0.1.21
|
|
9
17
|
|
package/FAQ.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# 常见问题
|
|
2
|
+
|
|
3
|
+
## 同一个 id 定位到错误输入框怎么办
|
|
4
|
+
|
|
5
|
+
如果账号框和密码框都是 `id/tg_edit`,只按 id 回放可能输入到第一个输入框。
|
|
6
|
+
|
|
7
|
+
建议:
|
|
8
|
+
|
|
9
|
+
- 直接分别选择账号框和密码框录制输入。
|
|
10
|
+
- 在节点详情里确认 selector 是否显示“重复 N”,以及“推荐定位”是否显示父级 + 子级。
|
|
11
|
+
- 如果推荐定位不准确,展开录制步骤,手动修改父级上下文 selector。
|
|
12
|
+
- 坐标点击只作为最后兜底。
|
|
13
|
+
|
|
14
|
+
## 点击后没有检测到页面跳转怎么办
|
|
15
|
+
|
|
16
|
+
可能原因:
|
|
17
|
+
|
|
18
|
+
- 按钮当前禁用。
|
|
19
|
+
- 账号密码不正确。
|
|
20
|
+
- App 使用单 Activity,Activity 不变化。
|
|
21
|
+
- 页面跳转依赖网络。
|
|
22
|
+
|
|
23
|
+
推荐:
|
|
24
|
+
|
|
25
|
+
- 使用“断言存在”判断下个页面核心元素。
|
|
26
|
+
- 多 Activity 页面可以拆成两个脚本,在判断分支中用“连接脚本”串起来;回放会等待目标 Activity 出现。
|
|
27
|
+
- 单 Activity App 不要依赖 Activity 变化,应使用页面核心元素判断是否已进入目标状态。
|
|
28
|
+
|
|
29
|
+
## 为什么连接脚本提示入口 Activity 不匹配
|
|
30
|
+
|
|
31
|
+
- 主流程连接要求目标脚本入口 Activity 与当前插入点一致,适合复用同一页面上的公共流程。
|
|
32
|
+
- 登录后进入首页等跨 Activity 场景,应先添加“判断存在”,再在“是”或“否”分支中添加“连接脚本”。
|
|
33
|
+
- 分支连接允许选择同一 App 的其他 Activity,但不会主动跳转页面;前面的点击或输入必须真正触发跳转。
|
|
34
|
+
- 如果页面未跳转,回放会在等待目标 Activity 超时后失败,这是为了防止在错误页面执行目标脚本。
|
|
35
|
+
|
|
36
|
+
## 为什么在桌面点击 App 后回放无效
|
|
37
|
+
|
|
38
|
+
部分厂商系统会限制桌面或启动器页面的控件信息,Appium 可能无法获取桌面图标的稳定 id。此时在桌面录制“点击 App 图标”可能无法回放。
|
|
39
|
+
|
|
40
|
+
建议在开始节点后添加“启动 App”操作,由系统按包名打开目标 App,替代桌面点击图标。需要重置登录态或缓存时,可以先添加“清理 App 缓存”,再添加“启动 App”。
|
|
41
|
+
|
|
42
|
+
## 弹窗只出现一次怎么办
|
|
43
|
+
|
|
44
|
+
使用“判断存在”。
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
判断存在 确认按钮
|
|
48
|
+
是 -> 点击 确认按钮
|
|
49
|
+
否 -> 不添加操作
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
不要直接把弹窗确认按钮作为普通必选点击,否则弹窗不出现时脚本会失败。
|
|
53
|
+
|
|
54
|
+
## 什么时候用等待 Activity
|
|
55
|
+
|
|
56
|
+
适合多 Activity App。
|
|
57
|
+
|
|
58
|
+
如果 App 是单 Activity 架构,优先用:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
断言存在 页面核心元素
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 什么时候用可选步骤
|
|
65
|
+
|
|
66
|
+
可选步骤适合非主流程阻塞项:
|
|
67
|
+
|
|
68
|
+
- 权限弹窗
|
|
69
|
+
- 协议弹窗
|
|
70
|
+
- 活动弹窗
|
|
71
|
+
- 首次引导
|
|
72
|
+
|
|
73
|
+
不建议把登录按钮、提交按钮、核心断言设置为可选。
|
|
74
|
+
|
|
75
|
+
## 回放时出现 UiAutomation not connected 怎么办
|
|
76
|
+
|
|
77
|
+
组件树刷新和 Appium 回放不能同时占用 UiAutomation。当前版本会在回放期间暂停组件树自动刷新,并在断开时清理残留抓取进程后重试一次。
|
|
78
|
+
|
|
79
|
+
如果仍然失败:
|
|
80
|
+
|
|
81
|
+
1. 确认手机保持解锁,USB 调试授权没有失效。
|
|
82
|
+
2. 确认 Appium 和 UiAutomator2 Driver 已正常启动。
|
|
83
|
+
3. 停止其他正在抓取同一设备组件树的工具。
|
|
84
|
+
4. 重新连接设备后再次回放。
|
|
85
|
+
|
|
86
|
+
## 设备预览或组件树没有更新怎么办
|
|
87
|
+
|
|
88
|
+
1. 先确认设备仍显示在设备下拉框中。
|
|
89
|
+
2. 点击设备预览刷新按钮重新获取画面。
|
|
90
|
+
3. 点击“刷新组件树”重新抓取页面结构。
|
|
91
|
+
4. 回放期间组件树自动刷新会暂停,回放完成后会自动恢复。
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ appium driver install uiautomator2
|
|
|
32
32
|
## 启动
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
npx android-midscene-automation
|
|
35
|
+
npx --yes android-midscene-automation@latest
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
启动后访问:
|
|
@@ -52,6 +52,7 @@ npx android-midscene-automation --port 5174
|
|
|
52
52
|
## 功能文档
|
|
53
53
|
|
|
54
54
|
- [Appium 录制器使用说明](#appium-录制器使用说明)
|
|
55
|
+
- [常见问题](#常见问题)
|
|
55
56
|
- [项目更新记录](#项目更新记录)
|
|
56
57
|
|
|
57
58
|
## 源码开发
|
|
@@ -164,7 +165,7 @@ midscene_run/ # Midscene 执行报告和运行产物
|
|
|
164
165
|
|
|
165
166
|
本文档说明 Appium 组件树录制器的完整使用方式,包括每个操作按钮的用途、录制方法和示例。
|
|
166
167
|
|
|
167
|
-
本文档随 npm 包发布,也可以从 README
|
|
168
|
+
本文档随 npm 包发布,也可以从 README 的“功能文档”直接打开。常见问题请查看 [常见问题](#常见问题)。
|
|
168
169
|
|
|
169
170
|
## 使用前准备
|
|
170
171
|
|
|
@@ -1105,9 +1106,20 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1105
1106
|
- 校验页面返回路径。
|
|
1106
1107
|
- 校验返回后列表仍可见。
|
|
1107
1108
|
|
|
1108
|
-
## 常见问题
|
|
1109
1109
|
|
|
1110
|
-
|
|
1110
|
+
## 推荐录制规范
|
|
1111
|
+
|
|
1112
|
+
- 每个脚本只负责一个清晰场景。
|
|
1113
|
+
- 登录、首页、详情页可以拆成多个脚本,用“连接脚本”组合。
|
|
1114
|
+
- 每次页面跳转后,添加一个“断言存在”或“等待 Activity”。
|
|
1115
|
+
- 弹窗或其他可选组件使用“判断存在”,不要直接写死必点。
|
|
1116
|
+
- 坐标点击只作为兜底。
|
|
1117
|
+
- 给关键节点填写“登录按钮”“账号输入框”等备注,方便查看流程和报告。
|
|
1118
|
+
- 回放失败后优先查看“回放输出”中的失败节点、Activity 和 selector,再打开 `output` 中的 Markdown 报告查看完整配置。
|
|
1119
|
+
|
|
1120
|
+
# 常见问题
|
|
1121
|
+
|
|
1122
|
+
## 同一个 id 定位到错误输入框怎么办
|
|
1111
1123
|
|
|
1112
1124
|
如果账号框和密码框都是 `id/tg_edit`,只按 id 回放可能输入到第一个输入框。
|
|
1113
1125
|
|
|
@@ -1118,7 +1130,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1118
1130
|
- 如果推荐定位不准确,展开录制步骤,手动修改父级上下文 selector。
|
|
1119
1131
|
- 坐标点击只作为最后兜底。
|
|
1120
1132
|
|
|
1121
|
-
|
|
1133
|
+
## 点击后没有检测到页面跳转怎么办
|
|
1122
1134
|
|
|
1123
1135
|
可能原因:
|
|
1124
1136
|
|
|
@@ -1133,20 +1145,20 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1133
1145
|
- 多 Activity 页面可以拆成两个脚本,在判断分支中用“连接脚本”串起来;回放会等待目标 Activity 出现。
|
|
1134
1146
|
- 单 Activity App 不要依赖 Activity 变化,应使用页面核心元素判断是否已进入目标状态。
|
|
1135
1147
|
|
|
1136
|
-
|
|
1148
|
+
## 为什么连接脚本提示入口 Activity 不匹配
|
|
1137
1149
|
|
|
1138
1150
|
- 主流程连接要求目标脚本入口 Activity 与当前插入点一致,适合复用同一页面上的公共流程。
|
|
1139
1151
|
- 登录后进入首页等跨 Activity 场景,应先添加“判断存在”,再在“是”或“否”分支中添加“连接脚本”。
|
|
1140
1152
|
- 分支连接允许选择同一 App 的其他 Activity,但不会主动跳转页面;前面的点击或输入必须真正触发跳转。
|
|
1141
1153
|
- 如果页面未跳转,回放会在等待目标 Activity 超时后失败,这是为了防止在错误页面执行目标脚本。
|
|
1142
1154
|
|
|
1143
|
-
|
|
1155
|
+
## 为什么在桌面点击 App 后回放无效
|
|
1144
1156
|
|
|
1145
1157
|
部分厂商系统会限制桌面或启动器页面的控件信息,Appium 可能无法获取桌面图标的稳定 id。此时在桌面录制“点击 App 图标”可能无法回放。
|
|
1146
1158
|
|
|
1147
1159
|
建议在开始节点后添加“启动 App”操作,由系统按包名打开目标 App,替代桌面点击图标。需要重置登录态或缓存时,可以先添加“清理 App 缓存”,再添加“启动 App”。
|
|
1148
1160
|
|
|
1149
|
-
|
|
1161
|
+
## 弹窗只出现一次怎么办
|
|
1150
1162
|
|
|
1151
1163
|
使用“判断存在”。
|
|
1152
1164
|
|
|
@@ -1158,7 +1170,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1158
1170
|
|
|
1159
1171
|
不要直接把弹窗确认按钮作为普通必选点击,否则弹窗不出现时脚本会失败。
|
|
1160
1172
|
|
|
1161
|
-
|
|
1173
|
+
## 什么时候用等待 Activity
|
|
1162
1174
|
|
|
1163
1175
|
适合多 Activity App。
|
|
1164
1176
|
|
|
@@ -1168,7 +1180,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1168
1180
|
断言存在 页面核心元素
|
|
1169
1181
|
```
|
|
1170
1182
|
|
|
1171
|
-
|
|
1183
|
+
## 什么时候用可选步骤
|
|
1172
1184
|
|
|
1173
1185
|
可选步骤适合非主流程阻塞项:
|
|
1174
1186
|
|
|
@@ -1179,7 +1191,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1179
1191
|
|
|
1180
1192
|
不建议把登录按钮、提交按钮、核心断言设置为可选。
|
|
1181
1193
|
|
|
1182
|
-
|
|
1194
|
+
## 回放时出现 UiAutomation not connected 怎么办
|
|
1183
1195
|
|
|
1184
1196
|
组件树刷新和 Appium 回放不能同时占用 UiAutomation。当前版本会在回放期间暂停组件树自动刷新,并在断开时清理残留抓取进程后重试一次。
|
|
1185
1197
|
|
|
@@ -1190,29 +1202,27 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1190
1202
|
3. 停止其他正在抓取同一设备组件树的工具。
|
|
1191
1203
|
4. 重新连接设备后再次回放。
|
|
1192
1204
|
|
|
1193
|
-
|
|
1205
|
+
## 设备预览或组件树没有更新怎么办
|
|
1194
1206
|
|
|
1195
1207
|
1. 先确认设备仍显示在设备下拉框中。
|
|
1196
1208
|
2. 点击设备预览刷新按钮重新获取画面。
|
|
1197
1209
|
3. 点击“刷新组件树”重新抓取页面结构。
|
|
1198
1210
|
4. 回放期间组件树自动刷新会暂停,回放完成后会自动恢复。
|
|
1199
1211
|
|
|
1200
|
-
|
|
1212
|
+
# 项目更新记录
|
|
1201
1213
|
|
|
1202
|
-
|
|
1203
|
-
- 登录、首页、详情页可以拆成多个脚本,用“连接脚本”组合。
|
|
1204
|
-
- 每次页面跳转后,添加一个“断言存在”或“等待 Activity”。
|
|
1205
|
-
- 弹窗或其他可选组件使用“判断存在”,不要直接写死必点。
|
|
1206
|
-
- 坐标点击只作为兜底。
|
|
1207
|
-
- 给关键节点填写“登录按钮”“账号输入框”等备注,方便查看流程和报告。
|
|
1208
|
-
- 回放失败后优先查看“回放输出”中的失败节点、Activity 和 selector,再打开 `output` 中的 Markdown 报告查看完整配置。
|
|
1214
|
+
## v0.1.24
|
|
1209
1215
|
|
|
1210
|
-
|
|
1216
|
+
- 参数配置的运行配置新增保存校验,Android SDK 路径必须包含 `platform-tools/adb`,回放报告目录必须填写已存在且可写入的绝对路径。
|
|
1217
|
+
- Midscene 自定义提供方的 `Model Name` 和 `Model Family` 改为输入框,方便填写官方支持列表之外的模型名称和模型系列。
|
|
1218
|
+
|
|
1219
|
+
## v0.1.23
|
|
1220
|
+
|
|
1221
|
+
- 常见问题拆分为独立 `FAQ.md` 文档,并在 README 的“功能文档”中增加跳转入口。
|
|
1211
1222
|
|
|
1212
1223
|
## v0.1.22
|
|
1213
1224
|
|
|
1214
|
-
- 升级 Midscene 相关依赖到 `v1.12.0
|
|
1215
|
-
- 跟进 Midscene `v1.12.0` 的 Test Runner Beta、报告 wall/model call time 汇总、`deepseek-v4-flash-vision-exp` 支持,以及 Android ASAR 外部二进制路径修复。
|
|
1225
|
+
- 升级 Midscene 相关依赖到 `v1.12.0`
|
|
1216
1226
|
|
|
1217
1227
|
## v0.1.21
|
|
1218
1228
|
|
package/USAGE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
本文档说明 Appium 组件树录制器的完整使用方式,包括每个操作按钮的用途、录制方法和示例。
|
|
4
4
|
|
|
5
|
-
本文档随 npm 包发布,也可以从 README
|
|
5
|
+
本文档随 npm 包发布,也可以从 README 的“功能文档”直接打开。常见问题请查看 [FAQ.md](./FAQ.md)。
|
|
6
6
|
|
|
7
7
|
## 使用前准备
|
|
8
8
|
|
|
@@ -945,95 +945,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
945
945
|
|
|
946
946
|
## 常见问题
|
|
947
947
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
如果账号框和密码框都是 `id/tg_edit`,只按 id 回放可能输入到第一个输入框。
|
|
951
|
-
|
|
952
|
-
建议:
|
|
953
|
-
|
|
954
|
-
- 直接分别选择账号框和密码框录制输入。
|
|
955
|
-
- 在节点详情里确认 selector 是否显示“重复 N”,以及“推荐定位”是否显示父级 + 子级。
|
|
956
|
-
- 如果推荐定位不准确,展开录制步骤,手动修改父级上下文 selector。
|
|
957
|
-
- 坐标点击只作为最后兜底。
|
|
958
|
-
|
|
959
|
-
### 点击后没有检测到页面跳转怎么办
|
|
960
|
-
|
|
961
|
-
可能原因:
|
|
962
|
-
|
|
963
|
-
- 按钮当前禁用。
|
|
964
|
-
- 账号密码不正确。
|
|
965
|
-
- App 使用单 Activity,Activity 不变化。
|
|
966
|
-
- 页面跳转依赖网络。
|
|
967
|
-
|
|
968
|
-
推荐:
|
|
969
|
-
|
|
970
|
-
- 使用“断言存在”判断下个页面核心元素。
|
|
971
|
-
- 多 Activity 页面可以拆成两个脚本,在判断分支中用“连接脚本”串起来;回放会等待目标 Activity 出现。
|
|
972
|
-
- 单 Activity App 不要依赖 Activity 变化,应使用页面核心元素判断是否已进入目标状态。
|
|
973
|
-
|
|
974
|
-
### 为什么连接脚本提示入口 Activity 不匹配
|
|
975
|
-
|
|
976
|
-
- 主流程连接要求目标脚本入口 Activity 与当前插入点一致,适合复用同一页面上的公共流程。
|
|
977
|
-
- 登录后进入首页等跨 Activity 场景,应先添加“判断存在”,再在“是”或“否”分支中添加“连接脚本”。
|
|
978
|
-
- 分支连接允许选择同一 App 的其他 Activity,但不会主动跳转页面;前面的点击或输入必须真正触发跳转。
|
|
979
|
-
- 如果页面未跳转,回放会在等待目标 Activity 超时后失败,这是为了防止在错误页面执行目标脚本。
|
|
980
|
-
|
|
981
|
-
### 为什么在桌面点击 App 后回放无效
|
|
982
|
-
|
|
983
|
-
部分厂商系统会限制桌面或启动器页面的控件信息,Appium 可能无法获取桌面图标的稳定 id。此时在桌面录制“点击 App 图标”可能无法回放。
|
|
984
|
-
|
|
985
|
-
建议在开始节点后添加“启动 App”操作,由系统按包名打开目标 App,替代桌面点击图标。需要重置登录态或缓存时,可以先添加“清理 App 缓存”,再添加“启动 App”。
|
|
986
|
-
|
|
987
|
-
### 弹窗只出现一次怎么办
|
|
988
|
-
|
|
989
|
-
使用“判断存在”。
|
|
990
|
-
|
|
991
|
-
```text
|
|
992
|
-
判断存在 确认按钮
|
|
993
|
-
是 -> 点击 确认按钮
|
|
994
|
-
否 -> 不添加操作
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
不要直接把弹窗确认按钮作为普通必选点击,否则弹窗不出现时脚本会失败。
|
|
998
|
-
|
|
999
|
-
### 什么时候用等待 Activity
|
|
1000
|
-
|
|
1001
|
-
适合多 Activity App。
|
|
1002
|
-
|
|
1003
|
-
如果 App 是单 Activity 架构,优先用:
|
|
1004
|
-
|
|
1005
|
-
```text
|
|
1006
|
-
断言存在 页面核心元素
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
### 什么时候用可选步骤
|
|
1010
|
-
|
|
1011
|
-
可选步骤适合非主流程阻塞项:
|
|
1012
|
-
|
|
1013
|
-
- 权限弹窗
|
|
1014
|
-
- 协议弹窗
|
|
1015
|
-
- 活动弹窗
|
|
1016
|
-
- 首次引导
|
|
1017
|
-
|
|
1018
|
-
不建议把登录按钮、提交按钮、核心断言设置为可选。
|
|
1019
|
-
|
|
1020
|
-
### 回放时出现 UiAutomation not connected 怎么办
|
|
1021
|
-
|
|
1022
|
-
组件树刷新和 Appium 回放不能同时占用 UiAutomation。当前版本会在回放期间暂停组件树自动刷新,并在断开时清理残留抓取进程后重试一次。
|
|
1023
|
-
|
|
1024
|
-
如果仍然失败:
|
|
1025
|
-
|
|
1026
|
-
1. 确认手机保持解锁,USB 调试授权没有失效。
|
|
1027
|
-
2. 确认 Appium 和 UiAutomator2 Driver 已正常启动。
|
|
1028
|
-
3. 停止其他正在抓取同一设备组件树的工具。
|
|
1029
|
-
4. 重新连接设备后再次回放。
|
|
1030
|
-
|
|
1031
|
-
### 设备预览或组件树没有更新怎么办
|
|
1032
|
-
|
|
1033
|
-
1. 先确认设备仍显示在设备下拉框中。
|
|
1034
|
-
2. 点击设备预览刷新按钮重新获取画面。
|
|
1035
|
-
3. 点击“刷新组件树”重新抓取页面结构。
|
|
1036
|
-
4. 回放期间组件树自动刷新会暂停,回放完成后会自动恢复。
|
|
948
|
+
常见问题已独立整理到 [FAQ.md](./FAQ.md)。
|
|
1037
949
|
|
|
1038
950
|
## 推荐录制规范
|
|
1039
951
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-midscene-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/oooooooko/android-midscene-automation.git"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"vite.config.ts",
|
|
24
24
|
"README.md",
|
|
25
25
|
"USAGE.md",
|
|
26
|
+
"FAQ.md",
|
|
26
27
|
"CHANGELOG.md"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
package/server/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import YAML from 'yaml';
|
|
4
5
|
import { appDataPath } from './paths';
|
|
@@ -31,6 +32,14 @@ export type AppConfig = {
|
|
|
31
32
|
let cachedConfig: AppConfig | null = null;
|
|
32
33
|
const configPath = appDataPath('config.json');
|
|
33
34
|
const legacyConfigPath = appDataPath('config.yaml');
|
|
35
|
+
const adbFileName = process.platform === 'win32' ? 'adb.exe' : 'adb';
|
|
36
|
+
|
|
37
|
+
export class ConfigValidationError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ConfigValidationError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
34
43
|
|
|
35
44
|
function defaultConfig(): AppConfig {
|
|
36
45
|
return {
|
|
@@ -95,6 +104,65 @@ function normalizeConfig(config: Partial<AppConfig> | null | undefined): AppConf
|
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
function expandRuntimePath(value: string) {
|
|
108
|
+
const trimmed = value.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
109
|
+
if (trimmed === '~') return os.homedir();
|
|
110
|
+
if (trimmed.startsWith(`~${path.sep}`) || trimmed.startsWith('~/')) {
|
|
111
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
112
|
+
}
|
|
113
|
+
return path.isAbsolute(trimmed) ? path.normalize(trimmed) : appDataPath(trimmed);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isAbsoluteRuntimePath(value: string) {
|
|
117
|
+
const trimmed = value.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
118
|
+
return trimmed === '~'
|
|
119
|
+
|| trimmed.startsWith(`~${path.sep}`)
|
|
120
|
+
|| trimmed.startsWith('~/')
|
|
121
|
+
|| path.isAbsolute(trimmed);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateAndroidSdkPath(value: string) {
|
|
125
|
+
if (!value) return;
|
|
126
|
+
const candidate = expandRuntimePath(value);
|
|
127
|
+
const lowerBaseName = path.basename(candidate).toLowerCase();
|
|
128
|
+
const root = lowerBaseName === adbFileName.toLowerCase()
|
|
129
|
+
? path.dirname(path.dirname(candidate))
|
|
130
|
+
: lowerBaseName === 'platform-tools'
|
|
131
|
+
? path.dirname(candidate)
|
|
132
|
+
: candidate;
|
|
133
|
+
const adbPath = path.join(root, 'platform-tools', adbFileName);
|
|
134
|
+
if (!fs.existsSync(adbPath)) {
|
|
135
|
+
throw new ConfigValidationError(
|
|
136
|
+
`Android SDK 路径无效:${value}。请选择包含 platform-tools/${adbFileName} 的 SDK 目录。`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateReportOutputPath(value: string) {
|
|
142
|
+
if (!value) return;
|
|
143
|
+
if (!isAbsoluteRuntimePath(value)) {
|
|
144
|
+
throw new ConfigValidationError(`回放报告目录无效:${value}。请填写绝对路径,或留空使用默认 output 目录。`);
|
|
145
|
+
}
|
|
146
|
+
const outputDir = expandRuntimePath(value);
|
|
147
|
+
try {
|
|
148
|
+
if (!fs.existsSync(outputDir)) {
|
|
149
|
+
throw new Error('目录不存在');
|
|
150
|
+
}
|
|
151
|
+
if (!fs.statSync(outputDir).isDirectory()) {
|
|
152
|
+
throw new Error('不是目录');
|
|
153
|
+
}
|
|
154
|
+
fs.accessSync(outputDir, fs.constants.W_OK);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const detail = error instanceof Error && error.message ? `(${error.message})` : '';
|
|
157
|
+
throw new ConfigValidationError(`回放报告目录无效:${value}。请选择已经存在并可写入的目录${detail}。`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateRuntimeConfig(config: AppConfig) {
|
|
162
|
+
validateAndroidSdkPath(config.runtime.androidSdkPath);
|
|
163
|
+
validateReportOutputPath(config.runtime.reportOutputPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
98
166
|
export function loadConfig(): AppConfig {
|
|
99
167
|
if (cachedConfig) {
|
|
100
168
|
return cachedConfig;
|
|
@@ -126,7 +194,9 @@ export function loadConfig(): AppConfig {
|
|
|
126
194
|
}
|
|
127
195
|
|
|
128
196
|
export function saveConfig(config: AppConfig) {
|
|
129
|
-
|
|
197
|
+
const normalized = normalizeConfig(config);
|
|
198
|
+
validateRuntimeConfig(normalized);
|
|
199
|
+
cachedConfig = normalized;
|
|
130
200
|
saveModelConfigToDb(cachedConfig);
|
|
131
201
|
}
|
|
132
202
|
|
package/server/http-api.ts
CHANGED
|
@@ -9,7 +9,7 @@ type NextHandleFunction = (req: IncomingMessage, res: ServerResponse, next: Next
|
|
|
9
9
|
|
|
10
10
|
import { execFile, spawn } from 'node:child_process';
|
|
11
11
|
import { appPath } from './paths';
|
|
12
|
-
import { loadConfig, saveConfig, type AppConfig } from './config';
|
|
12
|
+
import { ConfigValidationError, loadConfig, saveConfig, type AppConfig } from './config';
|
|
13
13
|
import { listAppPresetRecords, removeAppPresetRecord, saveAppPresetRecord } from './config-store';
|
|
14
14
|
import { generatePlan } from './script-agent';
|
|
15
15
|
import { importMidsceneModelUsage } from './model-call-usage-importer';
|
|
@@ -648,7 +648,7 @@ export function createApiMiddleware() {
|
|
|
648
648
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
649
649
|
res.end(JSON.stringify({ success: true }));
|
|
650
650
|
} catch (error) {
|
|
651
|
-
res.statusCode = 500;
|
|
651
|
+
res.statusCode = error instanceof ConfigValidationError ? 400 : 500;
|
|
652
652
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
653
653
|
res.end(JSON.stringify({ message: error instanceof Error ? error.message : 'Unknown error' }));
|
|
654
654
|
}
|
package/src/pages/ConfigPage.vue
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
|
-
import { Delete, Edit } from '@element-plus/icons-vue';
|
|
3
|
+
import { Delete, Edit, QuestionFilled } from '@element-plus/icons-vue';
|
|
4
4
|
import ModelUsageChart from '../components/config/ModelUsageChart.vue';
|
|
5
5
|
import {
|
|
6
6
|
codexMidsceneModelOptions,
|
|
7
|
-
midsceneModelFamilyOptions,
|
|
8
|
-
midsceneModelOptions,
|
|
9
7
|
midsceneModelPresets,
|
|
10
8
|
type MidsceneModelProvider,
|
|
11
9
|
type MidsceneModelPresetKey,
|
|
@@ -45,6 +43,7 @@ const emit = defineEmits<{
|
|
|
45
43
|
const activeMidsceneProvider = computed<MidsceneModelProvider>(() =>
|
|
46
44
|
props.configForm.midscene.model.provider === 'codex' ? 'codex' : 'custom',
|
|
47
45
|
);
|
|
46
|
+
const modelConfigGuideUrl = 'https://midscenejs.com/zh/model-common-config.html';
|
|
48
47
|
|
|
49
48
|
const activeMidscenePresetKey = computed(() => {
|
|
50
49
|
const model = props.configForm.midscene.model;
|
|
@@ -56,13 +55,9 @@ const activeMidscenePresetKey = computed(() => {
|
|
|
56
55
|
)?.key || '';
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
const activeMidsceneModelOptions = computed(() =>
|
|
60
|
-
activeMidsceneProvider.value === 'codex' ? codexMidsceneModelOptions : midsceneModelOptions,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
58
|
const updateMidsceneModelName = (value: string) => {
|
|
64
59
|
props.configForm.midscene.model.name = value;
|
|
65
|
-
const option =
|
|
60
|
+
const option = codexMidsceneModelOptions.find((item) => item.value === value);
|
|
66
61
|
if (option) {
|
|
67
62
|
props.configForm.midscene.model.family = option.family;
|
|
68
63
|
}
|
|
@@ -79,6 +74,10 @@ const updateMidsceneProvider = (value: string) => {
|
|
|
79
74
|
emit('updateMidsceneModelProvider', value);
|
|
80
75
|
}
|
|
81
76
|
};
|
|
77
|
+
|
|
78
|
+
const openModelConfigGuide = () => {
|
|
79
|
+
window.open(modelConfigGuideUrl, '_blank', 'noopener,noreferrer');
|
|
80
|
+
};
|
|
82
81
|
</script>
|
|
83
82
|
|
|
84
83
|
<template>
|
|
@@ -204,29 +203,41 @@ const updateMidsceneProvider = (value: string) => {
|
|
|
204
203
|
<el-form-item label="API Key">
|
|
205
204
|
<el-input v-model="configForm.midscene.model.apiKey" show-password />
|
|
206
205
|
</el-form-item>
|
|
207
|
-
<el-form-item
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
<el-form-item>
|
|
207
|
+
<template #label>
|
|
208
|
+
<span class="config-field-label">
|
|
209
|
+
<span>Model Name</span>
|
|
210
|
+
<el-tooltip content="查看模型填写参考" placement="top">
|
|
211
|
+
<el-button
|
|
212
|
+
class="config-field-help"
|
|
213
|
+
text
|
|
214
|
+
size="small"
|
|
215
|
+
:icon="QuestionFilled"
|
|
216
|
+
aria-label="查看 Model Name 填写参考"
|
|
217
|
+
@click.stop="openModelConfigGuide"
|
|
218
|
+
/>
|
|
219
|
+
</el-tooltip>
|
|
220
|
+
</span>
|
|
221
|
+
</template>
|
|
222
|
+
<el-input v-model="configForm.midscene.model.name" placeholder="例如:gpt-5.5" />
|
|
220
223
|
</el-form-item>
|
|
221
|
-
<el-form-item
|
|
222
|
-
<
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
<el-form-item>
|
|
225
|
+
<template #label>
|
|
226
|
+
<span class="config-field-label">
|
|
227
|
+
<span>Model Family</span>
|
|
228
|
+
<el-tooltip content="查看模型填写参考" placement="top">
|
|
229
|
+
<el-button
|
|
230
|
+
class="config-field-help"
|
|
231
|
+
text
|
|
232
|
+
size="small"
|
|
233
|
+
:icon="QuestionFilled"
|
|
234
|
+
aria-label="查看 Model Family 填写参考"
|
|
235
|
+
@click.stop="openModelConfigGuide"
|
|
236
|
+
/>
|
|
237
|
+
</el-tooltip>
|
|
238
|
+
</span>
|
|
239
|
+
</template>
|
|
240
|
+
<el-input v-model="configForm.midscene.model.family" placeholder="例如:gpt-5" />
|
|
230
241
|
</el-form-item>
|
|
231
242
|
</template>
|
|
232
243
|
|
|
@@ -246,7 +257,7 @@ const updateMidsceneProvider = (value: string) => {
|
|
|
246
257
|
@change="updateMidsceneModelName"
|
|
247
258
|
>
|
|
248
259
|
<el-option
|
|
249
|
-
v-for="option in
|
|
260
|
+
v-for="option in codexMidsceneModelOptions"
|
|
250
261
|
:key="option.value"
|
|
251
262
|
:label="option.label"
|
|
252
263
|
:value="option.value"
|
package/src/style.css
CHANGED
|
@@ -1587,6 +1587,18 @@ select {
|
|
|
1587
1587
|
gap: 8px;
|
|
1588
1588
|
}
|
|
1589
1589
|
|
|
1590
|
+
.config-field-label {
|
|
1591
|
+
display: inline-flex;
|
|
1592
|
+
align-items: center;
|
|
1593
|
+
gap: 4px;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.config-field-help.el-button {
|
|
1597
|
+
height: 18px;
|
|
1598
|
+
padding: 0;
|
|
1599
|
+
color: var(--el-text-color-secondary);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1590
1602
|
.config-module-card {
|
|
1591
1603
|
min-width: 0;
|
|
1592
1604
|
}
|