android-midscene-automation 0.1.29 → 0.1.30

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # 更新记录
2
2
 
3
+ ## v0.1.30
4
+
5
+ - 新增节点批量删除功能,支持多选节点,选中节点及所属子节点以柔和红色背景和浅色文字标记,确认后统一删除;支持普通与放大视图同步操作。
6
+
3
7
  ## v0.1.29
4
8
 
5
9
  - 判断节点新增“合并分支”,支持选择一侧公共流程起点,将两侧后续汇入同一段居中流程;另一侧可保留独有操作或确认删除重复后缀,支持嵌套判断及各类分支,公共节点只执行一次且不随单侧节点删除。
package/README.md CHANGED
@@ -1229,6 +1229,10 @@ Linux: ~/.local/share/android-midscene-automation
1229
1229
 
1230
1230
  # 项目更新记录
1231
1231
 
1232
+ ## v0.1.30
1233
+
1234
+ - 新增节点批量删除功能,支持多选节点,选中节点及所属子节点以柔和红色背景和浅色文字标记,确认后统一删除;支持普通与放大视图同步操作。
1235
+
1232
1236
  ## v0.1.29
1233
1237
 
1234
1238
  - 判断节点新增“合并分支”,支持选择一侧公共流程起点,将两侧后续汇入同一段居中流程;另一侧可保留独有操作或确认删除重复后缀,支持嵌套判断及各类分支,公共节点只执行一次且不随单侧节点删除。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android-midscene-automation",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oooooooko/android-midscene-automation.git"
