android-midscene-automation 0.1.0

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 (78) hide show
  1. package/README.md +160 -0
  2. package/bin/android-midscene-automation.js +27 -0
  3. package/index.html +12 -0
  4. package/package.json +49 -0
  5. package/remote-agent/index.ts +206 -0
  6. package/server/appium-recorder/appium-runner.ts +427 -0
  7. package/server/appium-recorder/repository.ts +228 -0
  8. package/server/appium-recorder/routes.ts +219 -0
  9. package/server/config-store.ts +167 -0
  10. package/server/config.ts +130 -0
  11. package/server/device-locks/repository.ts +147 -0
  12. package/server/device-locks/service.ts +72 -0
  13. package/server/device-locks/types.ts +22 -0
  14. package/server/device-sessions/repository.ts +169 -0
  15. package/server/device-sessions/service.ts +59 -0
  16. package/server/device-sessions/types.ts +24 -0
  17. package/server/http-api.ts +1389 -0
  18. package/server/model-call-usage-importer.ts +108 -0
  19. package/server/model-tester.ts +104 -0
  20. package/server/model-usage-repository.ts +131 -0
  21. package/server/operations/repository.ts +218 -0
  22. package/server/operations/service.ts +84 -0
  23. package/server/operations/types.ts +27 -0
  24. package/server/paths.ts +27 -0
  25. package/server/remote-agents/protocol.ts +38 -0
  26. package/server/remote-agents/registry.ts +136 -0
  27. package/server/remote-agents/routes.ts +89 -0
  28. package/server/script-agent.ts +284 -0
  29. package/server/script-db.ts +281 -0
  30. package/server/script-runner.ts +551 -0
  31. package/server/storage/sqlite.ts +49 -0
  32. package/server/test-case-import/formatter.ts +28 -0
  33. package/server/test-case-import/parsers/excel.ts +69 -0
  34. package/server/test-case-import/parsers/txt.ts +11 -0
  35. package/server/test-case-import/parsers/word.ts +9 -0
  36. package/server/test-case-import/service.ts +58 -0
  37. package/server/test-case-import/text-normalizer.ts +98 -0
  38. package/server/test-case-import/types.ts +24 -0
  39. package/server/test-case-import/validator.ts +34 -0
  40. package/src/App.vue +1450 -0
  41. package/src/api.ts +290 -0
  42. package/src/appium-recorder/AppiumPage.vue +894 -0
  43. package/src/appium-recorder/api.ts +64 -0
  44. package/src/appium-recorder/components/ComponentTree.vue +44 -0
  45. package/src/appium-recorder/components/NodeDetail.vue +152 -0
  46. package/src/appium-recorder/components/RecordedSteps.vue +79 -0
  47. package/src/appium-recorder/tree.ts +129 -0
  48. package/src/appium-recorder/types.ts +88 -0
  49. package/src/assets/device-actions/back.svg +5 -0
  50. package/src/assets/device-actions/home.svg +3 -0
  51. package/src/assets/device-actions/power.svg +5 -0
  52. package/src/assets/device-actions/tasks.svg +3 -0
  53. package/src/assets/device-actions/volume-down.svg +3 -0
  54. package/src/assets/device-actions/volume-up.svg +3 -0
  55. package/src/components/config/ModelUsageChart.vue +188 -0
  56. package/src/components/device/DevicePreviewPanel.vue +266 -0
  57. package/src/components/generator/GeneratedCodePanel.vue +70 -0
  58. package/src/components/generator/TestCaseFileUpload.vue +97 -0
  59. package/src/config/midscene-model-presets.ts +75 -0
  60. package/src/config/prompt-example.ts +6 -0
  61. package/src/main.ts +7 -0
  62. package/src/pages/AiGeneratorPage.vue +90 -0
  63. package/src/pages/AutomationPage.vue +161 -0
  64. package/src/pages/ConfigPage.vue +273 -0
  65. package/src/pages/GeneratorPage.vue +97 -0
  66. package/src/pages/ManualStepsPage.vue +179 -0
  67. package/src/script-generator/codegen.ts +126 -0
  68. package/src/script-generator/index.ts +4 -0
  69. package/src/script-generator/presets.ts +37 -0
  70. package/src/script-generator/step-options.ts +52 -0
  71. package/src/script-generator/types.ts +27 -0
  72. package/src/style.css +1983 -0
  73. package/src/types.ts +157 -0
  74. package/src/vite-env.d.ts +1 -0
  75. package/tsconfig.app.json +8 -0
  76. package/tsconfig.json +11 -0
  77. package/tsconfig.node.json +16 -0
  78. package/vite.config.ts +28 -0
