android-midscene-automation 0.1.26 → 0.1.27

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.
@@ -80,6 +80,7 @@ type ReplayResult = {
80
80
  reportId?: string;
81
81
  logPath?: string;
82
82
  htmlReportPath?: string;
83
+ softFailureCount?: number;
83
84
  };
84
85
 
85
86
  type ReplayStreamEvent =
@@ -118,7 +118,19 @@ function handlePaneReady() {
118
118
  void nextTick(() => scheduleResetView());
119
119
  }
120
120
 
121
- watch(() => props.steps.map((step) => step.id).join('|'), () => {
121
+ function flowStructureKey() {
122
+ return props.steps.map((step) => [
123
+ step.id,
124
+ step.flow?.nodeKind || '',
125
+ step.flow?.parentConditionId || '',
126
+ step.flow?.parentBranch || '',
127
+ step.flow?.yesTargetId || '',
128
+ step.flow?.noTargetId || '',
129
+ step.flow?.successTargetId || '',
130
+ ].join(':')).join('|');
131
+ }
132
+
133
+ watch(flowStructureKey, () => {
122
134
  measuredNodeHeights.value = {};
123
135
  void nextTick(scheduleNodeInternalsUpdate);
124
136
  });
@@ -168,6 +180,7 @@ function handleInsert(payload: {
168
180
  :nodes-draggable="false"
169
181
  :nodes-connectable="false"
170
182
  :elements-selectable="false"
183
+ :only-render-visible-elements="false"
171
184
  :pan-on-drag="true"
172
185
  :zoom-on-scroll="true"
173
186
  zoom-activation-key-code="Control"
@@ -1,8 +1,11 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue';
2
3
  import type { AppiumRecordedStep, AppiumSelector } from '../types';
4
+ import { normalizeVisualChangeConfig } from '../visual-change';
3
5
 
4
6
  type FlowKind = 'action' | 'condition' | 'assertion';
5
7
  type SwipeGesture = NonNullable<AppiumRecordedStep['swipe']>;
8
+ type VisualChangeConfig = NonNullable<AppiumRecordedStep['visualChange']>;
6
9
 
7
10
  const props = defineProps<{
8
11
  step: AppiumRecordedStep;
@@ -14,9 +17,21 @@ const emit = defineEmits<{
14
17
  update: [payload: { index: number; step: AppiumRecordedStep }];
15
18
  }>();
16
19
 
20
+ const visualChangeConfig = computed(() => normalizeVisualChangeConfig(props.step.visualChange));
21
+ const visualChangeRoleLabel = computed(() => (
22
+ visualChangeConfig.value.role === 'start'
23
+ ? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 开始节点`
24
+ : visualChangeConfig.value.role === 'end'
25
+ ? `${visualChangeConfig.value.pairLabel || '检测画面变化'} · 结束节点`
26
+ : '起止节点截图对比'
27
+ ));
28
+ const visualChangeConfigDisabled = computed(() => Boolean(
29
+ props.disabled || visualChangeConfig.value.role === 'end',
30
+ ));
31
+
17
32
  function defaultKind(): FlowKind {
18
33
  if (props.step.flow?.nodeKind) return props.step.flow.nodeKind;
19
- if (props.step.type === 'assertExists' || props.step.type === 'assertText') return 'assertion';
34
+ if (props.step.type === 'assertExists' || props.step.type === 'assertText' || props.step.type === 'visualChange') return 'assertion';
20
35
  return 'action';
21
36
  }
22
37
 
@@ -55,6 +70,25 @@ function patchSwipe(patch: Partial<SwipeGesture>) {
55
70
  });
56
71
  }
57
72
 
73
+ function patchVisualChange(patch: Partial<VisualChangeConfig>) {
74
+ patchStep({
75
+ visualChange: normalizeVisualChangeConfig({
76
+ ...normalizeVisualChangeConfig(props.step.visualChange),
77
+ ...patch,
78
+ }),
79
+ });
80
+ }
81
+
82
+ function patchVisualRegion(key: keyof VisualChangeConfig['region'], value: unknown) {
83
+ const current = normalizeVisualChangeConfig(props.step.visualChange);
84
+ patchVisualChange({
85
+ region: {
86
+ ...current.region,
87
+ [key]: Number(value),
88
+ },
89
+ });
90
+ }
91
+
58
92
  function timeoutLabel() {
59
93
  return props.step.type === 'longPress' ? '长按时间 ms' : '超时时间 ms';
60
94
  }
@@ -121,6 +155,91 @@ function patchTimeout(value: unknown) {
121
155
  @update:model-value="patchStep({ value: String($event) })"
122
156
  />
123
157
  </el-form-item>
158
+ <template v-if="step.type === 'visualChange'">
159
+ <div class="appium-flow-editor__grid">
160
+ <el-form-item label="检测标记">
161
+ <el-input :model-value="visualChangeRoleLabel" disabled />
162
+ </el-form-item>
163
+ <el-form-item label="检测目标">
164
+ <el-select
165
+ :model-value="visualChangeConfig.mode"
166
+ :disabled="visualChangeConfigDisabled"
167
+ @update:model-value="patchVisualChange({ mode: $event as VisualChangeConfig['mode'] })"
168
+ >
169
+ <el-option label="当前选中元素" value="selectedElement" />
170
+ <el-option label="手动区域" value="region" />
171
+ </el-select>
172
+ </el-form-item>
173
+ <el-form-item label="变化阈值 %">
174
+ <el-input-number
175
+ :model-value="visualChangeConfig.changeRatioThreshold"
176
+ :disabled="visualChangeConfigDisabled"
177
+ :min="0.01"
178
+ :max="100"
179
+ :step="0.1"
180
+ controls-position="right"
181
+ @update:model-value="patchVisualChange({ changeRatioThreshold: Number($event) })"
182
+ />
183
+ </el-form-item>
184
+ </div>
185
+ <div class="appium-flow-editor__grid appium-flow-editor__grid--visual">
186
+ <el-form-item label="X">
187
+ <el-input-number
188
+ :model-value="visualChangeConfig.region.x"
189
+ :disabled="visualChangeConfigDisabled"
190
+ :min="0"
191
+ :max="99999"
192
+ :precision="0"
193
+ controls-position="right"
194
+ @update:model-value="patchVisualRegion('x', $event)"
195
+ />
196
+ </el-form-item>
197
+ <el-form-item label="Y">
198
+ <el-input-number
199
+ :model-value="visualChangeConfig.region.y"
200
+ :disabled="visualChangeConfigDisabled"
201
+ :min="0"
202
+ :max="99999"
203
+ :precision="0"
204
+ controls-position="right"
205
+ @update:model-value="patchVisualRegion('y', $event)"
206
+ />
207
+ </el-form-item>
208
+ <el-form-item label="宽度">
209
+ <el-input-number
210
+ :model-value="visualChangeConfig.region.width"
211
+ :disabled="visualChangeConfigDisabled"
212
+ :min="1"
213
+ :max="99999"
214
+ :precision="0"
215
+ controls-position="right"
216
+ @update:model-value="patchVisualRegion('width', $event)"
217
+ />
218
+ </el-form-item>
219
+ <el-form-item label="高度">
220
+ <el-input-number
221
+ :model-value="visualChangeConfig.region.height"
222
+ :disabled="visualChangeConfigDisabled"
223
+ :min="1"
224
+ :max="99999"
225
+ :precision="0"
226
+ controls-position="right"
227
+ @update:model-value="patchVisualRegion('height', $event)"
228
+ />
229
+ </el-form-item>
230
+ <el-form-item label="像素容差">
231
+ <el-input-number
232
+ :model-value="visualChangeConfig.pixelmatchThreshold"
233
+ :disabled="visualChangeConfigDisabled"
234
+ :min="0"
235
+ :max="1"
236
+ :step="0.01"
237
+ controls-position="right"
238
+ @update:model-value="patchVisualChange({ pixelmatchThreshold: Number($event) })"
239
+ />
240
+ </el-form-item>
241
+ </div>
242
+ </template>
124
243
  <div v-if="step.type === 'swipe'" class="appium-flow-editor__grid appium-flow-editor__grid--swipe">
125
244
  <el-form-item label="起点 X">
126
245
  <el-input-number
@@ -222,7 +341,7 @@ function patchTimeout(value: unknown) {
222
341
  />
223
342
  </el-form-item>
224
343
  </div>
225
- <el-form-item label="可选步骤">
344
+ <el-form-item v-if="step.type !== 'visualChange'" label="可选步骤">
226
345
  <el-switch
227
346
  :model-value="Boolean(step.optional)"
228
347
  :disabled="disabled"
@@ -32,7 +32,11 @@ type InsertAction =
32
32
  | 'assertText'
33
33
  | 'waitDisappear'
34
34
  | 'waitActivity'
35
- | 'runScript';
35
+ | 'runScript'
36
+ | 'noop'
37
+ | 'visualChangeStart'
38
+ | 'visualChangeEnd'
39
+ | 'visualChange';
36
40
 
37
41
  const PASTE_COMMAND = '__paste_flow_nodes__';
38
42
 
@@ -87,6 +91,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
87
91
  { type: 'keyPower', label: '电源键' },
88
92
  { type: 'swipe', label: '滑动' },
89
93
  { type: 'pinch', label: '双指缩放' },
94
+ { type: 'noop', label: '空节点' },
90
95
  ],
91
96
  },
92
97
  {
@@ -103,6 +108,8 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
103
108
  { type: 'assertText', label: '断言文本' },
104
109
  { type: 'waitDisappear', label: '等待元素消失' },
105
110
  { type: 'waitActivity', label: '等待 Activity' },
111
+ { type: 'visualChangeStart', label: '检测画面变化开始节点' },
112
+ { type: 'visualChangeEnd', label: '检测画面变化结束节点' },
106
113
  ],
107
114
  },
108
115
  { title: '流程控制', actions: [{ type: 'runScript', label: '连接脚本' }] },
@@ -110,7 +117,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
110
117
 
111
118
  function defaultKind(step: AppiumRecordedStep): FlowKind {
112
119
  if (step.flow?.nodeKind) return step.flow.nodeKind;
113
- if (step.type === 'assertExists' || step.type === 'assertText') return 'assertion';
120
+ if (step.type === 'assertExists' || step.type === 'assertText' || step.type === 'visualChange') return 'assertion';
114
121
  return 'action';
115
122
  }
116
123
 
@@ -125,7 +132,8 @@ function typeLabel(step: AppiumRecordedStep) {
125
132
  waitFor: '等待出现', waitDisappear: '等待消失', assertExists: '断言存在',
126
133
  assertText: '断言文本', key: '按键', waitActivity: '等待 Activity', delay: '延时',
127
134
  coordinateTap: '坐标点击', swipe: '滑动', launchApp: '启动 APP', clearAppData: '清理 APP 缓存', longPress: '长按',
128
- pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图',
135
+ pinch: '双指缩放', runScript: '连接脚本', screenshot: '截图', visualChange: '检测画面变化',
136
+ noop: '空节点',
129
137
  };
130
138
  return labels[step.type] || step.type;
131
139
  }
@@ -139,6 +147,14 @@ function stepMeta(step: AppiumRecordedStep) {
139
147
  const swipe = step.swipe;
140
148
  return swipe ? `[${swipe.startX},${swipe.startY}] -> [${swipe.endX},${swipe.endY}]` : '';
141
149
  }
150
+ if (step.type === 'visualChange') {
151
+ const config = step.visualChange;
152
+ if (!config) return '';
153
+ if (config.role === 'start') return `${config.pairLabel || '检测画面变化'} · 开始 · 阈值 ${config.changeRatioThreshold}%`;
154
+ if (config.role === 'end') return `${config.pairLabel || '检测画面变化'} · 结束`;
155
+ return `${config.startStepId && config.endStepId ? '起止节点截图对比' : '请选择起止节点'} · 阈值 ${config.changeRatioThreshold}%`;
156
+ }
157
+ if (step.type === 'noop') return '';
142
158
  if (defaultKind(step) === 'condition' && step.value) {
143
159
  return `${step.flow?.textMatch === 'exact' ? '精准匹配' : '模糊匹配'}:${step.value}`;
144
160
  }
@@ -199,6 +215,12 @@ function isActionDisabled(action: InsertAction) {
199
215
  return Boolean(props.disabled && !props.allowedLockedActions?.includes(action));
200
216
  }
201
217
 
218
+ function canCopyStep(step: AppiumRecordedStep) {
219
+ return step.type !== 'launchApp'
220
+ && step.type !== 'clearAppData'
221
+ && step.visualChange?.role !== 'end';
222
+ }
223
+
202
224
  function insert(branch: BranchName, command: string | number | object) {
203
225
  if (command === PASTE_COMMAND) emit('paste', props.conditionIndex, branch);
204
226
  else emit('insertBranchAction', props.conditionIndex, branch, command as InsertAction);
@@ -275,7 +297,7 @@ function branchSplitPath() {
275
297
  </button>
276
298
  <span class="appium-flow-node__actions" @click.stop>
277
299
  <el-button
278
- v-if="!copyMode"
300
+ v-if="!copyMode && canCopyStep(item.step)"
279
301
  text
280
302
  size="small"
281
303
  :icon="CopyDocument"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, shallowRef } from 'vue';
2
+ import { computed, nextTick, shallowRef, watch } from 'vue';
3
3
  import { ElMessage } from 'element-plus';
4
4
  import { Aim, CopyDocument, FullScreen } from '@element-plus/icons-vue';
5
5
  import type { AppiumRecordedStep } from '../types';
@@ -35,6 +35,7 @@ const copyMode = shallowRef(false);
35
35
  const selectedCopyIndexes = shallowRef<number[]>([]);
36
36
  const flowDialogVisible = shallowRef(false);
37
37
  const mainResetViewToken = shallowRef(0);
38
+ const dialogResetViewToken = shallowRef(0);
38
39
 
39
40
  const insertActionGroups: FlowActionGroup[] = [
40
41
  {
@@ -56,6 +57,7 @@ const insertActionGroups: FlowActionGroup[] = [
56
57
  { type: 'keyPower', label: '电源键' },
57
58
  { type: 'swipe', label: '滑动' },
58
59
  { type: 'pinch', label: '双指缩放' },
60
+ { type: 'noop', label: '空节点' },
59
61
  { type: 'clearAppData', label: '清理 App 缓存' },
60
62
  { type: 'launchApp', label: '启动 App' },
61
63
  ],
@@ -74,6 +76,8 @@ const insertActionGroups: FlowActionGroup[] = [
74
76
  { type: 'assertText', label: '断言文本' },
75
77
  { type: 'waitDisappear', label: '等待元素消失' },
76
78
  { type: 'waitActivity', label: '等待 Activity' },
79
+ { type: 'visualChangeStart', label: '检测画面变化开始节点' },
80
+ { type: 'visualChangeEnd', label: '检测画面变化结束节点' },
77
81
  ],
78
82
  },
79
83
  {
@@ -169,6 +173,13 @@ function resetMainFlowPosition() {
169
173
  mainResetViewToken.value += 1;
170
174
  }
171
175
 
176
+ watch(flowDialogVisible, (visible) => {
177
+ if (!visible) return;
178
+ void nextTick(() => {
179
+ dialogResetViewToken.value += 1;
180
+ });
181
+ });
182
+
172
183
  function canOpenInsertMenu() {
173
184
  return !props.disabled || Boolean(props.allowedLockedActions?.length);
174
185
  }
@@ -308,6 +319,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
308
319
  :remove-disabled="removeDisabled"
309
320
  :launching-step-id="launchingStepId"
310
321
  :clipboard-count="clipboardCount"
322
+ :reset-view-token="dialogResetViewToken"
311
323
  :start-action-groups="startActionGroups"
312
324
  :main-action-groups="mainActionGroups"
313
325
  :can-open-insert-menu="canOpenInsertMenu()"
@@ -0,0 +1,151 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { Aim } from '@element-plus/icons-vue';
4
+ import type { AppiumVisualChangeConfig } from '../types';
5
+ import { normalizeVisualChangeConfig } from '../visual-change';
6
+
7
+ const props = defineProps<{
8
+ modelValue: boolean;
9
+ config: AppiumVisualChangeConfig;
10
+ hasSelectedElement: boolean;
11
+ pickingRegion?: boolean;
12
+ }>();
13
+
14
+ const emit = defineEmits<{
15
+ 'update:modelValue': [value: boolean];
16
+ 'update:config': [value: AppiumVisualChangeConfig];
17
+ pickRegion: [];
18
+ confirm: [];
19
+ }>();
20
+
21
+ const normalizedConfig = computed(() => normalizeVisualChangeConfig(props.config));
22
+
23
+ function patchConfig(patch: Partial<AppiumVisualChangeConfig>) {
24
+ emit('update:config', normalizeVisualChangeConfig({
25
+ ...normalizedConfig.value,
26
+ ...patch,
27
+ }));
28
+ }
29
+
30
+ function patchRegion(key: keyof AppiumVisualChangeConfig['region'], value: unknown) {
31
+ patchConfig({
32
+ region: {
33
+ ...normalizedConfig.value.region,
34
+ [key]: Number(value),
35
+ },
36
+ });
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <el-dialog
42
+ :model-value="modelValue"
43
+ title="检测画面变化"
44
+ width="480px"
45
+ top="9vh"
46
+ class="appium-visual-change-dialog"
47
+ append-to-body
48
+ draggable
49
+ :modal="false"
50
+ modal-penetrable
51
+ :close-on-click-modal="false"
52
+ :close-on-press-escape="false"
53
+ @update:model-value="emit('update:modelValue', $event)"
54
+ >
55
+ <el-form label-position="top" size="small" class="appium-visual-change-form">
56
+ <el-form-item label="检测目标">
57
+ <el-radio-group
58
+ :model-value="normalizedConfig.mode"
59
+ @update:model-value="patchConfig({ mode: $event as AppiumVisualChangeConfig['mode'] })"
60
+ >
61
+ <el-radio-button label="selectedElement" :disabled="!hasSelectedElement">
62
+ 当前选中元素
63
+ </el-radio-button>
64
+ <el-radio-button label="region">手动框选区域</el-radio-button>
65
+ </el-radio-group>
66
+ </el-form-item>
67
+
68
+ <el-alert
69
+ v-if="normalizedConfig.mode === 'selectedElement' && !hasSelectedElement"
70
+ type="warning"
71
+ :closable="false"
72
+ show-icon
73
+ title="当前没有可用的选中元素,请先选择组件或改为手动框选区域"
74
+ />
75
+
76
+ <el-form-item v-if="normalizedConfig.mode === 'region'" label="框选区域">
77
+ <el-button :icon="Aim" :loading="pickingRegion" @click="emit('pickRegion')">
78
+ {{ pickingRegion ? '请在设备预览拖拽' : '框选/拖动区域' }}
79
+ </el-button>
80
+ </el-form-item>
81
+
82
+ <div class="appium-visual-change-form__grid">
83
+ <el-form-item label="X">
84
+ <el-input-number
85
+ :model-value="normalizedConfig.region.x"
86
+ :min="0"
87
+ :max="99999"
88
+ :precision="0"
89
+ controls-position="right"
90
+ @update:model-value="patchRegion('x', $event)"
91
+ />
92
+ </el-form-item>
93
+ <el-form-item label="Y">
94
+ <el-input-number
95
+ :model-value="normalizedConfig.region.y"
96
+ :min="0"
97
+ :max="99999"
98
+ :precision="0"
99
+ controls-position="right"
100
+ @update:model-value="patchRegion('y', $event)"
101
+ />
102
+ </el-form-item>
103
+ <el-form-item label="宽度">
104
+ <el-input-number
105
+ :model-value="normalizedConfig.region.width"
106
+ :min="1"
107
+ :max="99999"
108
+ :precision="0"
109
+ controls-position="right"
110
+ @update:model-value="patchRegion('width', $event)"
111
+ />
112
+ </el-form-item>
113
+ <el-form-item label="高度">
114
+ <el-input-number
115
+ :model-value="normalizedConfig.region.height"
116
+ :min="1"
117
+ :max="99999"
118
+ :precision="0"
119
+ controls-position="right"
120
+ @update:model-value="patchRegion('height', $event)"
121
+ />
122
+ </el-form-item>
123
+ <el-form-item label="变化阈值 %">
124
+ <el-input-number
125
+ :model-value="normalizedConfig.changeRatioThreshold"
126
+ :min="0.01"
127
+ :max="100"
128
+ :step="0.1"
129
+ controls-position="right"
130
+ @update:model-value="patchConfig({ changeRatioThreshold: Number($event) })"
131
+ />
132
+ </el-form-item>
133
+ <el-form-item label="像素容差">
134
+ <el-input-number
135
+ :model-value="normalizedConfig.pixelmatchThreshold"
136
+ :min="0"
137
+ :max="1"
138
+ :step="0.01"
139
+ controls-position="right"
140
+ @update:model-value="patchConfig({ pixelmatchThreshold: Number($event) })"
141
+ />
142
+ </el-form-item>
143
+ </div>
144
+ </el-form>
145
+
146
+ <template #footer>
147
+ <el-button @click="emit('update:modelValue', false)">取消</el-button>
148
+ <el-button type="primary" @click="emit('confirm')">添加开始节点</el-button>
149
+ </template>
150
+ </el-dialog>
151
+ </template>
@@ -58,6 +58,33 @@ function wireContinuation(step: AppiumRecordedStep, targetId: string) {
58
58
  if (!step.flow.noTargetId) setFlowTarget(step, 'noTargetId', targetId);
59
59
  }
60
60
 
61
+ function isVisualChangeStart(step: AppiumRecordedStep) {
62
+ return step.visualChange?.role === 'start';
63
+ }
64
+
65
+ function isVisualChangeEnd(step: AppiumRecordedStep) {
66
+ return step.visualChange?.role === 'end';
67
+ }
68
+
69
+ function visualChangePairKeys(step: AppiumRecordedStep) {
70
+ const config = step.visualChange;
71
+ if (!config) return [];
72
+ return [
73
+ config.pairId ? `pair:${config.pairId}` : '',
74
+ step.id ? `start:${step.id}` : '',
75
+ config.startStepId ? `start:${config.startStepId}` : '',
76
+ config.pairLabel ? `label:${config.pairLabel}` : '',
77
+ ].filter(Boolean);
78
+ }
79
+
80
+ function hasCopiedVisualChangeStartForEnd(copiedSteps: AppiumRecordedStep[], endStep: AppiumRecordedStep) {
81
+ const endKeys = new Set(visualChangePairKeys(endStep));
82
+ return copiedSteps.some((step) => (
83
+ isVisualChangeStart(step)
84
+ && visualChangePairKeys(step).some((key) => endKeys.has(key))
85
+ ));
86
+ }
87
+
61
88
  function branchConditionForTarget(
62
89
  steps: AppiumRecordedStep[],
63
90
  target: PasteTarget,
@@ -65,10 +92,10 @@ function branchConditionForTarget(
65
92
  if (!target.branch) return undefined;
66
93
  const anchor = steps[target.afterIndex];
67
94
  if (!anchor) return undefined;
95
+ if (anchor.flow?.nodeKind === 'condition') return anchor;
68
96
  if (anchor.flow?.parentConditionId && anchor.flow.parentBranch === target.branch) {
69
97
  return steps.find((step) => step.id === anchor.flow?.parentConditionId);
70
98
  }
71
- if (anchor.flow?.nodeKind === 'condition') return anchor;
72
99
  return undefined;
73
100
  }
74
101
 
@@ -123,6 +150,9 @@ export function createFlowClipboard(steps: AppiumRecordedStep[], indexes: number
123
150
  if (copiedSteps.some((step) => step.type === 'launchApp')) {
124
151
  throw new Error('启动 APP 节点不能复制');
125
152
  }
153
+ if (copiedSteps.some((step) => isVisualChangeEnd(step) && !hasCopiedVisualChangeStartForEnd(copiedSteps, step))) {
154
+ throw new Error('检测画面变化结束节点不能单独复制');
155
+ }
126
156
  return { steps: clone(copiedSteps), rootIds };
127
157
  }
128
158
 
@@ -141,6 +171,11 @@ export function pasteFlowClipboard(
141
171
  throw new Error('目标分支不存在');
142
172
  }
143
173
 
174
+ const pairMap = new Map<string, string>();
175
+ clipboard.steps.filter(isVisualChangeStart).forEach((step) => {
176
+ const pairId = createId();
177
+ visualChangePairKeys(step).forEach((key) => pairMap.set(key, pairId));
178
+ });
144
179
  const copies = clipboard.steps.map((source) => {
145
180
  const step = clone(source);
146
181
  step.id = idMap.get(source.id) || createId();
@@ -168,6 +203,17 @@ export function pasteFlowClipboard(
168
203
  }
169
204
  }
170
205
  step.flow = Object.keys(flow).length ? flow : undefined;
206
+ if (step.visualChange) {
207
+ const nextPairId = visualChangePairKeys(source)
208
+ .map((key) => pairMap.get(key))
209
+ .find(Boolean);
210
+ step.visualChange = {
211
+ ...step.visualChange,
212
+ pairId: nextPairId || step.visualChange.pairId,
213
+ startStepId: idMap.get(step.visualChange.startStepId || '') || step.visualChange.startStepId,
214
+ endStepId: idMap.get(step.visualChange.endStepId || '') || step.visualChange.endStepId,
215
+ };
216
+ }
171
217
  return step;
172
218
  });
173
219