PyClickerRPA 0.5.0__tar.gz → 0.6.0__tar.gz

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 (76) hide show
  1. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PKG-INFO +22 -12
  2. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/PKG-INFO +22 -12
  3. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/SOURCES.txt +2 -0
  4. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/README.md +20 -10
  5. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_editor.py +76 -18
  6. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_node.py +21 -2
  7. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/main_window.py +2 -1
  8. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/task_widget.py +281 -36
  9. pyclickerrpa-0.6.0/autoclicker/lint.py +203 -0
  10. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/models.py +108 -9
  11. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/runner.py +272 -62
  12. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/pyproject.toml +2 -2
  13. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_lint.py +45 -0
  14. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_recorder_gui.py +7 -1
  15. pyclickerrpa-0.6.0/tests/test_structured_flow.py +377 -0
  16. pyclickerrpa-0.6.0/tests/test_structured_gui.py +223 -0
  17. pyclickerrpa-0.5.0/autoclicker/lint.py +0 -163
  18. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/LICENSE +0 -0
  19. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
  20. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
  21. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/requires.txt +0 -0
  22. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
  23. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/__init__.py +0 -0
  24. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/adb.py +0 -0
  25. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/app.py +0 -0
  26. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/capture.py +0 -0
  27. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/clicker.py +0 -0
  28. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/diagnose.py +0 -0
  29. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/dpi.py +0 -0
  30. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/expr.py +0 -0
  31. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/__init__.py +0 -0
  32. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_connection.py +0 -0
  33. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/hotkeys.py +0 -0
  34. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/lint_dialog.py +0 -0
  35. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/monitor.py +0 -0
  36. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/preview_label.py +0 -0
  37. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/qt_runner.py +0 -0
  38. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/recorder.py +0 -0
  39. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/scheduler_widget.py +0 -0
  40. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/snippet_library.py +0 -0
  41. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/testkit_widget.py +0 -0
  42. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/matcher.py +0 -0
  43. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/ocr.py +0 -0
  44. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/run.py +0 -0
  45. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/templatize.py +0 -0
  46. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/__init__.py +0 -0
  47. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/__main__.py +0 -0
  48. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/assertions.py +0 -0
  49. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/case.py +0 -0
  50. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/loader.py +0 -0
  51. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/pytest_plugin.py +0 -0
  52. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/report.py +0 -0
  53. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/runner.py +0 -0
  54. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/visual.py +0 -0
  55. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/__init__.py +0 -0
  56. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/__main__.py +0 -0
  57. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/run_manager.py +0 -0
  58. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/scheduler.py +0 -0
  59. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/serialize.py +0 -0
  60. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/server.py +0 -0
  61. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/app.js +0 -0
  62. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/index.html +0 -0
  63. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/style.css +0 -0
  64. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/windows.py +0 -0
  65. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/setup.cfg +0 -0
  66. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_adb.py +0 -0
  67. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_diagnose.py +0 -0
  68. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_diagnose_gui.py +0 -0
  69. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_flow_branch.py +0 -0
  70. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_matcher.py +0 -0
  71. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_models.py +0 -0
  72. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_runner.py +0 -0
  73. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_templatize.py +0 -0
  74. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_testkit.py +0 -0
  75. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_testkit_gui.py +0 -0
  76. {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_web.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.5.0
4
- Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, task health checks and failure attribution, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
3
+ Version: 0.6.0
4
+ Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, structured conditions and loops with array iteration, node flow editor, smart recorder, task health checks and failure attribution, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
5
5
  Author: Equinox
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
@@ -44,8 +44,8 @@ ADB) input — then turn those flows into a **visual test framework** with
44
44
  assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
45
45
  for remote runs.
46
46
 
47
- Python/PySide6, started as a port of a Qt/C++ AutoClicker (still reads/writes the
48
- same `.json` task format).
47
+ Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
48
+ remain supported; structured control-flow blocks are a Python-only extension.
49
49
 
50
50
  ## Install
51
51
 
@@ -91,14 +91,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
91
91
 
92
92
  ## Authoring (desktop GUI)
93
93
 