package/src/api.ts ADDED
@@ -0,0 +1,290 @@
1
+ import type { ScriptStep } from './script-generator';
2
+ import type {
3
+ AndroidDevice,
4
+ AppPreset,
5
+ ConfigForm,
6
+ DeviceLock,
7
+ DeviceSession,
8
+ ModelUsageRecord,
9
+ OperationEvent,
10
+ OperationRecord,
11
+ RunScriptStreamEvent,
12
+ SavedScript,
13
+ TestCaseImportResult,
14
+ } from './types';
15
+
16
+ // 应用挂载基路径:独立运行(dev/preview)时为 '',作为 DSH 插件挂载在
17
+ // /midscene 下时由 vite 的 base 推导为 '/midscene'。所有后端调用都拼上它。
18
+ export const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/+$/, '');
19
+
20
+ // 统一解析接口响应,并把后端 message 转成前端可直接展示的 Error。
21
+ async function readJson<T>(response: Response) {
22
+ const payload = (await response.json().catch(() => ({}))) as T & { message?: string };
23
+ if (!response.ok) {
24
+ throw new Error(payload.message || '请求失败');
25
+ }
26
+ return payload;
27
+ }
28
+
29
+ // 项目内大部分接口都是 JSON POST,这里集中处理请求头和错误转换。
30
+ async function postJson<T>(url: string, body?: unknown) {
31
+ const response = await fetch(url, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(body || {}),
35
+ });
36
+ return readJson<T>(response);
37
+ }
38
+
39
+ // 读取已保存的脚本列表,自动化测试页面左侧脚本列表使用。
40
+ export async function getScripts() {
41
+ const response = await fetch(`${APP_BASE}/api/scripts`);
42
+ return readJson<{ scripts?: SavedScript[] }>(response);
43
+ }
44
+
45
+ // 检查脚本名称对应的本地脚本文件是否已经存在,用于保存前同名校验。
46
+ export function checkScript(input: { scriptName: string }) {
47
+ return postJson<{ exists?: boolean; filePath?: string }>(`${APP_BASE}/api/check-script`, input);
48
+ }
49
+
50
+ // 校验生成页编辑后的 TypeScript 代码,只返回校验结果,不写入脚本文件或数据库。
51
+ export function validateScriptCode(input: { code: string }) {
52
+ return postJson<{ valid: true }>(`${APP_BASE}/api/validate-script-code`, input);
53
+ }
54
+
55
+ // 上传单个测试用例文件;后端负责解析、有效性校验并返回规范化 Prompt。
56
+ export async function importTestCaseFile(file: File) {
57
+ const response = await fetch(`${APP_BASE}/api/import-test-case?fileName=${encodeURIComponent(file.name)}`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/octet-stream' },
60
+ body: file,
61
+ });
62
+ return readJson<TestCaseImportResult>(response);
63
+ }
64
+
65
+ // 保存生成后的脚本代码、场景标题、原始 Prompt 和结构化步骤到 SQLite 与 scripts-output。
66
+ export function saveScript(input: {
67
+ scriptName: string;
68
+ code: string;
69
+ promptTitle: string;
70
+ sourcePrompt: string;
71
+ steps: ScriptStep[];
72
+ }) {
73
+ return postJson<{ success?: boolean; filePath?: string; script?: SavedScript }>(`${APP_BASE}/api/save-script`, input);
74
+ }
75
+
76
+ // 删除脚本记录,并删除 scripts-output 下对应的脚本文件。
77
+ export function deleteScript(input: { id: string }) {
78
+ return postJson<{ success?: boolean; deletedFile?: boolean; script?: SavedScript }>(`${APP_BASE}/api/delete-script`, input);
79
+ }
80
+
81
+ // 保存脚本查看弹窗里编辑后的代码;后端会先做 TypeScript 格式校验。
82
+ export function updateScriptCode(input: { id: string; code: string }) {
83
+ return postJson<{ success?: boolean; script?: SavedScript }>(`${APP_BASE}/api/update-script-code`, input);
84
+ }
85
+
86
+ // 调用脚本优化模型,把原始 Prompt 转成项目内的结构化步骤。
87
+ export function generatePlan(input: { prompt: string }) {
88
+ return postJson<{
89
+ promptTitle?: string;
90
+ steps?: Array<Omit<ScriptStep, 'id'>>;
91
+ raw?: string;
92
+ durationMs?: number;
93
+ usage?: {
94
+ promptTokens?: number;
95
+ completionTokens?: number;
96
+ totalTokens?: number;
97
+ };
98
+ }>(`${APP_BASE}/api/generate`, input);
99
+ }
100
+
101
+ // 读取模型配置和脚本优化模型配置。
102
+ export async function getConfig() {
103
+ const response = await fetch(`${APP_BASE}/api/config`);
104
+ return readJson<ConfigForm>(response);
105
+ }
106
+
107
+ // 保存模型配置;当前由参数配置页的各模块保存按钮调用。
108
+ export function saveConfig(input: ConfigForm) {
109
+ return postJson<{ success?: boolean } | ConfigForm>(`${APP_BASE}/api/config`, input);
110
+ }
111
+
112
+ // 测试指定模型配置是否可用,通常用于保存前验证 baseUrl/apiKey/model。
113
+ export function testModel(input: {
114
+ modelKey: 'midscene' | 'scriptOptimizer';
115
+ model: {
116
+ provider?: 'custom' | 'codex';
117
+ baseUrl: string;
118
+ apiKey: string;
119
+ name: string;
120
+ family?: string;
121
+ };
122
+ }) {
123
+ return postJson<{
124
+ content?: string;
125
+ durationMs?: number;
126
+ usage?: {
127
+ promptTokens?: number;
128
+ completionTokens?: number;
129
+ totalTokens?: number;
130
+ };
131
+ }>(`${APP_BASE}/api/test-model`, input);
132
+ }
133
+
134
+ // 读取模型测试消耗记录,后端按时间保存在 SQLite。
135
+ export async function getModelUsageRecords(limit = 50) {
136
+ const response = await fetch(`${APP_BASE}/api/model-usage-records?limit=${encodeURIComponent(String(limit))}`);
137
+ return readJson<{ records?: ModelUsageRecord[] }>(response);
138
+ }
139
+
140
+ // 读取预设 App 列表,测试脚本生成时用于自动拼接 App 名称和包名上下文。
141
+ export async function getAppPresets() {
142
+ const response = await fetch(`${APP_BASE}/api/app-presets`);
143
+ return readJson<{ apps?: AppPreset[] }>(response);
144
+ }
145
+
146
+ // 新增或更新预设 App;同名或同包名校验由后端负责。
147
+ export function saveAppPreset(input: { id?: string; name: string; packageName: string }) {
148
+ return postJson<{ app?: AppPreset }>(`${APP_BASE}/api/save-app-preset`, input);
149
+ }
150
+
151
+ // 删除一个预设 App 配置。
152
+ export function deleteAppPreset(input: { id: string }) {
153
+ return postJson<{ success?: boolean }>(`${APP_BASE}/api/delete-app-preset`, input);
154
+ }
155
+
156
+ // 读取 ADB 设备列表,并让后端确保当前设备会话存在。
157
+ export async function getAndroidDevices() {
158
+ const response = await fetch(`${APP_BASE}/api/android-devices`);
159
+ return readJson<{ devices?: AndroidDevice[]; currentDeviceId?: string }>(response);
160
+ }
161
+
162
+ // 读取后端缓存的设备会话状态,用于排查页面切换后是否复用同一设备连接。
163
+ export async function getDeviceSessions() {
164
+ const response = await fetch(`${APP_BASE}/api/device-sessions`);
165
+ return readJson<{ sessions?: DeviceSession[] }>(response);
166
+ }
167
+
168
+ // 读取当前活跃设备锁,用于排查是否有脚本执行占用某台手机。
169
+ export async function getDeviceLocks() {
170
+ const response = await fetch(`${APP_BASE}/api/device-locks`);
171
+ return readJson<{ locks?: DeviceLock[] }>(response);
172
+ }
173
+
174
+ // 读取脚本执行记录列表,包含执行状态、设备、脚本名和结果摘要。
175
+ export async function getOperations(limit = 30) {
176
+ const response = await fetch(`${APP_BASE}/api/operations?limit=${encodeURIComponent(String(limit))}`);
177
+ return readJson<{ operations?: OperationRecord[] }>(response);
178
+ }
179
+
180
+ // 读取单次脚本执行详情和事件流,便于查看输出、步骤事件和失败原因。
181
+ export async function getOperation(operationId: string) {
182
+ const response = await fetch(`${APP_BASE}/api/operations/${encodeURIComponent(operationId)}`);
183
+ return readJson<{ operation?: OperationRecord; events?: OperationEvent[] }>(response);
184
+ }
185
+
186
+ // 读取 Android 设备分辨率,用于把预览区域坐标映射到真实设备坐标。
187
+ export async function getAndroidDisplayInfo(deviceId: string) {
188
+ const response = await fetch(`${APP_BASE}/api/android-display-info?deviceId=${encodeURIComponent(deviceId)}`);
189
+ return readJson<{ width?: number; height?: number }>(response);
190
+ }
191
+
192
+ // 查询 Android Playground 状态;后端会同步更新设备会话中的预览连接信息。
193
+ export async function getPlaygroundStatus() {
194
+ const response = await fetch(`${APP_BASE}/api/playground-status`);
195
+ return readJson<{
196
+ available?: boolean;
197
+ url?: string;
198
+ deviceId?: string;
199
+ previewKind?: string;
200
+ sessionConnected?: boolean;
201
+ previewError?: string;
202
+ setupState?: string;
203
+ }>(response);
204
+ }
205
+
206
+ // 切换当前自动化测试设备,并创建或复用对应设备会话。
207
+ export function setAndroidDevice(input: { deviceId: string }) {
208
+ return postJson<{ currentDeviceId?: string }>(`${APP_BASE}/api/android-device`, input);
209
+ }
210
+
211
+ // 向 Android 设备发送 tap 坐标,用于设备预览中的鼠标/触摸点击。
212
+ export function tapAndroid(input: { deviceId: string; x: number; y: number }) {
213
+ return postJson<{ success?: boolean; message?: string }>(`${APP_BASE}/api/android-tap`, input);
214
+ }
215
+
216
+ // 向 Android 设备发送 swipe 手势,用于设备预览中的拖动操作。
217
+ export function swipeAndroid(input: {
218
+ deviceId: string;
219
+ startX: number;
220
+ startY: number;
221
+ endX: number;
222
+ endY: number;
223
+ duration: number;
224
+ }) {
225
+ return postJson<{ success?: boolean; message?: string }>(`${APP_BASE}/api/android-swipe`, input);
226
+ }
227
+
228
+ // 向 Android 设备发送系统按键,例如返回、Home、任务、音量、电源等。
229
+ export function sendAndroidKeyevent(input: { deviceId: string; keyCode: number }) {
230
+ return postJson<{ success?: boolean; message?: string }>(`${APP_BASE}/api/android-keyevent`, input);
231
+ }
232
+
233
+ // 停止当前脚本执行进程;后端会中止运行并释放设备锁。
234
+ export function stopScript() {
235
+ return postJson<{ success?: boolean; stopped?: boolean }>(`${APP_BASE}/api/stop-script`);
236
+ }
237
+
238
+ // run-script 返回 NDJSON 流,每一行都是一个执行事件,边读边回调给执行面板更新 UI。
239
+ export async function runScript(
240
+ input: {
241
+ code: string;
242
+ scriptName: string;
243
+ deviceId: string;
244
+ steps: ScriptStep[];
245
+ signal?: AbortSignal;
246
+ },
247
+ onEvent: (event: RunScriptStreamEvent) => void,
248
+ ) {
249
+ const response = await fetch(`${APP_BASE}/api/run-script`, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ signal: input.signal,
253
+ body: JSON.stringify({
254
+ code: input.code,
255
+ scriptName: input.scriptName,
256
+ deviceId: input.deviceId,
257
+ steps: input.steps,
258
+ }),
259
+ });
260
+
261
+ if (!response.ok) {
262
+ const message = await response.text();
263
+ throw new Error(message || '执行失败');
264
+ }
265
+
266
+ if (!response.body) {
267
+ throw new Error('执行接口没有返回输出流');
268
+ }
269
+
270
+ const reader = response.body.getReader();
271
+ const decoder = new TextDecoder();
272
+ let buffer = '';
273
+
274
+ // 分块数据可能把一行 JSON 拆开,buffer 保留未读完的半行。
275
+ while (true) {
276
+ const { done, value } = await reader.read();
277
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
278
+ const lines = buffer.split(/\r?\n/);
279
+ buffer = lines.pop() || '';
280
+ for (const line of lines) {
281
+ if (!line.trim()) continue;
282
+ onEvent(JSON.parse(line) as RunScriptStreamEvent);
283
+ }
284
+ if (done) break;
285
+ }
286
+
287
+ if (buffer.trim()) {
288
+ onEvent(JSON.parse(buffer) as RunScriptStreamEvent);
289
+ }
290
+ }