PyClickerRPA 0.6.0__tar.gz → 0.11.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 (116) hide show
  1. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PKG-INFO +34 -1
  2. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/PKG-INFO +34 -1
  3. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/SOURCES.txt +27 -0
  4. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/README.md +33 -0
  5. pyclickerrpa-0.11.0/autoclicker/actions.py +455 -0
  6. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/adb.py +72 -15
  7. pyclickerrpa-0.11.0/autoclicker/capture.py +165 -0
  8. pyclickerrpa-0.11.0/autoclicker/clicker.py +362 -0
  9. pyclickerrpa-0.11.0/autoclicker/controllers.py +780 -0
  10. pyclickerrpa-0.11.0/autoclicker/expr.py +177 -0
  11. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/flow_connection.py +3 -1
  12. pyclickerrpa-0.11.0/autoclicker/gui/flow_editor.py +602 -0
  13. pyclickerrpa-0.11.0/autoclicker/gui/flow_node.py +149 -0
  14. pyclickerrpa-0.11.0/autoclicker/gui/hotkeys.py +125 -0
  15. pyclickerrpa-0.11.0/autoclicker/gui/log_panel.py +39 -0
  16. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/main_window.py +1183 -1013
  17. pyclickerrpa-0.6.0/autoclicker/gui/task_widget.py → pyclickerrpa-0.11.0/autoclicker/gui/property_panel.py +875 -911
  18. pyclickerrpa-0.11.0/autoclicker/gui/qt_runner.py +248 -0
  19. pyclickerrpa-0.11.0/autoclicker/gui/queue_panel.py +65 -0
  20. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/recorder.py +222 -33
  21. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/snippet_library.py +13 -4
  22. pyclickerrpa-0.11.0/autoclicker/gui/step_list_widget.py +257 -0
  23. pyclickerrpa-0.11.0/autoclicker/gui/task_widget.py +9 -0
  24. pyclickerrpa-0.11.0/autoclicker/gui/var_panel.py +32 -0
  25. pyclickerrpa-0.11.0/autoclicker/matcher.py +430 -0
  26. pyclickerrpa-0.11.0/autoclicker/migrations.py +196 -0
  27. pyclickerrpa-0.11.0/autoclicker/models.py +1463 -0
  28. pyclickerrpa-0.11.0/autoclicker/ocr.py +300 -0
  29. pyclickerrpa-0.11.0/autoclicker/recognizers.py +523 -0
  30. pyclickerrpa-0.11.0/autoclicker/reports.py +150 -0
  31. pyclickerrpa-0.11.0/autoclicker/resources.py +126 -0
  32. pyclickerrpa-0.11.0/autoclicker/run.py +127 -0
  33. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/runner.py +653 -342
  34. pyclickerrpa-0.11.0/autoclicker/sandbox.py +443 -0
  35. pyclickerrpa-0.11.0/autoclicker/task_queue.py +351 -0
  36. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/__main__.py +11 -4
  37. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/assertions.py +99 -23
  38. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/case.py +25 -2
  39. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/loader.py +5 -2
  40. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/report.py +70 -22
  41. pyclickerrpa-0.11.0/autoclicker/testkit/runner.py +309 -0
  42. pyclickerrpa-0.11.0/autoclicker/testkit/visual.py +135 -0
  43. pyclickerrpa-0.11.0/autoclicker/web/run_manager.py +380 -0
  44. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/server.py +88 -45
  45. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/app.js +25 -5
  46. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/index.html +2 -0
  47. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/style.css +1 -0
  48. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/pyproject.toml +1 -1
  49. pyclickerrpa-0.11.0/tests/test_actions.py +613 -0
  50. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_adb.py +23 -8
  51. pyclickerrpa-0.11.0/tests/test_controller_strategies.py +203 -0
  52. pyclickerrpa-0.11.0/tests/test_engine_integration.py +357 -0
  53. pyclickerrpa-0.11.0/tests/test_engine_services.py +867 -0
  54. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_flow_branch.py +26 -18
  55. pyclickerrpa-0.11.0/tests/test_listener_lifecycle.py +384 -0
  56. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_matcher.py +88 -1
  57. pyclickerrpa-0.11.0/tests/test_migrations.py +139 -0
  58. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_models.py +4 -2
  59. pyclickerrpa-0.11.0/tests/test_p1_blackbox.py +226 -0
  60. pyclickerrpa-0.11.0/tests/test_phase4_integration.py +985 -0
  61. pyclickerrpa-0.11.0/tests/test_platform_recognition.py +1552 -0
  62. pyclickerrpa-0.11.0/tests/test_queue_entrypoints.py +289 -0
  63. pyclickerrpa-0.11.0/tests/test_recognition_dispatch.py +173 -0
  64. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_recorder_gui.py +181 -5
  65. pyclickerrpa-0.11.0/tests/test_resource_bundle.py +150 -0
  66. pyclickerrpa-0.11.0/tests/test_sandbox.py +738 -0
  67. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_structured_flow.py +120 -0
  68. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_testkit.py +34 -0
  69. pyclickerrpa-0.11.0/tests/test_testkit_contracts.py +393 -0
  70. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_web.py +22 -10
  71. pyclickerrpa-0.6.0/autoclicker/capture.py +0 -110
  72. pyclickerrpa-0.6.0/autoclicker/clicker.py +0 -212
  73. pyclickerrpa-0.6.0/autoclicker/expr.py +0 -94
  74. pyclickerrpa-0.6.0/autoclicker/gui/flow_editor.py +0 -582
  75. pyclickerrpa-0.6.0/autoclicker/gui/flow_node.py +0 -175
  76. pyclickerrpa-0.6.0/autoclicker/gui/hotkeys.py +0 -40
  77. pyclickerrpa-0.6.0/autoclicker/gui/qt_runner.py +0 -91
  78. pyclickerrpa-0.6.0/autoclicker/matcher.py +0 -262
  79. pyclickerrpa-0.6.0/autoclicker/models.py +0 -622
  80. pyclickerrpa-0.6.0/autoclicker/ocr.py +0 -107
  81. pyclickerrpa-0.6.0/autoclicker/run.py +0 -73
  82. pyclickerrpa-0.6.0/autoclicker/testkit/runner.py +0 -147
  83. pyclickerrpa-0.6.0/autoclicker/testkit/visual.py +0 -94
  84. pyclickerrpa-0.6.0/autoclicker/web/run_manager.py +0 -181
  85. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/LICENSE +0 -0
  86. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
  87. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
  88. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/requires.txt +0 -0
  89. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
  90. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/__init__.py +0 -0
  91. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/app.py +0 -0
  92. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/diagnose.py +0 -0
  93. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/dpi.py +0 -0
  94. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/__init__.py +0 -0
  95. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/lint_dialog.py +0 -0
  96. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/monitor.py +0 -0
  97. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/preview_label.py +0 -0
  98. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/scheduler_widget.py +0 -0
  99. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/gui/testkit_widget.py +0 -0
  100. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/lint.py +0 -0
  101. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/templatize.py +0 -0
  102. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/__init__.py +0 -0
  103. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/pytest_plugin.py +0 -0
  104. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/__init__.py +0 -0
  105. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/__main__.py +0 -0
  106. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/scheduler.py +0 -0
  107. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/web/serialize.py +0 -0
  108. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/autoclicker/windows.py +0 -0
  109. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/setup.cfg +0 -0
  110. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_diagnose.py +0 -0
  111. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_diagnose_gui.py +0 -0
  112. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_lint.py +0 -0
  113. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_runner.py +0 -0
  114. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_structured_gui.py +0 -0
  115. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_templatize.py +0 -0
  116. {pyclickerrpa-0.6.0 → pyclickerrpa-0.11.0}/tests/test_testkit_gui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.6.0