94
- Drag-and-drop **node flow graph** (condition nodes have **true/false branch
95
- ports** for visual if-else; a terminal **stop** node ends the flow) and an
96
- interoperable **step list** with a full
97
- per-step dialog; snippet library, task
98
- packages (`.zip` with templates), on-preview coordinate/color picking, template
99
- crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
100
- and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
101
- run visual test suites.
94
+ Drag-and-drop **node flow graph** and an interoperable **step list**, with
95
+ nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
96
+ **Break/Continue** — no step numbers required. Double-click a block to edit its
97
+ body or branches. Legacy condition nodes retain their true/false ports.
98
+
99
+ Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
100
+ item. Enter a custom JSON array or generate a numeric range, then reference the
101
+ current item in text actions, assignments, and conditions. For example, iterate
102
+ `[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
103
+ the actions, and return to the server-selection screen inside the same body.
104
+ Enable exact text matching so `1服` does not also match `11服`. A failed action
105
+ inside a block stops execution unless its failure policy explicitly skips it.
106
+
107
+ Also includes a snippet library, task packages (`.zip` with nested templates),
108
+ on-preview coordinate/color picking, template crop, single-step test run;
109
+ global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
110
+ **monitor/trigger** with tray icon; a **Test** tab to load and run visual test
111
+ suites.
102
112
 
103
113
  ### Smart recorder
104
114
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.5.0
4
- Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, task health checks and failure attribution, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
3
+ Version: 0.6.0
4
+ Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, structured conditions and loops with array iteration, node flow editor, smart recorder, task health checks and failure attribution, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
5
5
  Author: Equinox
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
@@ -44,8 +44,8 @@ ADB) input — then turn those flows into a **visual test framework** with
44
44
  assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
45
45
  for remote runs.
46
46
 
47
- Python/PySide6, started as a port of a Qt/C++ AutoClicker (still reads/writes the
48
- same `.json` task format).
47
+ Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
48
+ remain supported; structured control-flow blocks are a Python-only extension.
49
49
 
50
50
  ## Install
51
51
 
@@ -91,14 +91,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
91
91
 
92
92
  ## Authoring (desktop GUI)
93
93
 
94
- Drag-and-drop **node flow graph** (condition nodes have **true/false branch
95
- ports** for visual if-else; a terminal **stop** node ends the flow) and an
96
- interoperable **step list** with a full
97
- per-step dialog; snippet library, task
98
- packages (`.zip` with templates), on-preview coordinate/color picking, template
99
- crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
100
- and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
101
- run visual test suites.
94
+ Drag-and-drop **node flow graph** and an interoperable **step list**, with
95
+ nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
96
+ **Break/Continue** — no step numbers required. Double-click a block to edit its
97
+ body or branches. Legacy condition nodes retain their true/false ports.
98
+
99
+ Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
100
+ item. Enter a custom JSON array or generate a numeric range, then reference the
101
+ current item in text actions, assignments, and conditions. For example, iterate
102
+ `[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
103
+ the actions, and return to the server-selection screen inside the same body.
104
+ Enable exact text matching so `1服` does not also match `11服`. A failed action
105
+ inside a block stops execution unless its failure policy explicitly skips it.
106
+
107
+ Also includes a snippet library, task packages (`.zip` with nested templates),
108
+ on-preview coordinate/color picking, template crop, single-step test run;
109
+ global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
110
+ **monitor/trigger** with tray icon; a **Test** tab to load and run visual test
111
+ suites.
102
112
 
103
113
  ### Smart recorder
104
114
 
@@ -65,6 +65,8 @@ tests/test_matcher.py
65
65
  tests/test_models.py
66
66
  tests/test_recorder_gui.py
67
67
  tests/test_runner.py
68
+ tests/test_structured_flow.py
69
+ tests/test_structured_gui.py
68
70
  tests/test_templatize.py
69
71
  tests/test_testkit.py
70
72
  tests/test_testkit_gui.py
@@ -6,8 +6,8 @@ ADB) input — then turn those flows into a **visual test framework** with
6
6
  assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
7
7
  for remote runs.
8
8
 
9
- Python/PySide6, started as a port of a Qt/C++ AutoClicker (still reads/writes the
10
- same `.json` task format).
9
+ Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
10
+ remain supported; structured control-flow blocks are a Python-only extension.
11
11
 
12
12
  ## Install
13
13
 
@@ -53,14 +53,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
53
53
 
54
54
  ## Authoring (desktop GUI)
55
55
 
