PyClickerRPA 0.5.0__tar.gz → 0.9.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 (115) hide show
  1. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PKG-INFO +22 -12
  2. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/PKG-INFO +22 -12
  3. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/SOURCES.txt +25 -0
  4. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/README.md +20 -10
  5. pyclickerrpa-0.9.0/autoclicker/actions.py +450 -0
  6. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/adb.py +36 -11
  7. pyclickerrpa-0.9.0/autoclicker/capture.py +165 -0
  8. pyclickerrpa-0.9.0/autoclicker/clicker.py +280 -0
  9. pyclickerrpa-0.9.0/autoclicker/controllers.py +594 -0
  10. pyclickerrpa-0.9.0/autoclicker/expr.py +177 -0
  11. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/flow_connection.py +3 -1
  12. pyclickerrpa-0.9.0/autoclicker/gui/flow_editor.py +602 -0
  13. pyclickerrpa-0.9.0/autoclicker/gui/flow_node.py +149 -0
  14. pyclickerrpa-0.9.0/autoclicker/gui/hotkeys.py +125 -0
  15. pyclickerrpa-0.9.0/autoclicker/gui/log_panel.py +39 -0
  16. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/main_window.py +1183 -1012
  17. pyclickerrpa-0.9.0/autoclicker/gui/property_panel.py +875 -0
  18. pyclickerrpa-0.9.0/autoclicker/gui/qt_runner.py +248 -0
  19. pyclickerrpa-0.9.0/autoclicker/gui/queue_panel.py +65 -0
  20. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/recorder.py +222 -33
  21. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/snippet_library.py +13 -4
  22. pyclickerrpa-0.9.0/autoclicker/gui/step_list_widget.py +257 -0
  23. pyclickerrpa-0.9.0/autoclicker/gui/task_widget.py +9 -0
  24. pyclickerrpa-0.9.0/autoclicker/gui/var_panel.py +32 -0
  25. pyclickerrpa-0.9.0/autoclicker/lint.py +203 -0
  26. pyclickerrpa-0.9.0/autoclicker/matcher.py +430 -0
  27. pyclickerrpa-0.9.0/autoclicker/migrations.py +187 -0
  28. pyclickerrpa-0.9.0/autoclicker/models.py +1417 -0
  29. pyclickerrpa-0.9.0/autoclicker/ocr.py +300 -0
  30. pyclickerrpa-0.9.0/autoclicker/recognizers.py +523 -0
  31. pyclickerrpa-0.9.0/autoclicker/reports.py +150 -0
  32. pyclickerrpa-0.9.0/autoclicker/resources.py +86 -0
  33. pyclickerrpa-0.9.0/autoclicker/run.py +127 -0
  34. pyclickerrpa-0.9.0/autoclicker/runner.py +1350 -0
  35. pyclickerrpa-0.9.0/autoclicker/sandbox.py +443 -0
  36. pyclickerrpa-0.9.0/autoclicker/task_queue.py +351 -0
  37. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__main__.py +11 -4
  38. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/assertions.py +99 -23
  39. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/case.py +25 -2
  40. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/loader.py +5 -2
  41. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/report.py +70 -22
  42. pyclickerrpa-0.9.0/autoclicker/testkit/runner.py +309 -0
  43. pyclickerrpa-0.9.0/autoclicker/testkit/visual.py +135 -0
  44. pyclickerrpa-0.9.0/autoclicker/web/run_manager.py +380 -0
  45. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/server.py +88 -45
  46. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/app.js +25 -5
  47. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/index.html +2 -0
  48. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/style.css +1 -0
  49. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/pyproject.toml +2 -2
  50. pyclickerrpa-0.9.0/tests/test_actions.py +612 -0
  51. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_adb.py +23 -8
  52. pyclickerrpa-0.9.0/tests/test_engine_integration.py +357 -0
  53. pyclickerrpa-0.9.0/tests/test_engine_services.py +867 -0
  54. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_flow_branch.py +26 -18
  55. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_lint.py +45 -0
  56. pyclickerrpa-0.9.0/tests/test_listener_lifecycle.py +384 -0
  57. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_matcher.py +88 -1
  58. pyclickerrpa-0.9.0/tests/test_migrations.py +128 -0
  59. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_models.py +4 -2
  60. pyclickerrpa-0.9.0/tests/test_phase4_integration.py +985 -0
  61. pyclickerrpa-0.9.0/tests/test_platform_recognition.py +1552 -0
  62. pyclickerrpa-0.9.0/tests/test_queue_entrypoints.py +289 -0
  63. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_recorder_gui.py +188 -6
  64. pyclickerrpa-0.9.0/tests/test_sandbox.py +738 -0
  65. pyclickerrpa-0.9.0/tests/test_structured_flow.py +497 -0
  66. pyclickerrpa-0.9.0/tests/test_structured_gui.py +223 -0
  67. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_testkit.py +34 -0
  68. pyclickerrpa-0.9.0/tests/test_testkit_contracts.py +393 -0
  69. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_web.py +22 -10
  70. pyclickerrpa-0.5.0/autoclicker/capture.py +0 -110
  71. pyclickerrpa-0.5.0/autoclicker/clicker.py +0 -212
  72. pyclickerrpa-0.5.0/autoclicker/expr.py +0 -94
  73. pyclickerrpa-0.5.0/autoclicker/gui/flow_editor.py +0 -524
  74. pyclickerrpa-0.5.0/autoclicker/gui/flow_node.py +0 -156
  75. pyclickerrpa-0.5.0/autoclicker/gui/hotkeys.py +0 -40
  76. pyclickerrpa-0.5.0/autoclicker/gui/qt_runner.py +0 -91
  77. pyclickerrpa-0.5.0/autoclicker/gui/task_widget.py +0 -666
  78. pyclickerrpa-0.5.0/autoclicker/lint.py +0 -163
  79. pyclickerrpa-0.5.0/autoclicker/matcher.py +0 -262
  80. pyclickerrpa-0.5.0/autoclicker/models.py +0 -523
  81. pyclickerrpa-0.5.0/autoclicker/ocr.py +0 -107
  82. pyclickerrpa-0.5.0/autoclicker/run.py +0 -73
  83. pyclickerrpa-0.5.0/autoclicker/runner.py +0 -971
  84. pyclickerrpa-0.5.0/autoclicker/testkit/runner.py +0 -147
  85. pyclickerrpa-0.5.0/autoclicker/testkit/visual.py +0 -94
  86. pyclickerrpa-0.5.0/autoclicker/web/run_manager.py +0 -181
  87. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/LICENSE +0 -0
  88. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
  89. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
  90. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/requires.txt +0 -0
  91. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
  92. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/__init__.py +0 -0
  93. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/app.py +0 -0
  94. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/diagnose.py +0 -0
  95. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/dpi.py +0 -0
  96. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/__init__.py +0 -0
  97. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/lint_dialog.py +0 -0
  98. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/monitor.py +0 -0
  99. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/preview_label.py +0 -0
  100. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/scheduler_widget.py +0 -0
  101. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/gui/testkit_widget.py +0 -0
  102. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/templatize.py +0 -0
  103. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__init__.py +0 -0
  104. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/pytest_plugin.py +0 -0
  105. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/__init__.py +0 -0
  106. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/__main__.py +0 -0
  107. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/scheduler.py +0 -0
  108. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/web/serialize.py +0 -0
  109. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/autoclicker/windows.py +0 -0
  110. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/setup.cfg +0 -0
  111. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_diagnose.py +0 -0
  112. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_diagnose_gui.py +0 -0
  113. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_runner.py +0 -0
  114. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_templatize.py +0 -0
  115. {pyclickerrpa-0.5.0 → pyclickerrpa-0.9.0}/tests/test_testkit_gui.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.9.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.9.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
 