3
+ Version: 0.11.0
4
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
@@ -89,6 +89,39 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
89
89
  activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
90
90
  **stop** step to end a run — with retry / repeat / until policies.
91
91
 
92
+ ### Black-box automation runtime
93
+
94
+ - **Recognition-driven dispatch** (MAA-style): a step's `next` list is scanned
95
+ in order and the first candidate whose recognition gate hits is executed.
96
+ Set `next_timeout_ms` (>0, or -1 for unbounded) with `rate_limit_ms` to wait
97
+ and rescan instead of failing immediately — `next_timeout_ms: 0` (default)
98
+ keeps the classic single-pass behavior.
99
+ - **Resource bundles**: `Task.resource_paths` lists bundle root directories;
100
+ template names that aren't real paths resolve as `<root>/image/<name>` with
101
+ later roots overriding earlier ones (overlay). `Task.load_bundle(dir)`
102
+ assembles a task from `interface.json` (`resource` roots + `pipeline` entry)
103
+ or a plain `task.json`. Hot reload rides the signature-checked template cache.
104
+ - **Configurable capture/input channels**: controllers accept
105
+ `screencap_method` / `input_method`. Win32: `gdi_bitblt` (default),
106
+ `printwindow`, input `sendinput` (default) or `postmessage` (background,
107
+ needs a window handle; continuous gestures unsupported). ADB: `screencap`
108
+ (default) or `raw_gzip` (device-side gzip, steadier on slow links), input
109
+ `adb_input`. `minicap` / `minitouch` / `maatouch` / `desktop_dup` are
110
+ registered placeholders that fall back to the default with a
111
+ `strategy_fallback` warning — they require extra device components that are
112
+ not bundled (real-device behavior unverified).
113
+ - **Target resolution** (MAA-style): an action's `target` param can be a fixed
114
+ region `[x, y, w, h]` (clicks its center) or a **node name** — it then clicks
115
+ the center of that node's latest recognition box, enabling "recognize A, then
116
+ click next to A". `target: true` / absent keeps the step's own targeting.
117
+ - **Post-action freeze wait**: `post_wait_freezes: {time, target}` on any action
118
+ waits until the watched region stops changing before moving on — the standard
119
+ remedy for loading screens and animations. Unlike a standalone `wait_stable`
120
+ step it never fails the run; it waits up to `time` and proceeds.
121
+ - **Human-like jitter**: click coordinates are sampled from a truncated Gaussian
122
+ (sigma = `randomOffset`/2, clamped to the bound) instead of a uniform spread,
123
+ which looks more like a real hand and is friendlier to anti-automation checks.
124
+
92
125
  ## Authoring (desktop GUI)