@@ -2095,14 +2095,11 @@ watch(
2095
2095
  />
2096
2096
  </el-select>
2097
2097
  </div>
2098
- <el-alert
2098
+ <details
2099
2099
  v-if="scriptActivityMismatch"
2100
- class="appium-activity-lock-alert"
2101
- title="当前脚本已锁定"
2102
- type="warning"
2103
- show-icon
2104
- :closable="false"
2100
+ class="appium-activity-summary"
2105
2101
  >
2102
+ <summary>当前页面与脚本不一致,部分录制操作不可用</summary>
2106
2103
  <div class="appium-activity-lock-alert__details">
2107
2104
  <div>
2108
2105
  <strong>脚本绑定</strong>
@@ -2114,7 +2111,7 @@ watch(
2114
2111
  </div>
2115
2112
  <p>请在录制流程中添加或执行“启动 APP”节点;Activity 匹配后会自动解除编辑锁定。</p>
2116
2113
  </div>
2117
- </el-alert>
2114
+ </details>
2118
2115
  <NodeDetail
2119
2116
  :node="selectedNode"
2120
2117
  :current-activity="currentActivity"
@@ -2122,7 +2119,7 @@ watch(
2122
2119
  />
2123
2120
  </section>
2124
2121
 
2125
- <section class="appium-workbench__section">
2122
+ <section class="appium-workbench__section appium-workbench__flow">
2126
2123
  <h3>录制步骤</h3>
2127
2124
  <RecordedSteps
2128
2125
  :key="newScriptRevision"
@@ -2403,3 +2400,18 @@ watch(
2403
2400
  </el-tooltip>
2404
2401
  </section>
2405
2402
  </template>
2403
+
2404
+ <style scoped>
2405
+ .appium-activity-summary { margin: 8px 0; color: #946000; font-size: 12px; }
2406
+ .appium-activity-summary summary { cursor: pointer; line-height: 20px; }
2407
+ .appium-activity-summary[open] .appium-activity-lock-alert__details { max-height: 110px; overflow: auto; margin-top: 6px; }
2408
+ :deep(.appium-recorder-card--workbench > .el-card__body) { overflow: hidden; }
2409
+ :deep(.appium-workbench-tabs > .el-tabs__content) { overflow: hidden; }
2410
+ :deep(.appium-workbench-tabs .el-tab-pane) { height: 100%; overflow: auto; }
2411
+ .appium-workbench { height: 100%; box-sizing: border-box; grid-template-rows: auto minmax(180px, 1fr); gap: 10px; overflow: auto; }
2412
+ .appium-workbench__flow { display: flex; flex-direction: column; min-height: 0; }
2413
+ .appium-workbench__flow h3 { margin-bottom: 6px; }
2414
+ .appium-workbench__flow :deep(.appium-recorded-steps-panel) { display: flex; flex-direction: column; flex: 1; min-height: 0; }
2415
+ .appium-workbench__flow :deep(.appium-flow-toolbar) { flex: none; margin-bottom: 8px; }
2416
+ .appium-workbench__flow :deep(.appium-flow-canvas--vue:not(.appium-flow-canvas--dialog)) { flex: 1; height: auto; min-height: 120px; }
2417
+ </style>
@@ -21,6 +21,7 @@ const props = defineProps<{
21
21
  steps: AppiumRecordedStep[];
22
22
  expandedStepIndex: number | null;
23
23
  copyMode: boolean;
24
+ deleteMode?: boolean;
24
25
  selectedCopyIndexes: number[];
25
26
  disabled?: boolean;
26
27
  removeDisabled?: boolean;
@@ -61,6 +62,7 @@ let resetViewFrame = 0;
61
62
  let pendingResetView = false;
62
63
 
63
64
  const graph = computed(() => buildFlowGraph(props.steps, {
65
+ deleteMode: props.deleteMode,
64
66
  mergeDisabled: props.mergeDisabled,
65
67
  aiRecognitionModelConfigured: props.aiRecognitionModelConfigured,
66
68
  expandedStepIndex: props.expandedStepIndex,
@@ -159,6 +159,7 @@ watch(() => props.data, () => {
159
159
  {
160
160
  'appium-flow-step-shell--expanded': data.expanded,
161
161
  'appium-flow-step-shell--selected': data.selected,
162
+ 'appium-flow-step-shell--delete-selected': data.selected && data.deleteMode,
162
163
  'appium-flow-step-shell--copying': data.copyMode,
163
164
  'appium-flow-step-shell--readonly': props.readonly,
164
165
  },
@@ -181,7 +182,7 @@ watch(() => props.data, () => {
181
182
  <em v-if="data.note" :title="data.note">{{ data.note }}</em>
182
183
  </span>
183
184
  </button>
184
- <span v-if="!props.readonly" class="appium-flow-step-card__actions nodrag nopan" @click.stop>
185
+ <span v-if="!props.readonly && !data.deleteMode" class="appium-flow-step-card__actions nodrag nopan" @click.stop>
185
186
  <el-tooltip v-if="data.flowKind === 'condition' && !data.copyMode" content="合并分支:将两侧后续节点汇入公共流程" placement="top" :show-after="200">
186
187
  <span class="appium-node-action-tooltip"><el-button text size="small" :icon="Connection"
187
188
  title="合并分支" aria-label="合并分支" :disabled="data.mergeDisabled || Boolean(data.step.flow?.successTargetId)"
@@ -263,6 +264,10 @@ watch(() => props.data, () => {
263
264
  </template>
264
265
 
265
266
  <style scoped>
267
+ .appium-flow-step-shell--delete-selected .appium-flow-step-card { background: #b83232; border-color: #a52a2a; }
268
+ .appium-flow-step-shell--delete-selected .appium-flow-step-card__content strong,
269
+ .appium-flow-step-shell--delete-selected .appium-flow-step-card__content em { color: #fff; }
270
+ .appium-flow-step-shell--delete-selected .appium-flow-step-card__content small { color: #fee2e2; }
266
271
  .appium-node-action-tooltip { display: inline-flex; }
267
272
  .appium-ai-model-warning { float: left; margin: 2px 6px 0 0; font-size: 16px; }
268
273
  </style>
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import { View } from '@element-plus/icons-vue';
2
4
  import type { AppiumNode } from '../types';
3
5
 
4
6
  defineProps<{
@@ -6,6 +8,7 @@ defineProps<{
6
8
  currentActivity?: string;
7
9
  loading?: boolean;
8
10
  }>();
11
+ const detailsVisible = ref(false);
9
12
  </script>
10
13
 
11
14
  <template>
@@ -14,6 +17,15 @@ defineProps<{
14
17
  element-loading-text="正在检测页面变化..."
15
18
  class="appium-node-detail"
16
19
  >
20
+ <div class="appium-node-summary">
21
+ <div class="appium-node-summary__text" v-if="node">
22
+ <strong :title="node.label">{{ node.label }}</strong>
23
+ <span>{{ node.className.split('.').pop() }}<template v-if="node.checkable"> · {{ node.checked === undefined ? '勾选状态未知' : node.checked ? '已勾选' : '未勾选' }}</template><template v-if="node.selector.matchCount"> · {{ node.selector.matchCount > 1 ? `定位重复 ${node.selector.matchCount}` : '定位唯一' }}</template></span>
24
+ </div>
25
+ <span v-else class="appium-node-summary__empty">请选择组件</span>
26
+ <el-button size="small" type="primary" :icon="View" :disabled="!node" @click="detailsVisible = true">详情</el-button>
27
+ </div>
28
+ <el-drawer v-model="detailsVisible" title="组件详情" size="520px" style="max-width: 94vw" append-to-body>
17
29
  <template v-if="node">
18
30
  <div class="appium-node-detail__title">{{ node.label }}</div>
19
31
  <el-descriptions :column="1" border size="small">
@@ -25,7 +37,6 @@ defineProps<{
25
37
  <el-descriptions-item v-if="node.checkable" label="checked">{{ node.checked === undefined ? '-' : String(node.checked) }}</el-descriptions-item>
26
38
  <el-descriptions-item label="当前 Activity">{{ currentActivity || '-' }}</el-descriptions-item>
27
39
  <el-descriptions-item label="selector">
28
- <span v-if="currentActivity">Activity {{ currentActivity }} · </span>
29
40
  {{ node.selector.strategy }} {{ node.selector.value || '' }}
30
41
  <el-tag
31
42
  v-if="node.selector.matchCount"
@@ -37,12 +48,10 @@ defineProps<{
37
48
  </el-descriptions-item>
38
49
  <el-descriptions-item label="推荐定位">
39
50
  <span v-if="node.contextSelector">
40
- <template v-if="currentActivity">Activity {{ currentActivity }} + </template>
41
51
  父级 {{ node.contextSelector.strategy }} {{ node.contextSelector.value || '' }}
42
52
  + 子级 {{ node.selector.strategy }} {{ node.selector.value || '' }}
43
53
  </span>
44
54
  <span v-else>
45
- <template v-if="currentActivity">Activity {{ currentActivity }} + </template>
46
55
  {{ node.selector.unique ? '当前 selector 唯一' : '使用当前 selector 或坐标兜底' }}
47
56
  </span>
48
57
  </el-descriptions-item>
@@ -54,7 +63,16 @@ defineProps<{
54
63
  </el-descriptions-item>
55
64
  </el-descriptions>
56
65
  </template>
57
- <el-empty v-else description="请选择组件" />
66
+ <p v-else>请选择组件</p>
67
+ </el-drawer>
58
68
 
59
69
  </div>
60
70
  </template>
71
+
72
+ <style scoped>
73
+ .appium-node-summary { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 44px; padding: 6px 0; }
74
+ .appium-node-summary__text { display: grid; gap: 2px; min-width: 0; font-size: 12px; }
75
+ .appium-node-summary__text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
76
+ .appium-node-summary__text span, .appium-node-summary__empty { color: var(--el-text-color-secondary); font-size: 12px; }
77
+ :deep(.el-descriptions__content) { overflow-wrap: anywhere; word-break: break-word; }
78
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, nextTick, shallowRef, watch } from 'vue';
3
- import { ElMessage } from 'element-plus';
4
- import { Aim, CopyDocument, FullScreen } from '@element-plus/icons-vue';
3
+ import { ElMessage, ElMessageBox } from 'element-plus';
4
+ import { Aim, CopyDocument, FullScreen, Close, Delete } from '@element-plus/icons-vue';
5
+ import { removeFlowSteps } from '../flow-remove';
5
6
  import type { AppiumRecordedStep } from '../types';
6
7
  import { createFlowClipboard } from '../flow-copy';
7
8
  import FlowCanvas from './FlowCanvas.vue';
@@ -37,6 +38,8 @@ const emit = defineEmits<{
37
38
  const expandedStepIndex = shallowRef<number | null>(null);
38
39
  const mergeConditionId = shallowRef('');
39
40
  const copyMode = shallowRef(false);
41
+ const deleteMode = shallowRef(false);
42
+ const deleting = shallowRef(false);
40
43
  const selectedCopyIndexes = shallowRef<number[]>([]);
41
44
  const flowDialogVisible = shallowRef(false);
42
45
  const mainResetViewToken = shallowRef(0);
@@ -126,7 +129,7 @@ function toggleCopySelection(index: number) {
126
129
  const step = props.steps[index];
127
130
  if (!step) return;
128
131
  if (step.type === 'launchApp' || step.type === 'clearAppData') {
129
- ElMessage.warning('App 初始化节点不能复制');
132
+ ElMessage.warning(deleteMode.value ? 'App 初始化节点不参与批量删除' : 'App 初始化节点不能复制');
130
133
  return;
131
134
  }
132
135
  if (selectedCopyIndexes.value.includes(index)) {
@@ -144,7 +147,8 @@ function toggleCopySelection(index: number) {
144
147
  try {
145
148
  createFlowClipboard(props.steps, candidate);
146
149
  } catch (error) {
147
- ElMessage.warning(error instanceof Error ? error.message : '当前节点不能一起复制');
150
+ const message = error instanceof Error ? error.message : '当前节点不能一起选择';
151
+ ElMessage.warning(deleteMode.value ? message.replace(/复制/g, '删除') : message);
148
152
  return;
149
153
  }
150
154
  selectedCopyIndexes.value = candidate;
@@ -166,12 +170,14 @@ function handleNodeClick(index: number) {
166
170
  }
167
171
 
168
172
  function startCopyMode() {
173
+ deleteMode.value = false;
169
174
  copyMode.value = true;
170
175
  selectedCopyIndexes.value = [];
171
176
  expandedStepIndex.value = null;
172
177
  }
173
178
 
174
179
  function cancelCopyMode() {
180
+ deleteMode.value = false;
175
181
  copyMode.value = false;
176
182
  selectedCopyIndexes.value = [];
177
183
  }
@@ -182,6 +188,33 @@ function copySelectedNodes() {
182
188
  cancelCopyMode();
183
189
  }
184
190
 
191
+ function startDeleteMode() {
192
+ if (props.mergeDisabled || props.removeDisabled) return;
193
+ startCopyMode();
194
+ deleteMode.value = true;
195
+ }
196
+
197
+ async function deleteSelectedNodes() {
198
+ if (deleting.value || props.mergeDisabled || props.removeDisabled || !selectedCopyIndexes.value.length) return;
199
+ const original = props.steps;
200
+ const next = removeFlowSteps(original, selectedCopyIndexes.value);
201
+ deleting.value = true;
202
+ try {
203
+ await ElMessageBox.confirm(`确认删除选中的 ${original.length - next.length} 个节点?所属分支子节点会一起删除,公共流程保留。`, '批量删除', {
204
+ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消', appendTo: 'body',
205
+ });
206
+ if (props.steps !== original || props.mergeDisabled || props.removeDisabled) {
207
+ ElMessage.warning('流程或运行状态已变化,请重新选择');
208
+ return;
209
+ }
210
+ emit('replaceSteps', next);
211
+ cancelCopyMode();
212
+ } catch { /* 取消确认时保留选中状态。 */ }
213
+ finally { deleting.value = false; }
214
+ }
215
+
216
+ watch(() => props.steps, () => { if (copyMode.value) cancelCopyMode(); });
217
+
185
218
  function resetMainFlowPosition() {
186
219
  mainResetViewToken.value += 1;
187
220
  }
@@ -221,40 +254,43 @@ function updateStep(index: number, step: AppiumRecordedStep) {
221
254
  <div class="appium-flow-toolbar">
222
255
  <template v-if="copyMode">
223
256
  <span class="appium-flow-toolbar__status">已选择 {{ selectedCopyIndexes.length }} 个节点</span>
224
- <el-button
257
+ <el-tooltip :content="deleteMode ? '删除选中' : '复制选中'" placement="top" :show-after="200"><span class="flow-toolbar-icon">
258
+ <el-button :aria-label="deleteMode ? '删除选中' : '复制选中'"
225
259
  size="small"
226
- type="primary"
227
- :disabled="!selectedCopyIndexes.length"
228
- :icon="CopyDocument"
229
- @click="copySelectedNodes"
230
- >
231
- 复制选中
232
- </el-button>
233
- <el-button size="small" @click="cancelCopyMode">取消</el-button>
260
+ :type="deleteMode ? 'danger' : 'primary'"
261
+ :loading="deleting"
262
+ :disabled="!selectedCopyIndexes.length || (deleteMode && (mergeDisabled || removeDisabled))"
263
+ :icon="deleteMode ? Delete : CopyDocument"
264
+ @click="deleteMode ? deleteSelectedNodes() : copySelectedNodes()"
265
+ />
266
+ </span></el-tooltip>
267
+ <el-tooltip content="取消" placement="top" :show-after="200"><span class="flow-toolbar-icon"><el-button size="small" :icon="Close" aria-label="取消" @click="cancelCopyMode" /></span></el-tooltip>
234
268
  </template>
235
- <el-button
236
- v-else
269
+ <el-tooltip v-else content="批量复制" placement="top" :show-after="200"><span class="flow-toolbar-icon">
270
+ <el-button aria-label="批量复制"
237
271
  size="small"
238
272
  :icon="CopyDocument"
239
273
  :disabled="!steps.length"
240
274
  @click="startCopyMode"
241
- >
242
- 批量复制
243
- </el-button>
244
- <el-button
275
+ />
276
+ </span></el-tooltip>
277
+ <el-tooltip v-if="!copyMode" content="批量删除" placement="top" :show-after="200"><span class="flow-toolbar-icon">
278
+ <el-button aria-label="批量删除" size="small" :icon="Delete" :disabled="!steps.length || mergeDisabled || removeDisabled" @click="startDeleteMode" />
279
+ </span></el-tooltip>
280
+ <el-tooltip content="还原位置" placement="top" :show-after="200"><span class="flow-toolbar-icon">
281
+ <el-button aria-label="还原位置"
245
282
  size="small"
246
283
  :icon="Aim"
247
284
  @click="resetMainFlowPosition"
248
- >
249
- 还原位置
250
- </el-button>
251
- <el-button
285
+ />
286
+ </span></el-tooltip>
287
+ <el-tooltip content="放大" placement="top" :show-after="200"><span class="flow-toolbar-icon">
288
+ <el-button aria-label="放大"
252
289
  size="small"
253
290
  :icon="FullScreen"
254
291
  @click="flowDialogVisible = true"
255
- >
256
- 放大
257
- </el-button>
292
+ />
293
+ </span></el-tooltip>
258
294
  </div>
259
295
 
260
296
  <FlowCanvas
@@ -262,6 +298,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
262
298
  :steps="steps"
263
299
  :expanded-step-index="expandedStepIndex"
264
300
  :copy-mode="copyMode"
301
+ :delete-mode="deleteMode"
265
302
  :selected-copy-indexes="selectedCopyIndexes"
266
303
  :disabled="disabled"
267
304
  :remove-disabled="removeDisabled"
@@ -301,26 +338,29 @@ function updateStep(index: number, step: AppiumRecordedStep) {
301
338
  <div class="appium-flow-toolbar appium-flow-dialog__toolbar">
302
339
  <template v-if="copyMode">
303
340
  <span class="appium-flow-toolbar__status">已选择 {{ selectedCopyIndexes.length }} 个节点</span>
304
- <el-button
341
+ <el-tooltip :content="deleteMode ? '删除选中' : '复制选中'" placement="top" :show-after="200"><span class="flow-toolbar-icon">
342
+ <el-button :aria-label="deleteMode ? '删除选中' : '复制选中'"
305
343
  size="small"
306
- type="primary"
307
- :disabled="!selectedCopyIndexes.length"
308
- :icon="CopyDocument"
309
- @click="copySelectedNodes"
310
- >
311
- 复制选中
312
- </el-button>
313
- <el-button size="small" @click="cancelCopyMode">取消</el-button>
344
+ :type="deleteMode ? 'danger' : 'primary'"
345
+ :loading="deleting"
346
+ :disabled="!selectedCopyIndexes.length || (deleteMode && (mergeDisabled || removeDisabled))"
347
+ :icon="deleteMode ? Delete : CopyDocument"
348
+ @click="deleteMode ? deleteSelectedNodes() : copySelectedNodes()"
349
+ />
350
+ </span></el-tooltip>
351
+ <el-tooltip content="取消" placement="top" :show-after="200"><span class="flow-toolbar-icon"><el-button size="small" :icon="Close" aria-label="取消" @click="cancelCopyMode" /></span></el-tooltip>
314
352
  </template>
315
- <el-button
316
- v-else
353
+ <el-tooltip v-else content="批量复制" placement="top" :show-after="200"><span class="flow-toolbar-icon">
354
+ <el-button aria-label="批量复制"
317
355
  size="small"
318
356
  :icon="CopyDocument"
319
357
  :disabled="!steps.length"
320
358
  @click="startCopyMode"
321
- >
322
- 批量复制
323
- </el-button>
359
+ />
360
+ </span></el-tooltip>
361
+ <el-tooltip v-if="!copyMode" content="批量删除" placement="top" :show-after="200"><span class="flow-toolbar-icon">
362
+ <el-button aria-label="批量删除" size="small" :icon="Delete" :disabled="!steps.length || mergeDisabled || removeDisabled" @click="startDeleteMode" />
363
+ </span></el-tooltip>
324
364
  </div>
325
365
  <FlowCanvas
326
366
  id="appium-flow-dialog"
@@ -328,6 +368,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
328
368
  :steps="steps"
329
369
  :expanded-step-index="expandedStepIndex"
330
370
  :copy-mode="copyMode"
371
+ :delete-mode="deleteMode"
331
372
  :selected-copy-indexes="selectedCopyIndexes"
332
373
  :disabled="disabled"
333
374
  :remove-disabled="removeDisabled"
@@ -361,3 +402,8 @@ function updateStep(index: number, step: AppiumRecordedStep) {
361
402
  @close="mergeConditionId = ''" @confirm="expandedStepIndex = null; emit('replaceSteps', $event)" />
362
403
  </div>
363
404
  </template>
405
+
406
+ <style scoped>
407
+ .flow-toolbar-icon { display: inline-flex; }
408
+ .flow-toolbar-icon .el-button { width: 30px; height: 30px; padding: 0; }
409
+ </style>
@@ -90,6 +90,7 @@ export type FlowGraphNodeData =
90
90
  expanded: boolean;
91
91
  selected: boolean;
92
92
  copyMode: boolean;
93
+ deleteMode?: boolean;
93
94
  disabled?: boolean;
94
95
  removeDisabled?: boolean;
95
96
  mergeDisabled?: boolean;
@@ -134,6 +135,7 @@ type BuildFlowGraphOptions = {
134
135
  expandedStepIndex: number | null;
135
136
  selectedCopyIndexes: number[];
136
137
  copyMode: boolean;
138
+ deleteMode?: boolean;
137
139
  disabled?: boolean;
138
140
  removeDisabled?: boolean;
139
141
  mergeDisabled?: boolean;
@@ -426,6 +428,7 @@ export function buildFlowGraph(
426
428
  expanded: options.expandedStepIndex === item.index,
427
429
  selected: options.isCopySelected(item.index),
428
430
  copyMode: options.copyMode,
431
+ deleteMode: options.deleteMode,
429
432
  disabled: item.step.type === 'launchApp' || item.step.type === 'clearAppData'
430
433
  ? options.isAppExecutionDisabled(item.step.type)
431
434
  : item.step.type === 'aiRecognition' ? options.isInsertActionDisabled('aiRecognition') : options.disabled,
@@ -4,6 +4,12 @@ type FlowTargetKey = 'yesTargetId' | 'noTargetId' | 'successTargetId' | 'failure
4
4
 
5
5
  const FLOW_TARGET_KEYS: FlowTargetKey[] = ['yesTargetId', 'noTargetId', 'successTargetId', 'failureTargetId'];
6
6
 
7
+ export function removeFlowSteps(steps: AppiumRecordedStep[], indexes: number[]) {
8
+ const ids = [...new Set(indexes)].sort((a, b) => b - a).map((index) => steps[index]?.id).filter(Boolean);
9
+ // 使用稳定 ID 逐个删除,避免前一次删除导致后续索引错位。
10
+ return ids.reduce((current, id) => removeFlowStep(current, current.findIndex((step) => step.id === id)), steps);
11
+ }
12
+
7
13
  function sameBranch(step: AppiumRecordedStep, removed: AppiumRecordedStep) {
8
14
  return Boolean(
9
15
  removed.flow?.parentConditionId