@@ -8,19 +8,27 @@ PyClickerRPA.egg-info/entry_points.txt
8
8
  PyClickerRPA.egg-info/requires.txt
9
9
  PyClickerRPA.egg-info/top_level.txt
10
10
  autoclicker/__init__.py
11
+ autoclicker/actions.py
11
12
  autoclicker/adb.py
12
13
  autoclicker/app.py
13
14
  autoclicker/capture.py
14
15
  autoclicker/clicker.py
16
+ autoclicker/controllers.py
15
17
  autoclicker/diagnose.py
16
18
  autoclicker/dpi.py
17
19
  autoclicker/expr.py
18
20
  autoclicker/lint.py
19
21
  autoclicker/matcher.py
22
+ autoclicker/migrations.py
20
23
  autoclicker/models.py
21
24
  autoclicker/ocr.py
25
+ autoclicker/recognizers.py
26
+ autoclicker/reports.py
27
+ autoclicker/resources.py
22
28
  autoclicker/run.py
23
29
  autoclicker/runner.py
30
+ autoclicker/sandbox.py
31
+ autoclicker/task_queue.py
24
32
  autoclicker/templatize.py
25
33
  autoclicker/windows.py
26
34
  autoclicker/gui/__init__.py
@@ -29,15 +37,20 @@ autoclicker/gui/flow_editor.py
29
37
  autoclicker/gui/flow_node.py