93
126
 
94
127
  Drag-and-drop **node flow graph** and an interoperable **step list**, with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.6.0
3
+ Version: 0.11.0
4
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
@@ -89,6 +89,39 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
89
89
  activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
90
90
  **stop** step to end a run — with retry / repeat / until policies.
91
91
 
92
+ ### Black-box automation runtime
93
+
94
+ - **Recognition-driven dispatch** (MAA-style): a step's `next` list is scanned
95
+ in order and the first candidate whose recognition gate hits is executed.
96
+ Set `next_timeout_ms` (>0, or -1 for unbounded) with `rate_limit_ms` to wait
97
+ and rescan instead of failing immediately — `next_timeout_ms: 0` (default)
98
+ keeps the classic single-pass behavior.
99
+ - **Resource bundles**: `Task.resource_paths` lists bundle root directories;
100
+ template names that aren't real paths resolve as `<root>/image/<name>` with
101
+ later roots overriding earlier ones (overlay). `Task.load_bundle(dir)`
102
+ assembles a task from `interface.json` (`resource` roots + `pipeline` entry)
103
+ or a plain `task.json`. Hot reload rides the signature-checked template cache.
104
+ - **Configurable capture/input channels**: controllers accept
105
+ `screencap_method` / `input_method`. Win32: `gdi_bitblt` (default),
106
+ `printwindow`, input `sendinput` (default) or `postmessage` (background,
107
+ needs a window handle; continuous gestures unsupported). ADB: `screencap`
108
+ (default) or `raw_gzip` (device-side gzip, steadier on slow links), input
109
+ `adb_input`. `minicap` / `minitouch` / `maatouch` / `desktop_dup` are
110
+ registered placeholders that fall back to the default with a
111
+ `strategy_fallback` warning — they require extra device components that are
112
+ not bundled (real-device behavior unverified).
113
+ - **Target resolution** (MAA-style): an action's `target` param can be a fixed
114
+ region `[x, y, w, h]` (clicks its center) or a **node name** — it then clicks
115
+ the center of that node's latest recognition box, enabling "recognize A, then
116
+ click next to A". `target: true` / absent keeps the step's own targeting.
117
+ - **Post-action freeze wait**: `post_wait_freezes: {time, target}` on any action
118
+ waits until the watched region stops changing before moving on — the standard
119
+ remedy for loading screens and animations. Unlike a standalone `wait_stable`
120
+ step it never fails the run; it waits up to `time` and proceeds.
121
+ - **Human-like jitter**: click coordinates are sampled from a truncated Gaussian
122
+ (sigma = `randomOffset`/2, clamped to the bound) instead of a uniform spread,
123
+ which looks more like a real hand and is friendlier to anti-automation checks.
124
+
92
125
  ## Authoring (desktop GUI)
