android-midscene-automation 0.1.5 → 0.1.9

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.
@@ -12,6 +12,10 @@ export type AppiumRecordedStepRecord = {
12
12
  type:
13
13
  | 'tap'
14
14
  | 'input'
15
+ | 'tapIfExists'
16
+ | 'inputIfExists'
17
+ | 'clearIfExists'
18
+ | 'backIfExists'
15
19
  | 'waitFor'
16
20
  | 'assertExists'
17
21
  | 'key'
@@ -25,23 +29,72 @@ export type AppiumRecordedStepRecord = {
25
29
  | 'waitDisappear'
26
30
  | 'assertText'
27
31
  | 'longPress'
28
- | 'pinch';
32
+ | 'pinch'
33
+ | 'runScript';
29
34
  label: string;
30
35
  selector?: {
31
36
  strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
32
37
  value?: string;
33
38
  centerX?: number;
34
39
  centerY?: number;
40
+ unique?: boolean;
41
+ matchCount?: number;
42
+ source?: 'node' | 'ancestor' | 'fallback';
35
43
  };
44
+ contextSelector?: {
45
+ strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
46
+ value?: string;
47
+ centerX?: number;
48
+ centerY?: number;
49
+ unique?: boolean;
50
+ matchCount?: number;
51
+ source?: 'node' | 'ancestor' | 'fallback';
52
+ };
53
+ selectorChain?: Array<{
54
+ strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
55
+ value?: string;
56
+ centerX?: number;
57
+ centerY?: number;
58
+ unique?: boolean;
59
+ matchCount?: number;
60
+ source?: 'node' | 'ancestor' | 'fallback';
61
+ }>;
36
62
  fallback?: {
37
63
  strategy: 'accessibilityId' | 'id' | 'androidUiAutomator' | 'xpath' | 'bounds';
38
64
  value?: string;
39
65
  centerX?: number;
40
66
  centerY?: number;
67
+ unique?: boolean;
68
+ matchCount?: number;
69
+ source?: 'node' | 'ancestor' | 'fallback';
41
70
  };
42
71
  value?: string;
72
+ optional?: boolean;
43
73
  keyCode?: number;
44
74
  timeoutMs?: number;
75
+ flow?: {
76
+ scope?: 'pre' | 'main' | 'post';
77
+ nodeKind?: 'action' | 'condition' | 'assertion';
78
+ yesTargetId?: string;
79
+ noTargetId?: string;
80
+ parentConditionId?: string;
81
+ parentBranch?: 'yes' | 'no';
82
+ successTargetId?: string;
83
+ failureTargetId?: string;
84
+ collapsed?: boolean;
85
+ };
86
+ pageBefore?: {
87
+ activity?: string;
88
+ packageName?: string;
89
+ treeSignature?: string;
90
+ selectedNodePath?: string;
91
+ };
92
+ pageAfter?: {
93
+ activity?: string;
94
+ packageName?: string;
95
+ treeSignature?: string;
96
+ selectedNodePath?: string;
97
+ };
45
98
  swipe?: {
46
99
  startX: number;
47
100
  startY: number;
@@ -80,16 +133,14 @@ type AppiumRecordedScriptRow = {
80
133
  app_package: string;
81
134
  app_activity: string | null;
82
135
  device_id: string | null;
83
- steps_json: string;
136
+ flow_json: string | null;
84
137
  created_at: string;
85
138
  updated_at: string;
86
139
  };
87
140
 
88
141
  let initialized = false;
89
142
 
90
- function initDb() {
91
- if (initialized) return;
92
- initialized = true;
143
+ function createAppiumRecordedScriptsTable() {
93
144
  runSql(`
94
145
  CREATE TABLE IF NOT EXISTS appium_recorded_scripts (
95
146
  id TEXT PRIMARY KEY,
@@ -97,7 +148,7 @@ function initDb() {
97
148
  app_package TEXT NOT NULL,
98
149
  app_activity TEXT,
99
150
  device_id TEXT,
100
- steps_json TEXT NOT NULL DEFAULT '[]',
151
+ flow_json TEXT NOT NULL DEFAULT '[]',
101
152
  created_at TEXT NOT NULL,
102
153
  updated_at TEXT NOT NULL
103
154
  );
@@ -106,6 +157,32 @@ function initDb() {
106
157
  `);
107
158
  }
108
159
 
160
+ function initDb() {
161
+ if (initialized) return;
162
+ initialized = true;
163
+ createAppiumRecordedScriptsTable();
164
+ const columns = querySql<{ name: string }>('PRAGMA table_info(appium_recorded_scripts);').map((column) => column.name);
165
+ if (columns.includes('steps_json') || !columns.includes('flow_json')) {
166
+ runSql('DROP TABLE appium_recorded_scripts;');
167
+ createAppiumRecordedScriptsTable();
168
+ }
169
+ }
170
+
171
+ function parseStepList(value?: string | null) {
172
+ try {
173
+ const parsed = JSON.parse(value || '[]');
174
+ if (!Array.isArray(parsed)) return [];
175
+ return parsed.filter((step) => (
176
+ step
177
+ && typeof step === 'object'
178
+ && typeof step.id === 'string'
179
+ && typeof step.type === 'string'
180
+ )) as AppiumRecordedStepRecord[];
181
+ } catch {
182
+ return [];
183
+ }
184
+ }
185
+
109
186
  function rowToRecord(row: AppiumRecordedScriptRow): AppiumRecordedScriptRecord {
110
187
  return {
111
188
  id: row.id,
@@ -113,7 +190,7 @@ function rowToRecord(row: AppiumRecordedScriptRow): AppiumRecordedScriptRecord {
113
190
  appPackage: row.app_package,
114
191
  appActivity: row.app_activity || '',
115
192
  deviceId: row.device_id || '',
116
- steps: JSON.parse(row.steps_json || '[]') as AppiumRecordedStepRecord[],
193
+ steps: parseStepList(row.flow_json),
117
194
  createdAt: row.created_at,
118
195
  updatedAt: row.updated_at,
119
196
  };
@@ -128,7 +205,7 @@ export function listAppiumRecordedScripts() {
128
205
  app_package,
129
206
  app_activity,
130
207
  device_id,
131
- steps_json,
208
+ flow_json,
132
209
  created_at,
133
210
  updated_at
134
211
  FROM appium_recorded_scripts
@@ -145,7 +222,7 @@ export function getAppiumRecordedScript(id: string) {
145
222
  app_package,
146
223
  app_activity,
147
224
  device_id,
148
- steps_json,
225
+ flow_json,
149
226
  created_at,
150
227
  updated_at
151
228
  FROM appium_recorded_scripts
@@ -180,7 +257,7 @@ export function saveAppiumRecordedScript(input: {
180
257
  app_package = ${sqlString(appPackage)},
181
258
  app_activity = ${sqlNullableString(input.appActivity || '')},
182
259
  device_id = ${sqlNullableString(input.deviceId || '')},
183
- steps_json = ${sqlJson(input.steps || [])},
260
+ flow_json = ${sqlJson(input.steps || [])},
184
261
  updated_at = ${sqlString(now)}
185
262
  WHERE id = ${sqlString(input.id)};
186
263
  `);
@@ -194,7 +271,7 @@ export function saveAppiumRecordedScript(input: {
194
271
  app_package,
195
272
  app_activity,
196
273
  device_id,
197
- steps_json,
274
+ flow_json,
198
275
  created_at,
199
276
  updated_at
200
277
  )
@@ -212,7 +289,7 @@ export function saveAppiumRecordedScript(input: {
212
289
  app_package = excluded.app_package,
213
290
  app_activity = excluded.app_activity,
214
291
  device_id = excluded.device_id,
215
- steps_json = excluded.steps_json,
292
+ flow_json = excluded.flow_json,
216
293
  updated_at = excluded.updated_at;
217
294
  `);
218
295
 
package/src/App.vue CHANGED
@@ -72,6 +72,7 @@ const playgroundFrameUrl = ref('');
72
72
  const playgroundFrameSignature = ref('');
73
73
  const devicePreviewUrl = ref('');
74
74
  const devicePreviewMode = ref<'stream' | 'screenshot' | ''>('');
75
+ const backendOffline = shallowRef(false);
75
76
  const androidDevices = ref<AndroidDevice[]>([]);
76
77
  const deviceInterfaceSize = reactive({ width: 0, height: 0 });
77
78
  const deviceDebug = reactive({
@@ -116,6 +117,17 @@ let appiumPreviewTimer: number | null = null;
116
117
  let executionProgressTimer: number | null = null;
117
118
  let scriptRunAbortController: AbortController | null = null;
118
119
 
120
+ const isBackendFetchError = (error: unknown) => {
121
+ const message = error instanceof Error ? error.message : String(error || '');
122
+ return error instanceof TypeError || /Failed to fetch|NetworkError|Load failed/i.test(message);
123
+ };
124
+
125
+ const stopPlaygroundPollTimer = () => {
126
+ if (!playgroundPollTimer) return;
127
+ window.clearInterval(playgroundPollTimer);
128
+ playgroundPollTimer = null;
129
+ };
130
+
119
131
  const form = reactive({
120
132
  promptTitle: '',
121
133
  testName: '',
@@ -739,10 +751,20 @@ const testModel = async (key: 'midscene' | 'scriptOptimizer') => {
739
751
  };
740
752
 
741
753
  const loadAndroidDevices = async () => {
742
- const payload = await api.getAndroidDevices();
743
- androidDevices.value = payload.devices || [];
744
- playgroundDeviceId.value = payload.currentDeviceId || '';
745
- playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
754
+ if (backendOffline.value) return;
755
+ try {
756
+ const payload = await api.getAndroidDevices();
757
+ backendOffline.value = false;
758
+ androidDevices.value = payload.devices || [];
759
+ playgroundDeviceId.value = payload.currentDeviceId || '';
760
+ playgroundAvailable.value = androidDevices.value.some((item) => item.status === 'device');
761
+ } catch (error) {
762
+ if (isBackendFetchError(error)) {
763
+ markBackendOffline();
764
+ return;
765
+ }
766
+ throw error;
767
+ }
746
768
  };
747
769
 
748
770
  const loadDeviceInterface = async () => {
@@ -757,6 +779,7 @@ const loadDeviceInterface = async () => {
757
779
  };
758
780
 
759
781
  const loadAdbPreview = () => {
782
+ if (backendOffline.value) return;
760
783
  if (!playgroundDeviceId.value) {
761
784
  devicePreviewUrl.value = '';
762
785
  devicePreviewMode.value = '';
@@ -786,6 +809,7 @@ const refreshDevicePreview = () => {
786
809
  };
787
810
 
788
811
  const startAppiumPreviewTimer = () => {
812
+ if (backendOffline.value) return;
789
813
  if (appiumPreviewTimer) return;
790
814
  loadAdbPreview();
791
815
  appiumPreviewTimer = window.setInterval(loadAdbPreview, 1000);
@@ -797,7 +821,22 @@ const stopAppiumPreviewTimer = () => {
797
821
  appiumPreviewTimer = null;
798
822
  };
799
823
 
824
+ const markBackendOffline = () => {
825
+ if (backendOffline.value) return;
826
+ backendOffline.value = true;
827
+ playgroundAvailable.value = false;
828
+ playgroundFrameUrl.value = '';
829
+ playgroundFrameSignature.value = '';
830
+ devicePreviewUrl.value = '';
831
+ devicePreviewMode.value = '';
832
+ playgroundPreviewError.value = '本地服务已断开,请重新运行启动命令后刷新页面。';
833
+ errorMessage.value = playgroundPreviewError.value;
834
+ stopPlaygroundPollTimer();
835
+ stopAppiumPreviewTimer();
836
+ };
837
+
800
838
  const loadPlaygroundStatus = async () => {
839
+ if (backendOffline.value) return;
801
840
  if (!playgroundDeviceId.value) {
802
841
  playgroundAvailable.value = false;
803
842
  playgroundPreviewError.value = '';
@@ -844,6 +883,10 @@ const loadPlaygroundStatus = async () => {
844
883
  loadAdbPreview();
845
884
  }
846
885
  } catch (error) {
886
+ if (isBackendFetchError(error)) {
887
+ markBackendOffline();
888
+ return;
889
+ }
847
890
  playgroundAvailable.value = false;
848
891
  playgroundFrameUrl.value = '';
849
892
  playgroundFrameSignature.value = '';
@@ -1207,15 +1250,17 @@ onMounted(async () => {
1207
1250
  await Promise.allSettled([loadConfig(), loadAppPresets(), loadAndroidDevices(), loadSavedScripts(), loadModelUsageRecords()]);
1208
1251
  await loadPlaygroundStatus();
1209
1252
  playgroundPollTimer = window.setInterval(() => {
1253
+ if (backendOffline.value) {
1254
+ stopPlaygroundPollTimer();
1255
+ return;
1256
+ }
1210
1257
  void loadAndroidDevices();
1211
1258
  void loadPlaygroundStatus();
1212
1259
  }, 2500);
1213
1260
  });
1214
1261
 
1215
1262
  onUnmounted(() => {
1216
- if (playgroundPollTimer) {
1217
- window.clearInterval(playgroundPollTimer);
1218
- }
1263
+ stopPlaygroundPollTimer();
1219
1264
  stopAppiumPreviewTimer();
1220
1265
  stopExecutionProgressTimer();
1221
1266
  scriptRunAbortController?.abort();