30
38
  autoclicker/gui/hotkeys.py
31
39
  autoclicker/gui/lint_dialog.py
40
+ autoclicker/gui/log_panel.py
32
41
  autoclicker/gui/main_window.py
33
42
  autoclicker/gui/monitor.py
34
43
  autoclicker/gui/preview_label.py
44
+ autoclicker/gui/property_panel.py
35
45
  autoclicker/gui/qt_runner.py
46
+ autoclicker/gui/queue_panel.py
36
47
  autoclicker/gui/recorder.py
37
48
  autoclicker/gui/scheduler_widget.py
38
49
  autoclicker/gui/snippet_library.py
50
+ autoclicker/gui/step_list_widget.py
39
51
  autoclicker/gui/task_widget.py
40
52
  autoclicker/gui/testkit_widget.py
53
+ autoclicker/gui/var_panel.py
41
54
  autoclicker/testkit/__init__.py
42
55
  autoclicker/testkit/__main__.py
43
56
  autoclicker/testkit/assertions.py
@@ -56,16 +69,28 @@ autoclicker/web/server.py
56
69
  autoclicker/web/static/app.js
57
70
  autoclicker/web/static/index.html
58
71
  autoclicker/web/static/style.css
72
+ tests/test_actions.py
59
73
  tests/test_adb.py
60
74
  tests/test_diagnose.py
61
75
  tests/test_diagnose_gui.py
76
+ tests/test_engine_integration.py
77
+ tests/test_engine_services.py
62
78
  tests/test_flow_branch.py
63
79
  tests/test_lint.py
80
+ tests/test_listener_lifecycle.py
64
81
  tests/test_matcher.py
82
+ tests/test_migrations.py
65
83
  tests/test_models.py
84
+ tests/test_phase4_integration.py
85
+ tests/test_platform_recognition.py
86
+ tests/test_queue_entrypoints.py
66
87
  tests/test_recorder_gui.py
67
88
  tests/test_runner.py
89
+ tests/test_sandbox.py
90
+ tests/test_structured_flow.py
91
+ tests/test_structured_gui.py
68
92
  tests/test_templatize.py
69
93
  tests/test_testkit.py
94
+ tests/test_testkit_contracts.py
70
95
  tests/test_testkit_gui.py
71
96
  tests/test_web.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
 