56
- Drag-and-drop **node flow graph** (condition nodes have **true/false branch
57
- ports** for visual if-else; a terminal **stop** node ends the flow) and an
58
- interoperable **step list** with a full
59
- per-step dialog; snippet library, task
60
- packages (`.zip` with templates), on-preview coordinate/color picking, template
61
- crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
62
- and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
63
- run visual test suites.
56
+ Drag-and-drop **node flow graph** and an interoperable **step list**, with
57
+ nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
58
+ **Break/Continue** — no step numbers required. Double-click a block to edit its
59
+ body or branches. Legacy condition nodes retain their true/false ports.
60
+
61
+ Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
62
+ item. Enter a custom JSON array or generate a numeric range, then reference the
63
+ current item in text actions, assignments, and conditions. For example, iterate
64
+ `[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
65
+ the actions, and return to the server-selection screen inside the same body.
66
+ Enable exact text matching so `1服` does not also match `11服`. A failed action
67
+ inside a block stops execution unless its failure policy explicitly skips it.
68
+
69
+ Also includes a snippet library, task packages (`.zip` with nested templates),
70
+ on-preview coordinate/color picking, template crop, single-step test run;
71
+ global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
72
+ **monitor/trigger** with tray icon; a **Test** tab to load and run visual test
73
+ suites.
64
74
 
65
75
  ### Smart recorder
66
76
 
@@ -13,10 +13,10 @@ from collections import deque
13
13
  from PySide6.QtCore import QLineF, QPointF, Qt, Signal
14
14
  from PySide6.QtGui import QColor, QKeySequence, QPainter, QPen
15
15
  from PySide6.QtWidgets import (
16
- QDialog, QGraphicsScene, QGraphicsView, QInputDialog, QMenu,
16
+ QDialog, QGraphicsScene, QGraphicsView, QInputDialog, QMenu, QMessageBox,
17
17
  )
18
18
 
19
- from autoclicker.models import Step, StepType, Task
19
+ from autoclicker.models import Step, StepType, Task, BLOCK_TYPES, iter_steps
20
20
  from autoclicker.gui.flow_node import FlowNode, FlowPort
21
21
  from autoclicker.gui.flow_connection import FlowConnection
22
22
 
@@ -29,16 +29,13 @@ _ADD_MENU = {
29
29
  "手势": [("拖拽", StepType.Drag), ("多点拖拽", StepType.MultiDrag),
30
30
  ("捏合", StepType.Pinch), ("滚轮", StepType.Scroll)],
31
31
  "识别/变量": [("读取文字→变量", StepType.ReadText), ("设置变量", StepType.SetVar)],
32
- "条件": [("条件(图片)", StepType.IfImageFound), ("条件(文字)", StepType.IfTextFound),
33
- ("条件(颜色)", StepType.IfColorMatch), ("条件(图片数量)", StepType.IfImageCount),
34
- ("条件(变量)", StepType.IfVar), ("If/Else", StepType.IfElse)],
32
+ "条件/循环": [("如果 / 否则", StepType.IfBlock), ("遍历数组", StepType.ForEach),
33
+ ("重复指定次数", StepType.RepeatBlock), ("条件循环 (While)", StepType.WhileBlock),
34
+ ("退出当前循环", StepType.Break), ("进入下一轮", StepType.Continue)],
35
35
  "流程": [("等待", StepType.Wait), ("随机等待", StepType.RandomWait),
36
36
  ("等待画面稳定", StepType.WaitStable),
37
37
  ("等待图片出现", StepType.WaitImage), ("等待图片消失", StepType.WaitImageGone),
38
- ("循环直到", StepType.DoUntil), ("While 条件", StepType.WhileCondition),
39
- ("跳转", StepType.Goto), ("调用子任务", StepType.CallSub),
40
- ("分组开始", StepType.GroupBegin), ("分组结束", StepType.GroupEnd),
41
- ("结束任务", StepType.Stop)],
38
+ ("调用子任务", StepType.CallSub), ("结束任务", StepType.Stop)],
42
39
  "窗口/高级": [("刷新/激活窗口", StepType.ActivateWindow), ("合成拖拽", StepType.MergeImage),
43
40
  ("HTTP 请求", StepType.HttpRequest), ("运行脚本(Python)", StepType.RunScript)],
44
41
  }
@@ -143,14 +140,12 @@ class FlowEditor(QGraphicsView):
143
140
  self._drag_line = None
144
141
  item = self.itemAt(event.position().toPoint())
145
142
  if isinstance(item, FlowPort) and item.port_type == FlowPort.INPUT and item is not self._drag_start_port:
146
- conn = self.connect_nodes(self._drag_start_port, item)
147
- if conn:
148
- from_node = self._drag_start_port.parentItem()
149
- to_node = item.parentItem()
150
- if to_node.pos().y() < from_node.pos().y(): # back-edge
151
- count, ok = QInputDialog.getInt(self, "循环次数", "重复次数 (-1 = 无限):", -1, -1, 99999, 1)
152
- if ok:
153
- conn.set_repeat_count(count)
143
+ from_node = self._drag_start_port.parentItem()
144
+ to_node = item.parentItem()
145
+ if from_node in self._reachable(to_node, set()):
146
+ QMessageBox.information(self, "使用循环块", "无需连接回线:选中需要重复的步骤,右键选择「将选中步骤包进…」。")
147
+ else:
148
+ self.connect_nodes(self._drag_start_port, item)
154
149
  self._drag_start_port = None
155
150
  return
156
151
  super().mouseReleaseEvent(event)
@@ -237,6 +232,10 @@ class FlowEditor(QGraphicsView):
237
232
  menu.addAction("删除", self._delete_selected)
238
233
  if self.selected_nodes():
239
234
  menu.addAction("复制选中节点 (Ctrl+C)", self._copy_selected)
235
+ wrap_menu = menu.addMenu("将选中步骤包进…")
236
+ for label, st in [("遍历数组", StepType.ForEach), ("重复指定次数", StepType.RepeatBlock),
237
+ ("条件循环", StepType.WhileBlock), ("如果 / 否则", StepType.IfBlock)]:
238
+ wrap_menu.addAction(label, lambda st=st: self.wrap_selection(st))
240
239
  if self._clip and self._clip["steps"]:
241
240
  menu.addAction("粘贴 (Ctrl+V)", self._paste_clip)
242
241
  if node or conn or self.selected_nodes() or self._clip:
@@ -246,9 +245,68 @@ class FlowEditor(QGraphicsView):
246
245
  for group, items in _ADD_MENU.items():
247
246
  sub = add_menu.addMenu(group)
248
247
  for label, st in items:
249
- sub.addAction(label, lambda st=st, p=scene_pos: self.add_node(Step(type=st), p))
248
+ sub.addAction(label, lambda st=st, p=scene_pos: self._add_configured_node(st, p))
250
249
  menu.exec(event.globalPos())
251
250
 
251
+ def _add_configured_node(self, step_type, pos):
252
+ from autoclicker.gui.task_widget import StepEditorDialog, new_step
253
+ dlg = StepEditorDialog(new_step(step_type), self)
254
+ if dlg.exec() == QDialog.Accepted:
255
+ self.add_node(dlg.result_step(), pos)
256
+
257
+ def wrap_selection(self, step_type):
258
+ from autoclicker.gui.task_widget import StepEditorDialog, new_step
259
+ selected = set(self.selected_nodes())
260
+ if not selected or step_type not in BLOCK_TYPES:
261
+ return
262
+ starts = [n for n in selected if not any(
263
+ c.from_port.parentItem() in selected for c in n.input_port.connections)]
264
+ ordered = []
265
+ node = starts[0] if len(starts) == 1 else None
266
+ while node is not None and node not in ordered:
267
+ ordered.append(node)
268
+ edges = [c for p in node.output_ports() for c in p.connections
269
+ if c.to_port.parentItem() in selected]
270
+ node = edges[0].to_port.parentItem() if len(edges) == 1 else None
271
+ incoming = [c for n in selected for c in n.input_port.connections
272
+ if c.from_port.parentItem() not in selected]
273
+ outgoing = [c for n in selected for p in n.output_ports() for c in p.connections
274
+ if c.to_port.parentItem() not in selected]
275
+ internal = [c for n in selected for p in n.output_ports() for c in p.connections
276
+ if c.to_port.parentItem() in selected]
277
+ linear = (len(ordered) == len(selected) and len(internal) == len(selected) - 1
278
+ and all(c.to_port.parentItem() is ordered[0] for c in incoming)
279
+ and all(c.from_port.parentItem() is ordered[-1] for c in outgoing)
280
+ and all(c.repeat_count == 1 for c in internal + incoming + outgoing)
281
+ and not any(n.is_condition() for n in selected))
282
+ legacy = {StepType.Loop, StepType.Goto, StepType.DoUntil, StepType.WhileCondition,
283
+ StepType.IfImageFound, StepType.IfTextFound, StepType.IfColorMatch,
284
+ StepType.IfImageCount, StepType.IfVar, StepType.IfElse,
285
+ StepType.GroupBegin, StepType.GroupEnd}
286
+ if not linear or any(s.type in legacy or s.failGoto >= 0
287
+ for s in iter_steps([n.step() for n in self.nodes])):
288
+ QMessageBox.warning(self, "无法包进块", "请选择连续、无分叉的步骤;流程中的旧式步骤号跳转须先改为条件/循环块。")
289
+ return
290
+ block = new_step(step_type)
291
+ block.body = [copy.deepcopy(n.step()) for n in ordered]
292
+ dlg = StepEditorDialog(block, self)
293
+ if dlg.exec() != QDialog.Accepted:
294
+ return
295
+ block = dlg.result_step()
296
+ pos = ordered[0].pos()
297
+ sources = [c.from_port for c in incoming]
298
+ targets = [c.to_port for c in outgoing]
299
+ self._scene.clearSelection()
300
+ for n in selected:
301
+ n.setSelected(True)
302
+ self._delete_selected()
303
+ wrapped = self.add_node(block, pos)
304
+ for source in sources:
305
+ self.connect_nodes(source, wrapped.input_port)
306
+ for target in targets:
307
+ self.connect_nodes(wrapped.output_port, target)
308
+ wrapped.setSelected(True)
309
+
252
310
  # ---- helpers ----
253
311
  def _node_at(self, view_pos) -> FlowNode | None:
254
312
  item = self.itemAt(view_pos)
@@ -7,9 +7,15 @@ from PySide6.QtCore import QRectF, Qt
7
7
  from PySide6.QtGui import QColor, QPen
8
8
  from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsTextItem
9
9
 
10
- from autoclicker.models import Step, StepType
10
+ from autoclicker.models import Step, StepType, BLOCK_TYPES
11
11
 
12
12
  _COLORS = {
13
+ StepType.IfBlock: (123, 65, 156),
14
+ StepType.ForEach: (0, 121, 107),
15
+ StepType.RepeatBlock: (192, 92, 40),
16
+ StepType.WhileBlock: (192, 92, 40),
17
+ StepType.Break: (96, 125, 139),
18
+ StepType.Continue: (96, 125, 139),
13
19
  StepType.ClickImage: (66, 133, 244),
14
20
  StepType.ClickText: (52, 168, 83),
15
21
  StepType.ClickPosition: (251, 188, 4),
@@ -141,7 +147,20 @@ class FlowNode(QGraphicsItem):
141
147
  painter.drawRoundedRect(QRectF(0, 0, self.WIDTH, self.HEIGHT), 8, 8)
142
148
 
143
149
  def _update_label(self):
144
- self._label.setPlainText(self._step.display_name())
150
+ label = self._step.display_name()
151
+ if self._step.type in BLOCK_TYPES:
152
+ title = {StepType.IfBlock: "如果 / 否则", StepType.ForEach: "遍历数组",
153
+ StepType.RepeatBlock: "重复指定次数", StepType.WhileBlock: "条件循环"}[self._step.type]
154
+ count = len(self._step.body) + len(self._step.elseBody)
155
+ self._label.setPlainText(f"{title} · {count} 步\n双击编辑块内步骤")
156
+ body = "\n".join(f" {s.display_name()}" for s in self._step.body)
157
+ other = "\n".join(f" {s.display_name()}" for s in self._step.elseBody)
158
+ label += f"\n执行步骤:\n{body or ' (空)'}"
159
+ if self._step.type == StepType.IfBlock:
160
+ label += f"\n否则:\n{other or ' (空)'}"
161
+ else:
162
+ self._label.setPlainText(label)
163
+ self.setToolTip(label)
145
164
  self._label.setPos(8, 8)
146
165
 
147
166
  def _color(self) -> QColor:
@@ -955,7 +955,8 @@ class MainWindow(QMainWindow):
955
955
  # ---- task package (.zip: task.json + images/) ----
956
956
  def _iter_image_slots(self, task: Task):
957
957
  """Yield (get, set) callables for every image path field in the task."""
958
- for s in task.steps:
958
+ from autoclicker.models import iter_steps
959
+ for s in iter_steps(task.steps):
959
960
  if s.imagePath:
960
961
  yield (lambda s=s: s.imagePath), (lambda v, s=s: setattr(s, "imagePath", v))
961
962
  for i in range(len(s.imagePaths)):