93
126
 
94
127
  Drag-and-drop **node flow graph** and an interoperable **step list**, with
@@ -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,18 +69,32 @@ 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
74
+ tests/test_controller_strategies.py
60
75
  tests/test_diagnose.py
61
76
  tests/test_diagnose_gui.py
77
+ tests/test_engine_integration.py
78
+ tests/test_engine_services.py
62
79
  tests/test_flow_branch.py
63
80
  tests/test_lint.py
81
+ tests/test_listener_lifecycle.py
64
82
  tests/test_matcher.py
83
+ tests/test_migrations.py
65
84
  tests/test_models.py
85
+ tests/test_p1_blackbox.py
86
+ tests/test_phase4_integration.py
87
+ tests/test_platform_recognition.py
88
+ tests/test_queue_entrypoints.py
89
+ tests/test_recognition_dispatch.py
66
90
  tests/test_recorder_gui.py
91
+ tests/test_resource_bundle.py
67
92
  tests/test_runner.py
93
+ tests/test_sandbox.py
68
94
  tests/test_structured_flow.py
69
95
  tests/test_structured_gui.py
70
96
  tests/test_templatize.py
71
97
  tests/test_testkit.py
98
+ tests/test_testkit_contracts.py
72
99
  tests/test_testkit_gui.py
73
100
  tests/test_web.py
@@ -51,6 +51,39 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
51
51
  activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
52
52
  **stop** step to end a run — with retry / repeat / until policies.
53
53
 
54
+ ### Black-box automation runtime
55
+
56
+ - **Recognition-driven dispatch** (MAA-style): a step's `next` list is scanned
57
+ in order and the first candidate whose recognition gate hits is executed.
58
+ Set `next_timeout_ms` (>0, or -1 for unbounded) with `rate_limit_ms` to wait
59
+ and rescan instead of failing immediately — `next_timeout_ms: 0` (default)
60
+ keeps the classic single-pass behavior.
61
+ - **Resource bundles**: `Task.resource_paths` lists bundle root directories;
62
+ template names that aren't real paths resolve as `<root>/image/<name>` with
63
+ later roots overriding earlier ones (overlay). `Task.load_bundle(dir)`
64
+ assembles a task from `interface.json` (`resource` roots + `pipeline` entry)
65
+ or a plain `task.json`. Hot reload rides the signature-checked template cache.
66
+ - **Configurable capture/input channels**: controllers accept
67
+ `screencap_method` / `input_method`. Win32: `gdi_bitblt` (default),
68
+ `printwindow`, input `sendinput` (default) or `postmessage` (background,
69
+ needs a window handle; continuous gestures unsupported). ADB: `screencap`
70
+ (default) or `raw_gzip` (device-side gzip, steadier on slow links), input
71
+ `adb_input`. `minicap` / `minitouch` / `maatouch` / `desktop_dup` are
72
+ registered placeholders that fall back to the default with a
73
+ `strategy_fallback` warning — they require extra device components that are
74
+ not bundled (real-device behavior unverified).
75
+ - **Target resolution** (MAA-style): an action's `target` param can be a fixed
76
+ region `[x, y, w, h]` (clicks its center) or a **node name** — it then clicks
77
+ the center of that node's latest recognition box, enabling "recognize A, then
78
+ click next to A". `target: true` / absent keeps the step's own targeting.
79
+ - **Post-action freeze wait**: `post_wait_freezes: {time, target}` on any action
80
+ waits until the watched region stops changing before moving on — the standard
81
+ remedy for loading screens and animations. Unlike a standalone `wait_stable`
82
+ step it never fails the run; it waits up to `time` and proceeds.
83
+ - **Human-like jitter**: click coordinates are sampled from a truncated Gaussian
84
+ (sigma = `randomOffset`/2, clamped to the bound) instead of a uniform spread,
85
+ which looks more like a real hand and is friendlier to anti-automation checks.
86
+
54
87
  ## Authoring (desktop GUI)
55
88
 
56
89
  Drag-and-drop **node flow graph** and an interoperable **step list**, with