@@ -0,0 +1,450 @@
1
+ """Action registry for the task engine.
2
+
3
+ Every built-in step has its own handler. Structured flow and recognition helpers
4
+ remain on the runner; handlers lazily import its result enum to avoid a cycle.
5
+ Third-party handlers can use enum values or custom strings, and replacing an
6
+ existing handler requires ``replace=True``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import random
11
+ import threading
12
+ from dataclasses import dataclass, field
13
+ from typing import TYPE_CHECKING, Any, Callable, Optional
14
+
15
+ from autoclicker.models import Step, StepType
16
+
17
+ if TYPE_CHECKING:
18
+ from autoclicker.runner import StepResult
19
+
20
+
21
+ @dataclass
22
+ class ExecutionContext:
23
+ """Shared backends and runner helpers available to an action."""
24
+ runner: Any
25
+ screenshot: Optional[Callable] = None
26
+ clicker: Any = None
27
+ matcher: Any = None
28
+ vars: dict[str, str] = field(default_factory=dict)
29
+ log: Callable[[str], None] = lambda msg: None
30
+ stop_event: Any = None
31
+ allow_unsafe_scripts: bool = False
32
+ load_template: Optional[Callable] = None
33
+
34
+ def sleep_ms(self, ms: int) -> None:
35
+ self.runner._sleep_ms(ms)
36
+
37
+ def interp(self, template: str) -> str:
38
+ return self.runner._interp_required(template)
39
+
40
+
41
+ ActionHandler = Callable[[ExecutionContext, Step], "StepResult"]
42
+ _ACTION_REGISTRY: dict[StepType | str, ActionHandler] = {}
43
+ _registry_lock = threading.RLock()
44
+ _installed = False
45
+
46
+
47
+ def _action_key(step_type: StepType | str) -> StepType | str:
48
+ if isinstance(step_type, StepType):
49
+ return step_type
50
+ if not isinstance(step_type, str):
51
+ raise TypeError("step_type must be a StepType or str")
52
+ # Do not call StepType(value): its legacy _missing_ maps unknowns to Wait.
53
+ return StepType._value2member_map_.get(step_type, step_type)
54
+
55
+
56
+ def register_action(step_type: StepType | str, *, replace: bool = False):
57
+ """Register a handler; reject duplicates unless explicitly replacing them."""
58
+ key = _action_key(step_type)
59
+
60
+ def decorator(fn: ActionHandler) -> ActionHandler:
61
+ if not callable(fn):
62
+ raise TypeError("action handler must be callable")
63
+ _install()
64
+ with _registry_lock:
65
+ if key in _ACTION_REGISTRY and not replace:
66
+ raise ValueError(f"Action already registered: {key}")
67
+ _ACTION_REGISTRY[key] = fn
68
+ return fn
69
+
70
+ return decorator
71
+
72
+
73
+ def registered_actions() -> list[StepType | str]:
74
+ """Return installed built-in and custom types in deterministic value order."""
75
+ _install()
76
+ with _registry_lock:
77
+ return sorted(_ACTION_REGISTRY,
78
+ key=lambda t: t.value if isinstance(t, StepType) else t)
79
+
80
+
81
+ def get_action(step_type: StepType | str) -> Optional[ActionHandler]:
82
+ """Return a handler, or None for an unknown type (never a Wait fallback)."""
83
+ key = _action_key(step_type)
84
+ _install()
85
+ with _registry_lock:
86
+ return _ACTION_REGISTRY.get(key)
87
+
88
+
89
+ def exec_click_image(ctx: ExecutionContext, step: Step) -> StepResult:
90
+ return ctx.runner._exec_image_poll(step, StepType.ClickImage)
91
+
92
+
93
+ def exec_click_text(ctx: ExecutionContext, step: Step) -> StepResult:
94
+ return ctx.runner._exec_click_text(step)
95
+
96
+
97
+ def exec_click_position(ctx: ExecutionContext, step: Step) -> StepResult:
98
+ from autoclicker.runner import StepResult
99
+ cx, cy = step.x, step.y
100
+ if step.varName == "use_var":
101
+ cx = int(ctx.vars.get("last_x", "0"))
102
+ cy = int(ctx.vars.get("last_y", "0"))
103
+ cx = ctx.runner._rand_offset(cx, step.randomOffset)
104
+ cy = ctx.runner._rand_offset(cy, step.randomOffset)
105
+ ctx.log(f" 点击 ({cx}, {cy})")
106
+ ctx.clicker.click(ctx.runner.target_hwnd, cx, cy)
107
+ return StepResult.Success
108
+
109
+
110
+ def exec_wait(ctx: ExecutionContext, step: Step) -> StepResult:
111
+ from autoclicker.runner import StepResult
112
+ ctx.log(f" 等待 {step.waitMs}ms")
113
+ ctx.sleep_ms(step.waitMs)
114
+ return StepResult.Success
115
+
116
+
117
+ def exec_loop(ctx: ExecutionContext, step: Step) -> StepResult:
118
+ from autoclicker.runner import StepResult
119
+ return StepResult.Success
120
+
121
+
122
+ def exec_if_image_found(ctx: ExecutionContext, step: Step) -> StepResult:
123
+ return ctx.runner._exec_if_image(step)
124
+
125
+
126
+ def exec_if_text_found(ctx: ExecutionContext, step: Step) -> StepResult:
127
+ result, ctx.runner._last_condition = ctx.runner._evaluate_condition(step, StepType.IfTextFound)
128
+ return result
129
+
130
+
131
+ def exec_if_else(ctx: ExecutionContext, step: Step) -> StepResult:
132
+ from autoclicker.runner import StepResult
133
+ ctx.log(f" If/Else 分支 (上次条件: {'true' if ctx.runner._last_condition else 'false'})")
134
+ return StepResult.Success
135
+
136
+
137
+ def exec_call_sub(ctx: ExecutionContext, step: Step) -> StepResult:
138
+ from autoclicker.runner import StepResult
139
+ if not step.subTaskPath:
140
+ ctx.log(" 错误: 未指定子任务路径")
141
+ return StepResult.Fail
142
+ # T1-4: the outputs mapping belongs to THIS CallSub step; the runner must
143
+ # not scan the task for "the first CallSub" (wrong for multiple/nested subs).
144
+ return ctx.runner._run_subtask(step.subTaskPath, step.params.get("outputs"))
145
+
146
+
147
+ def exec_set_var(ctx: ExecutionContext, step: Step) -> StepResult:
148
+ from autoclicker.runner import StepResult
149
+ value = step.varValue
150
+ typed = getattr(ctx.runner.task, "variable_mode", "legacy") == "typed"
151
+ if typed and isinstance(value, (bool, int, float, list)):
152
+ pass # native value assigned as-is
153
+ elif value.startswith("++"):
154
+ value = int(ctx.vars.get(step.varName, 0)) + 1
155
+ if not typed:
156
+ value = str(value)
157
+ elif value.startswith("--"):
158
+ value = int(ctx.vars.get(step.varName, 0)) - 1
159
+ if not typed:
160
+ value = str(value)
161
+ elif value.startswith("="):
162
+ from autoclicker import expr
163
+ try:
164
+ value = expr.safe_eval(value[1:], ctx.vars, typed=typed)
165
+ except Exception as e: # noqa: BLE001 - recoverable step failure
166
+ ctx.log(f" 表达式错误: {e}")
167
+ return StepResult.Fail
168
+ else:
169
+ value = ctx.interp(value)
170
+ ctx.vars[step.varName] = value
171
+ ctx.runner.cb.var_changed(step.varName, value)
172
+ ctx.log(f" 变量 {step.varName} = {value}")
173
+ return StepResult.Success
174
+
175
+
176
+ def exec_random_wait(ctx: ExecutionContext, step: Step) -> StepResult:
177
+ from autoclicker.runner import StepResult
178
+ lo, hi = step.waitMs, step.waitMsMax
179
+ if hi <= lo:
180
+ hi = lo + 1
181
+ actual = random.randint(lo, hi - 1)
182
+ ctx.log(f" 随机等待 {actual}ms (范围 {lo}~{hi})")
183
+ ctx.sleep_ms(actual)
184
+ return StepResult.Success
185
+
186
+
187
+ def exec_key_press(ctx: ExecutionContext, step: Step) -> StepResult:
188
+ from autoclicker.runner import StepResult
189
+ from autoclicker.clicker import parse_vk
190
+ vk = parse_vk(step.keyName)
191
+ if vk == 0:
192
+ ctx.log(f' 错误: 无效按键 "{step.keyName}"')
193
+ return StepResult.Fail
194
+ ctx.log(f" 按键: {step.keyName}")
195
+ ctx.clicker.key_press(vk)
196
+ return StepResult.Success
197
+
198
+
199
+ def exec_type_text(ctx: ExecutionContext, step: Step) -> StepResult:
200
+ from autoclicker.runner import StepResult
201
+ text = ctx.interp(step.text)
202
+ ctx.log(f' 输入文字: "{text}"')
203
+ ctx.clicker.type_text(text)
204
+ return StepResult.Success
205
+
206
+
207
+ def exec_hotkey(ctx: ExecutionContext, step: Step) -> StepResult:
208
+ from autoclicker.runner import StepResult
209
+ from autoclicker.clicker import parse_vk
210
+ mod, vk = parse_vk(step.modifierKey), parse_vk(step.keyName)
211
+ if vk == 0:
212
+ ctx.log(" 错误: 无效按键")
213
+ return StepResult.Fail
214
+ ctx.log(f" 快捷键: {step.modifierKey}+{step.keyName}")
215
+ ctx.clicker.hotkey(mod, vk)
216
+ return StepResult.Success
217
+
218
+
219
+ def exec_long_press(ctx: ExecutionContext, step: Step) -> StepResult:
220
+ from autoclicker.runner import StepResult
221
+ ctx.log(f" 长按 ({step.x}, {step.y}) {step.holdMs}ms")
222
+ ctx.clicker.long_press(ctx.runner.target_hwnd, step.x, step.y, step.holdMs)
223
+ return StepResult.Success
224
+
225
+
226
+ def exec_drag(ctx: ExecutionContext, step: Step) -> StepResult:
227
+ from autoclicker.runner import StepResult
228
+ ctx.log(f" 拖拽 ({step.x},{step.y}) → ({step.x2},{step.y2}) {step.durationMs}ms")
229
+ ctx.clicker.drag(ctx.runner.target_hwnd, step.x, step.y, step.x2, step.y2, step.durationMs)
230
+ return StepResult.Success
231
+
232
+
233
+ def exec_pinch(ctx: ExecutionContext, step: Step) -> StepResult:
234
+ from autoclicker.runner import StepResult
235
+ ctx.log(f" 捏合 中心({step.x},{step.y}) {step.pinchStartDist}→{step.pinchEndDist}")
236
+ ctx.clicker.pinch(ctx.runner.target_hwnd, step.x, step.y,
237
+ step.pinchStartDist, step.pinchEndDist, step.durationMs)
238
+ return StepResult.Success
239
+
240
+
241
+ def exec_wait_image(ctx: ExecutionContext, step: Step) -> StepResult:
242
+ return ctx.runner._exec_image_poll(step, StepType.WaitImage)
243
+
244
+
245
+ def exec_wait_image_gone(ctx: ExecutionContext, step: Step) -> StepResult:
246
+ return ctx.runner._exec_image_poll(step, StepType.WaitImageGone)
247
+
248
+
249
+ def exec_scroll(ctx: ExecutionContext, step: Step) -> StepResult:
250
+ from autoclicker.runner import StepResult
251
+ ctx.log(f" 滚轮 ({step.x},{step.y}) {step.scrollAmount}格")
252
+ ctx.clicker.scroll(ctx.runner.target_hwnd, step.x, step.y, step.scrollAmount)
253
+ return StepResult.Success
254
+
255
+
256
+ def exec_if_color_match(ctx: ExecutionContext, step: Step) -> StepResult:
257
+ from autoclicker.runner import StepResult
258
+ screen = ctx.screenshot()
259
+ if screen.size == 0 or step.x >= screen.shape[1] or step.y >= screen.shape[0]:
260
+ ctx.runner._last_condition = False
261
+ return StepResult.Success
262
+ ctx.runner._last_condition = ctx.runner._color_at_matches(
263
+ screen, step.x, step.y, step.targetColor, step.colorTolerance)
264
+ ctx.log(f" 条件(颜色): ({step.x},{step.y}) vs {step.targetColor} → "
265
+ f"{'true' if ctx.runner._last_condition else 'false'}")
266
+ return StepResult.Success
267
+
268
+
269
+ def exec_do_until(ctx: ExecutionContext, step: Step) -> StepResult:
270
+ from autoclicker.runner import StepResult
271
+ return StepResult.Success
272
+
273
+
274
+ def exec_merge_image(ctx: ExecutionContext, step: Step) -> StepResult:
275
+ return ctx.runner._exec_image_poll(step, StepType.MergeImage)
276
+
277
+
278
+ def exec_multi_drag(ctx: ExecutionContext, step: Step) -> StepResult:
279
+ return ctx.runner._exec_multidrag(step)
280
+
281
+
282
+ def exec_goto(ctx: ExecutionContext, step: Step) -> StepResult:
283
+ from autoclicker.runner import StepResult
284
+ return StepResult.Success
285
+
286
+
287
+ def exec_if_image_count(ctx: ExecutionContext, step: Step) -> StepResult:
288
+ return ctx.runner._exec_if_image_count(step)
289
+
290
+
291
+ def exec_if_var(ctx: ExecutionContext, step: Step) -> StepResult:
292
+ from autoclicker.runner import StepResult
293
+ val = ctx.vars.get(step.varName, "0")
294
+ ctx.runner._last_condition = ctx.runner._compare(val, step.compareOp, ctx.interp(step.compareValue))
295
+ ctx.log(f" 条件(变量): {step.varName}={val} {step.compareOp} {step.compareValue} → "
296
+ f"{'true' if ctx.runner._last_condition else 'false'}")
297
+ return StepResult.Success
298
+
299
+
300
+ def exec_click_all(ctx: ExecutionContext, step: Step) -> StepResult:
301
+ return ctx.runner._exec_click_all(step)
302
+
303
+
304
+ def exec_group_begin(ctx: ExecutionContext, step: Step) -> StepResult:
305
+ from autoclicker.runner import StepResult
306
+ return StepResult.Success
307
+
308
+
309
+ def exec_group_end(ctx: ExecutionContext, step: Step) -> StepResult:
310
+ from autoclicker.runner import StepResult
311
+ return StepResult.Success
312
+
313
+
314
+ def exec_while_condition(ctx: ExecutionContext, step: Step) -> StepResult:
315
+ from autoclicker.runner import StepResult
316
+ return StepResult.Success
317
+
318
+
319
+ def exec_read_text(ctx: ExecutionContext, step: Step) -> StepResult:
320
+ return ctx.runner._exec_read_text(step)
321
+
322
+
323
+ def exec_click_relative(ctx: ExecutionContext, step: Step) -> StepResult:
324
+ from autoclicker.runner import StepResult
325
+ bx = int(ctx.vars.get("last_x", "0")) + step.x
326
+ by = int(ctx.vars.get("last_y", "0")) + step.y
327
+ cx = ctx.runner._rand_offset(bx, step.randomOffset)
328
+ cy = ctx.runner._rand_offset(by, step.randomOffset)
329
+ ctx.log(f" 相对点击 last+({step.x},{step.y}) = ({cx}, {cy})")
330
+ ctx.clicker.click(ctx.runner.target_hwnd, cx, cy)
331
+ return StepResult.Success
332
+
333
+
334
+ def exec_http_request(ctx: ExecutionContext, step: Step) -> StepResult:
335
+ return ctx.runner._exec_http(step)
336
+
337
+
338
+ def exec_run_script(ctx: ExecutionContext, step: Step) -> StepResult:
339
+ return ctx.runner._exec_script(step)
340
+
341
+
342
+ def exec_drag_image(ctx: ExecutionContext, step: Step) -> StepResult:
343
+ return ctx.runner._exec_drag_image(step)
344
+
345
+
346
+ def exec_wait_stable(ctx: ExecutionContext, step: Step) -> StepResult:
347
+ return ctx.runner._exec_wait_stable(step)
348
+
349
+
350
+ def exec_activate_window(ctx: ExecutionContext, step: Step) -> StepResult:
351
+ return ctx.runner._exec_activate_window(step)
352
+
353
+
354
+ def exec_stop(ctx: ExecutionContext, step: Step) -> StepResult:
355
+ from autoclicker.runner import StepResult
356
+ ctx.log(" 结束任务")
357
+ ctx.runner._running = False
358
+ return StepResult.Success
359
+
360
+
361
+ def exec_if_block(ctx: ExecutionContext, step: Step) -> StepResult:
362
+ return ctx.runner._exec_block(step)
363
+
364
+
365
+ def exec_repeat_block(ctx: ExecutionContext, step: Step) -> StepResult:
366
+ return ctx.runner._exec_block(step)
367
+
368
+
369
+ def exec_while_block(ctx: ExecutionContext, step: Step) -> StepResult:
370
+ return ctx.runner._exec_block(step)
371
+
372
+
373
+ def exec_for_each(ctx: ExecutionContext, step: Step) -> StepResult:
374
+ return ctx.runner._exec_block(step)
375
+
376
+
377
+ def exec_break(ctx: ExecutionContext, step: Step) -> StepResult:
378
+ from autoclicker.runner import StepResult
379
+ if ctx.runner._loop_depth == 0:
380
+ ctx.log(" 退出循环 / 下一轮只能放在循环体内")
381
+ return StepResult.Abort
382
+ return StepResult.Break
383
+
384
+
385
+ def exec_continue(ctx: ExecutionContext, step: Step) -> StepResult:
386
+ from autoclicker.runner import StepResult
387
+ if ctx.runner._loop_depth == 0:
388
+ ctx.log(" 退出循环 / 下一轮只能放在循环体内")
389
+ return StepResult.Abort
390
+ return StepResult.Continue
391
+
392
+
393
+ _BUILTIN_ACTIONS: dict[StepType, ActionHandler] = {
394
+ StepType.ClickImage: exec_click_image,
395
+ StepType.ClickText: exec_click_text,
396
+ StepType.ClickPosition: exec_click_position,
397
+ StepType.Wait: exec_wait,
398
+ StepType.Loop: exec_loop,
399
+ StepType.IfImageFound: exec_if_image_found,
400
+ StepType.IfTextFound: exec_if_text_found,
401
+ StepType.IfElse: exec_if_else,
402
+ StepType.CallSub: exec_call_sub,
403
+ StepType.SetVar: exec_set_var,
404
+ StepType.RandomWait: exec_random_wait,
405
+ StepType.KeyPress: exec_key_press,
406
+ StepType.TypeText: exec_type_text,
407
+ StepType.Hotkey: exec_hotkey,
408
+ StepType.LongPress: exec_long_press,
409
+ StepType.Drag: exec_drag,
410
+ StepType.Pinch: exec_pinch,
411
+ StepType.WaitImage: exec_wait_image,
412
+ StepType.WaitImageGone: exec_wait_image_gone,
413
+ StepType.Scroll: exec_scroll,
414
+ StepType.IfColorMatch: exec_if_color_match,
415
+ StepType.DoUntil: exec_do_until,
416
+ StepType.MergeImage: exec_merge_image,
417
+ StepType.MultiDrag: exec_multi_drag,
418
+ StepType.Goto: exec_goto,
419
+ StepType.IfImageCount: exec_if_image_count,
420
+ StepType.IfVar: exec_if_var,
421
+ StepType.ClickAll: exec_click_all,
422
+ StepType.GroupBegin: exec_group_begin,
423
+ StepType.GroupEnd: exec_group_end,
424
+ StepType.WhileCondition: exec_while_condition,
425
+ StepType.ReadText: exec_read_text,
426
+ StepType.ClickRelative: exec_click_relative,
427
+ StepType.HttpRequest: exec_http_request,
428
+ StepType.RunScript: exec_run_script,
429
+ StepType.DragImage: exec_drag_image,
430
+ StepType.WaitStable: exec_wait_stable,
431
+ StepType.ActivateWindow: exec_activate_window,
432
+ StepType.Stop: exec_stop,
433
+ StepType.IfBlock: exec_if_block,
434
+ StepType.RepeatBlock: exec_repeat_block,
435
+ StepType.WhileBlock: exec_while_block,
436
+ StepType.ForEach: exec_for_each,
437
+ StepType.Break: exec_break,
438
+ StepType.Continue: exec_continue,
439
+ }
440
+
441
+
442
+ def _install() -> None:
443
+ """Install built-ins once, atomically, without overwriting user handlers."""
444
+ global _installed
445
+ with _registry_lock:
446
+ if _installed:
447
+ return
448
+ for step_type, handler in _BUILTIN_ACTIONS.items():
449
+ _ACTION_REGISTRY.setdefault(step_type, handler)
450
+ _installed = True