@@ -0,0 +1,455 @@
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
+ # P1-4: an explicit target (fixed region or another node's recognition box)
104
+ # overrides the literal coordinates.
105
+ override = ctx.runner._resolve_target(step)
106
+ if override is not None:
107
+ cx, cy = override
108
+ cx = ctx.runner._rand_offset(cx, step.randomOffset)
109
+ cy = ctx.runner._rand_offset(cy, step.randomOffset)
110
+ ctx.log(f" 点击 ({cx}, {cy})")
111
+ ctx.clicker.click(ctx.runner.target_hwnd, cx, cy)
112
+ return StepResult.Success
113
+
114
+
115
+ def exec_wait(ctx: ExecutionContext, step: Step) -> StepResult:
116
+ from autoclicker.runner import StepResult
117
+ ctx.log(f" 等待 {step.waitMs}ms")
118
+ ctx.sleep_ms(step.waitMs)
119
+ return StepResult.Success
120
+
121
+
122
+ def exec_loop(ctx: ExecutionContext, step: Step) -> StepResult:
123
+ from autoclicker.runner import StepResult
124
+ return StepResult.Success
125
+
126
+
127
+ def exec_if_image_found(ctx: ExecutionContext, step: Step) -> StepResult:
128
+ return ctx.runner._exec_if_image(step)
129
+
130
+
131
+ def exec_if_text_found(ctx: ExecutionContext, step: Step) -> StepResult:
132
+ result, ctx.runner._last_condition = ctx.runner._evaluate_condition(step, StepType.IfTextFound)
133
+ return result
134
+
135
+
136
+ def exec_if_else(ctx: ExecutionContext, step: Step) -> StepResult:
137
+ from autoclicker.runner import StepResult
138
+ ctx.log(f" If/Else 分支 (上次条件: {'true' if ctx.runner._last_condition else 'false'})")
139
+ return StepResult.Success
140
+
141
+
142
+ def exec_call_sub(ctx: ExecutionContext, step: Step) -> StepResult:
143
+ from autoclicker.runner import StepResult
144
+ if not step.subTaskPath:
145
+ ctx.log(" 错误: 未指定子任务路径")
146
+ return StepResult.Fail
147
+ # T1-4: the outputs mapping belongs to THIS CallSub step; the runner must
148
+ # not scan the task for "the first CallSub" (wrong for multiple/nested subs).
149
+ return ctx.runner._run_subtask(step.subTaskPath, step.params.get("outputs"))
150
+
151
+
152
+ def exec_set_var(ctx: ExecutionContext, step: Step) -> StepResult:
153
+ from autoclicker.runner import StepResult
154
+ value = step.varValue
155
+ typed = getattr(ctx.runner.task, "variable_mode", "legacy") == "typed"
156
+ if typed and isinstance(value, (bool, int, float, list)):
157
+ pass # native value assigned as-is
158
+ elif value.startswith("++"):
159
+ value = int(ctx.vars.get(step.varName, 0)) + 1
160
+ if not typed:
161
+ value = str(value)
162
+ elif value.startswith("--"):
163
+ value = int(ctx.vars.get(step.varName, 0)) - 1
164
+ if not typed:
165
+ value = str(value)
166
+ elif value.startswith("="):
167
+ from autoclicker import expr
168
+ try:
169
+ value = expr.safe_eval(value[1:], ctx.vars, typed=typed)
170
+ except Exception as e: # noqa: BLE001 - recoverable step failure
171
+ ctx.log(f" 表达式错误: {e}")
172
+ return StepResult.Fail
173
+ else:
174
+ value = ctx.interp(value)
175
+ ctx.vars[step.varName] = value
176
+ ctx.runner.cb.var_changed(step.varName, value)
177
+ ctx.log(f" 变量 {step.varName} = {value}")
178
+ return StepResult.Success
179
+
180
+
181
+ def exec_random_wait(ctx: ExecutionContext, step: Step) -> StepResult:
182
+ from autoclicker.runner import StepResult
183
+ lo, hi = step.waitMs, step.waitMsMax
184
+ if hi <= lo:
185
+ hi = lo + 1
186
+ actual = random.randint(lo, hi - 1)
187
+ ctx.log(f" 随机等待 {actual}ms (范围 {lo}~{hi})")
188
+ ctx.sleep_ms(actual)
189
+ return StepResult.Success
190
+
191
+
192
+ def exec_key_press(ctx: ExecutionContext, step: Step) -> StepResult:
193
+ from autoclicker.runner import StepResult
194
+ from autoclicker.clicker import parse_vk
195
+ vk = parse_vk(step.keyName)
196
+ if vk == 0:
197
+ ctx.log(f' 错误: 无效按键 "{step.keyName}"')
198
+ return StepResult.Fail
199
+ ctx.log(f" 按键: {step.keyName}")
200
+ ctx.clicker.key_press(vk)
201
+ return StepResult.Success
202
+
203
+
204
+ def exec_type_text(ctx: ExecutionContext, step: Step) -> StepResult:
205
+ from autoclicker.runner import StepResult
206
+ text = ctx.interp(step.text)
207
+ ctx.log(f' 输入文字: "{text}"')
208
+ ctx.clicker.type_text(text)
209
+ return StepResult.Success
210
+
211
+
212
+ def exec_hotkey(ctx: ExecutionContext, step: Step) -> StepResult:
213
+ from autoclicker.runner import StepResult
214
+ from autoclicker.clicker import parse_vk
215
+ mod, vk = parse_vk(step.modifierKey), parse_vk(step.keyName)
216
+ if vk == 0:
217
+ ctx.log(" 错误: 无效按键")
218
+ return StepResult.Fail
219
+ ctx.log(f" 快捷键: {step.modifierKey}+{step.keyName}")
220
+ ctx.clicker.hotkey(mod, vk)
221
+ return StepResult.Success
222
+
223
+
224
+ def exec_long_press(ctx: ExecutionContext, step: Step) -> StepResult:
225
+ from autoclicker.runner import StepResult
226
+ ctx.log(f" 长按 ({step.x}, {step.y}) {step.holdMs}ms")
227
+ ctx.clicker.long_press(ctx.runner.target_hwnd, step.x, step.y, step.holdMs)
228
+ return StepResult.Success
229
+
230
+
231
+ def exec_drag(ctx: ExecutionContext, step: Step) -> StepResult:
232
+ from autoclicker.runner import StepResult
233
+ ctx.log(f" 拖拽 ({step.x},{step.y}) → ({step.x2},{step.y2}) {step.durationMs}ms")
234
+ ctx.clicker.drag(ctx.runner.target_hwnd, step.x, step.y, step.x2, step.y2, step.durationMs)
235
+ return StepResult.Success
236
+
237
+
238
+ def exec_pinch(ctx: ExecutionContext, step: Step) -> StepResult:
239
+ from autoclicker.runner import StepResult
240
+ ctx.log(f" 捏合 中心({step.x},{step.y}) {step.pinchStartDist}→{step.pinchEndDist}")
241
+ ctx.clicker.pinch(ctx.runner.target_hwnd, step.x, step.y,
242
+ step.pinchStartDist, step.pinchEndDist, step.durationMs)
243
+ return StepResult.Success
244
+
245
+
246
+ def exec_wait_image(ctx: ExecutionContext, step: Step) -> StepResult:
247
+ return ctx.runner._exec_image_poll(step, StepType.WaitImage)
248
+
249
+
250
+ def exec_wait_image_gone(ctx: ExecutionContext, step: Step) -> StepResult:
251
+ return ctx.runner._exec_image_poll(step, StepType.WaitImageGone)
252
+
253
+
254
+ def exec_scroll(ctx: ExecutionContext, step: Step) -> StepResult:
255
+ from autoclicker.runner import StepResult
256
+ ctx.log(f" 滚轮 ({step.x},{step.y}) {step.scrollAmount}格")
257
+ ctx.clicker.scroll(ctx.runner.target_hwnd, step.x, step.y, step.scrollAmount)
258
+ return StepResult.Success
259
+
260
+
261
+ def exec_if_color_match(ctx: ExecutionContext, step: Step) -> StepResult:
262
+ from autoclicker.runner import StepResult
263
+ screen = ctx.screenshot()
264
+ if screen.size == 0 or step.x >= screen.shape[1] or step.y >= screen.shape[0]:
265
+ ctx.runner._last_condition = False
266
+ return StepResult.Success
267
+ ctx.runner._last_condition = ctx.runner._color_at_matches(
268
+ screen, step.x, step.y, step.targetColor, step.colorTolerance)
269
+ ctx.log(f" 条件(颜色): ({step.x},{step.y}) vs {step.targetColor} → "
270
+ f"{'true' if ctx.runner._last_condition else 'false'}")
271
+ return StepResult.Success
272
+
273
+
274
+ def exec_do_until(ctx: ExecutionContext, step: Step) -> StepResult:
275
+ from autoclicker.runner import StepResult
276
+ return StepResult.Success
277
+
278
+
279
+ def exec_merge_image(ctx: ExecutionContext, step: Step) -> StepResult:
280
+ return ctx.runner._exec_image_poll(step, StepType.MergeImage)
281
+
282
+
283
+ def exec_multi_drag(ctx: ExecutionContext, step: Step) -> StepResult:
284
+ return ctx.runner._exec_multidrag(step)
285
+
286
+
287
+ def exec_goto(ctx: ExecutionContext, step: Step) -> StepResult:
288
+ from autoclicker.runner import StepResult
289
+ return StepResult.Success
290
+
291
+
292
+ def exec_if_image_count(ctx: ExecutionContext, step: Step) -> StepResult:
293
+ return ctx.runner._exec_if_image_count(step)
294
+
295
+
296
+ def exec_if_var(ctx: ExecutionContext, step: Step) -> StepResult:
297
+ from autoclicker.runner import StepResult
298
+ val = ctx.vars.get(step.varName, "0")
299
+ ctx.runner._last_condition = ctx.runner._compare(val, step.compareOp, ctx.interp(step.compareValue))
300
+ ctx.log(f" 条件(变量): {step.varName}={val} {step.compareOp} {step.compareValue} → "
301
+ f"{'true' if ctx.runner._last_condition else 'false'}")
302
+ return StepResult.Success
303
+
304
+
305
+ def exec_click_all(ctx: ExecutionContext, step: Step) -> StepResult:
306
+ return ctx.runner._exec_click_all(step)
307
+
308
+
309
+ def exec_group_begin(ctx: ExecutionContext, step: Step) -> StepResult:
310
+ from autoclicker.runner import StepResult
311
+ return StepResult.Success
312
+
313
+
314
+ def exec_group_end(ctx: ExecutionContext, step: Step) -> StepResult:
315
+ from autoclicker.runner import StepResult
316
+ return StepResult.Success
317
+
318
+
319
+ def exec_while_condition(ctx: ExecutionContext, step: Step) -> StepResult:
320
+ from autoclicker.runner import StepResult
321
+ return StepResult.Success
322
+
323
+
324
+ def exec_read_text(ctx: ExecutionContext, step: Step) -> StepResult:
325
+ return ctx.runner._exec_read_text(step)
326
+
327
+
328
+ def exec_click_relative(ctx: ExecutionContext, step: Step) -> StepResult:
329
+ from autoclicker.runner import StepResult
330
+ bx = int(ctx.vars.get("last_x", "0")) + step.x
331
+ by = int(ctx.vars.get("last_y", "0")) + step.y
332
+ cx = ctx.runner._rand_offset(bx, step.randomOffset)
333
+ cy = ctx.runner._rand_offset(by, step.randomOffset)
334
+ ctx.log(f" 相对点击 last+({step.x},{step.y}) = ({cx}, {cy})")
335
+ ctx.clicker.click(ctx.runner.target_hwnd, cx, cy)
336
+ return StepResult.Success
337
+
338
+
339
+ def exec_http_request(ctx: ExecutionContext, step: Step) -> StepResult:
340
+ return ctx.runner._exec_http(step)
341
+
342
+
343
+ def exec_run_script(ctx: ExecutionContext, step: Step) -> StepResult:
344
+ return ctx.runner._exec_script(step)
345
+
346
+
347
+ def exec_drag_image(ctx: ExecutionContext, step: Step) -> StepResult:
348
+ return ctx.runner._exec_drag_image(step)
349
+
350
+
351
+ def exec_wait_stable(ctx: ExecutionContext, step: Step) -> StepResult:
352
+ return ctx.runner._exec_wait_stable(step)
353
+
354
+
355
+ def exec_activate_window(ctx: ExecutionContext, step: Step) -> StepResult:
356
+ return ctx.runner._exec_activate_window(step)
357
+
358
+
359
+ def exec_stop(ctx: ExecutionContext, step: Step) -> StepResult:
360
+ from autoclicker.runner import StepResult
361
+ ctx.log(" 结束任务")
362
+ ctx.runner._running = False
363
+ return StepResult.Success
364
+
365
+
366
+ def exec_if_block(ctx: ExecutionContext, step: Step) -> StepResult:
367
+ return ctx.runner._exec_block(step)
368
+
369
+
370
+ def exec_repeat_block(ctx: ExecutionContext, step: Step) -> StepResult:
371
+ return ctx.runner._exec_block(step)
372
+
373
+
374
+ def exec_while_block(ctx: ExecutionContext, step: Step) -> StepResult:
375
+ return ctx.runner._exec_block(step)
376
+
377
+
378
+ def exec_for_each(ctx: ExecutionContext, step: Step) -> StepResult:
379
+ return ctx.runner._exec_block(step)
380
+
381
+
382
+ def exec_break(ctx: ExecutionContext, step: Step) -> StepResult:
383
+ from autoclicker.runner import StepResult
384
+ if ctx.runner._loop_depth == 0:
385
+ ctx.log(" 退出循环 / 下一轮只能放在循环体内")
386
+ return StepResult.Abort
387
+ return StepResult.Break
388
+
389
+
390
+ def exec_continue(ctx: ExecutionContext, step: Step) -> StepResult:
391
+ from autoclicker.runner import StepResult
392
+ if ctx.runner._loop_depth == 0:
393
+ ctx.log(" 退出循环 / 下一轮只能放在循环体内")
394
+ return StepResult.Abort
395
+ return StepResult.Continue
396
+
397
+
398
+ _BUILTIN_ACTIONS: dict[StepType, ActionHandler] = {
399
+ StepType.ClickImage: exec_click_image,
400
+ StepType.ClickText: exec_click_text,
401
+ StepType.ClickPosition: exec_click_position,
402
+ StepType.Wait: exec_wait,
403
+ StepType.Loop: exec_loop,
404
+ StepType.IfImageFound: exec_if_image_found,
405
+ StepType.IfTextFound: exec_if_text_found,
406
+ StepType.IfElse: exec_if_else,
407
+ StepType.CallSub: exec_call_sub,
408
+ StepType.SetVar: exec_set_var,
409
+ StepType.RandomWait: exec_random_wait,
410
+ StepType.KeyPress: exec_key_press,
411
+ StepType.TypeText: exec_type_text,
412
+ StepType.Hotkey: exec_hotkey,
413
+ StepType.LongPress: exec_long_press,
414
+ StepType.Drag: exec_drag,
415
+ StepType.Pinch: exec_pinch,
416
+ StepType.WaitImage: exec_wait_image,
417
+ StepType.WaitImageGone: exec_wait_image_gone,
418
+ StepType.Scroll: exec_scroll,
419
+ StepType.IfColorMatch: exec_if_color_match,
420
+ StepType.DoUntil: exec_do_until,
421
+ StepType.MergeImage: exec_merge_image,
422
+ StepType.MultiDrag: exec_multi_drag,
423
+ StepType.Goto: exec_goto,
424
+ StepType.IfImageCount: exec_if_image_count,
425
+ StepType.IfVar: exec_if_var,
426
+ StepType.ClickAll: exec_click_all,
427
+ StepType.GroupBegin: exec_group_begin,
428
+ StepType.GroupEnd: exec_group_end,
429
+ StepType.WhileCondition: exec_while_condition,
430
+ StepType.ReadText: exec_read_text,
431
+ StepType.ClickRelative: exec_click_relative,
432
+ StepType.HttpRequest: exec_http_request,
433
+ StepType.RunScript: exec_run_script,
434
+ StepType.DragImage: exec_drag_image,
435
+ StepType.WaitStable: exec_wait_stable,
436
+ StepType.ActivateWindow: exec_activate_window,
437
+ StepType.Stop: exec_stop,
438
+ StepType.IfBlock: exec_if_block,
439
+ StepType.RepeatBlock: exec_repeat_block,
440
+ StepType.WhileBlock: exec_while_block,
441
+ StepType.ForEach: exec_for_each,
442
+ StepType.Break: exec_break,
443
+ StepType.Continue: exec_continue,
444
+ }
445
+
446
+
447
+ def _install() -> None:
448
+ """Install built-ins once, atomically, without overwriting user handlers."""
449
+ global _installed
450
+ with _registry_lock:
451
+ if _installed:
452
+ return
453
+ for step_type, handler in _BUILTIN_ACTIONS.items():
454
+ _ACTION_REGISTRY.setdefault(step_type, handler)
455
+ _installed = True