PyClickerRPA 0.9.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.
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PKG-INFO +34 -1
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/PKG-INFO +34 -1
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/SOURCES.txt +4 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/README.md +33 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/actions.py +5 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/adb.py +36 -4
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/clicker.py +82 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/controllers.py +196 -10
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/migrations.py +11 -2
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/models.py +46 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/resources.py +40 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/runner.py +154 -12
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/pyproject.toml +1 -1
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_actions.py +1 -0
- pyclickerrpa-0.11.0/tests/test_controller_strategies.py +203 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_migrations.py +13 -2
- pyclickerrpa-0.11.0/tests/test_p1_blackbox.py +226 -0
- pyclickerrpa-0.11.0/tests/test_recognition_dispatch.py +173 -0
- pyclickerrpa-0.11.0/tests/test_resource_bundle.py +150 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/LICENSE +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/capture.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/diagnose.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/expr.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/flow_connection.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/flow_editor.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/flow_node.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/hotkeys.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/lint_dialog.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/log_panel.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/main_window.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/property_panel.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/qt_runner.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/queue_panel.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/recorder.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/snippet_library.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/step_list_widget.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/task_widget.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/gui/var_panel.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/lint.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/matcher.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/ocr.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/recognizers.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/reports.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/run.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/sandbox.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/task_queue.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/templatize.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/__main__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/assertions.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/case.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/loader.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/report.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/runner.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/testkit/visual.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/run_manager.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/server.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/app.js +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/index.html +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/web/static/style.css +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/autoclicker/windows.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/setup.cfg +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_adb.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_diagnose.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_diagnose_gui.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_engine_integration.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_engine_services.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_flow_branch.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_lint.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_listener_lifecycle.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_matcher.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_models.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_phase4_integration.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_platform_recognition.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_queue_entrypoints.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_recorder_gui.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_runner.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_sandbox.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_structured_flow.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_structured_gui.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_templatize.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_testkit.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_testkit_contracts.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_testkit_gui.py +0 -0
- {pyclickerrpa-0.9.0 → pyclickerrpa-0.11.0}/tests/test_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 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.
|
|
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
|
|
@@ -71,6 +71,7 @@ autoclicker/web/static/index.html
|
|
|
71
71
|
autoclicker/web/static/style.css
|
|
72
72
|
tests/test_actions.py
|
|
73
73
|
tests/test_adb.py
|
|
74
|
+
tests/test_controller_strategies.py
|
|
74
75
|
tests/test_diagnose.py
|
|
75
76
|
tests/test_diagnose_gui.py
|
|
76
77
|
tests/test_engine_integration.py
|
|
@@ -81,10 +82,13 @@ tests/test_listener_lifecycle.py
|
|
|
81
82
|
tests/test_matcher.py
|
|
82
83
|
tests/test_migrations.py
|
|
83
84
|
tests/test_models.py
|
|
85
|
+
tests/test_p1_blackbox.py
|
|
84
86
|
tests/test_phase4_integration.py
|
|
85
87
|
tests/test_platform_recognition.py
|
|
86
88
|
tests/test_queue_entrypoints.py
|
|
89
|
+
tests/test_recognition_dispatch.py
|
|
87
90
|
tests/test_recorder_gui.py
|
|
91
|
+
tests/test_resource_bundle.py
|
|
88
92
|
tests/test_runner.py
|
|
89
93
|
tests/test_sandbox.py
|
|
90
94
|
tests/test_structured_flow.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
|
|
@@ -100,6 +100,11 @@ def exec_click_position(ctx: ExecutionContext, step: Step) -> StepResult:
|
|
|
100
100
|
if step.varName == "use_var":
|
|
101
101
|
cx = int(ctx.vars.get("last_x", "0"))
|
|
102
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
|
|
103
108
|
cx = ctx.runner._rand_offset(cx, step.randomOffset)
|
|
104
109
|
cy = ctx.runner._rand_offset(cy, step.randomOffset)
|
|
105
110
|
ctx.log(f" 点击 ({cx}, {cy})")
|
|
@@ -83,6 +83,23 @@ class AdbDevice:
|
|
|
83
83
|
raise RuntimeError("screencap 解码失败(设备可能不支持 exec-out)")
|
|
84
84
|
return img
|
|
85
85
|
|
|
86
|
+
def screencap_gzip(self) -> np.ndarray:
|
|
87
|
+
"""RPA-25: 设备端 gzip 压缩后再传回的截图(慢链路设备更稳)。
|
|
88
|
+
|
|
89
|
+
需要设备 shell 提供 gzip(Android toybox 自带)。
|
|
90
|
+
"""
|
|
91
|
+
import cv2
|
|
92
|
+
import gzip
|
|
93
|
+
raw = self._run(["exec-out", "sh", "-c", "screencap -p | gzip -1"])
|
|
94
|
+
try:
|
|
95
|
+
png = gzip.decompress(raw)
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise RuntimeError(f"screencap(gzip) 解压失败: {exc}") from exc
|
|
98
|
+
img = cv2.imdecode(np.frombuffer(png, dtype=np.uint8), cv2.IMREAD_COLOR)
|
|
99
|
+
if img is None or img.size == 0:
|
|
100
|
+
raise RuntimeError("screencap(gzip) 解码失败")
|
|
101
|
+
return img
|
|
102
|
+
|
|
86
103
|
# ---- input ----
|
|
87
104
|
def tap(self, x: int, y: int) -> None:
|
|
88
105
|
self._run(["shell", "input", "tap", str(int(x)), str(int(y))])
|
|
@@ -121,15 +138,30 @@ class AdbDevice:
|
|
|
121
138
|
|
|
122
139
|
|
|
123
140
|
class AdbCapture:
|
|
124
|
-
"""capture 后端:忽略 hwnd,一律截设备屏。
|
|
125
|
-
|
|
141
|
+
"""capture 后端:忽略 hwnd,一律截设备屏。
|
|
142
|
+
|
|
143
|
+
RPA-25: ``screencap_method`` 选择截图通道 —— ``"screencap"``(默认,
|
|
144
|
+
exec-out 直传 PNG)或 ``"raw_gzip"``(设备端 gzip 压缩后传回,慢链路更稳)。
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
METHODS = ("screencap", "raw_gzip")
|
|
148
|
+
|
|
149
|
+
def __init__(self, device: AdbDevice, screencap_method: str = "screencap"):
|
|
150
|
+
if screencap_method not in self.METHODS:
|
|
151
|
+
raise ValueError(f"未知截图方法: {screencap_method},可选 {self.METHODS}")
|
|
126
152
|
self.device = device
|
|
153
|
+
self.screencap_method = screencap_method
|
|
127
154
|
|
|
128
|
-
def
|
|
155
|
+
def _screencap(self) -> np.ndarray:
|
|
156
|
+
if self.screencap_method == "raw_gzip":
|
|
157
|
+
return self.device.screencap_gzip()
|
|
129
158
|
return self.device.screencap()
|
|
130
159
|
|
|
160
|
+
def capture_screen(self) -> np.ndarray:
|
|
161
|
+
return self._screencap()
|
|
162
|
+
|
|
131
163
|
def capture_window(self, hwnd) -> np.ndarray:
|
|
132
|
-
return self.
|
|
164
|
+
return self._screencap()
|
|
133
165
|
|
|
134
166
|
|
|
135
167
|
class AdbClicker:
|
|
@@ -52,6 +52,9 @@ def _api():
|
|
|
52
52
|
_user32 = loader("user32", use_last_error=True)
|
|
53
53
|
_user32.SendInput.argtypes = (wintypes.UINT, ctypes.POINTER(_INPUT), ctypes.c_int)
|
|
54
54
|
_user32.SendInput.restype = wintypes.UINT
|
|
55
|
+
_user32.PostMessageW.argtypes = (wintypes.HWND, wintypes.UINT,
|
|
56
|
+
wintypes.WPARAM, wintypes.LPARAM)
|
|
57
|
+
_user32.PostMessageW.restype = wintypes.BOOL
|
|
55
58
|
return _user32
|
|
56
59
|
|
|
57
60
|
|
|
@@ -250,6 +253,85 @@ class Clicker:
|
|
|
250
253
|
self.key_press(vk)
|
|
251
254
|
|
|
252
255
|
|
|
256
|
+
# ---- RPA-25: PostMessage background input (window-message based) ----
|
|
257
|
+
WM_LBUTTONDOWN, WM_LBUTTONUP = 0x0201, 0x0202
|
|
258
|
+
WM_RBUTTONDOWN, WM_RBUTTONUP = 0x0204, 0x0205
|
|
259
|
+
WM_MOUSEWHEEL = 0x020A
|
|
260
|
+
WM_KEYDOWN, WM_KEYUP, WM_CHAR = 0x0100, 0x0101, 0x0102
|
|
261
|
+
MK_LBUTTON = 0x0001
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _lparam(x: int, y: int) -> int:
|
|
265
|
+
return (int(y) & 0xFFFF) << 16 | (int(x) & 0xFFFF)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class PostMessageClicker:
|
|
269
|
+
"""RPA-25: background input via PostMessage to a target HWND.
|
|
270
|
+
|
|
271
|
+
Unlike SendInput, this does not steal focus and works on unfocused windows,
|
|
272
|
+
but it requires a valid window handle and only supports discrete messages —
|
|
273
|
+
continuous gestures (swipe/drag/pinch) are not expressible and raise.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def _post(self, hwnd, msg, wparam, lparam) -> None:
|
|
277
|
+
if not hwnd:
|
|
278
|
+
raise NotImplementedError("postmessage 输入需要目标窗口句柄 (hwnd)")
|
|
279
|
+
api = _api()
|
|
280
|
+
if not api.PostMessageW(hwnd, msg, wparam, lparam):
|
|
281
|
+
raise OSError(f"PostMessage 失败: msg=0x{msg:04X} hwnd={hwnd}")
|
|
282
|
+
|
|
283
|
+
# ---- mouse ----
|
|
284
|
+
def click(self, hwnd, x: int, y: int) -> None:
|
|
285
|
+
lp = _lparam(x, y)
|
|
286
|
+
self._post(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lp)
|
|
287
|
+
self._post(hwnd, WM_LBUTTONUP, 0, lp)
|
|
288
|
+
|
|
289
|
+
def double_click(self, hwnd, x: int, y: int) -> None:
|
|
290
|
+
self.click(hwnd, x, y)
|
|
291
|
+
time.sleep(0.05)
|
|
292
|
+
self.click(hwnd, x, y)
|
|
293
|
+
|
|
294
|
+
def right_click(self, hwnd, x: int, y: int) -> None:
|
|
295
|
+
lp = _lparam(x, y)
|
|
296
|
+
self._post(hwnd, WM_RBUTTONDOWN, 0, lp)
|
|
297
|
+
self._post(hwnd, WM_RBUTTONUP, 0, lp)
|
|
298
|
+
|
|
299
|
+
def long_press(self, hwnd, x: int, y: int, hold_ms: int) -> None:
|
|
300
|
+
lp = _lparam(x, y)
|
|
301
|
+
self._post(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lp)
|
|
302
|
+
time.sleep(hold_ms / 1000.0)
|
|
303
|
+
self._post(hwnd, WM_LBUTTONUP, 0, lp)
|
|
304
|
+
|
|
305
|
+
def scroll(self, hwnd, x, y, amount: int) -> None:
|
|
306
|
+
lp = _lparam(x, y)
|
|
307
|
+
self._post(hwnd, WM_MOUSEWHEEL, (amount * WHEEL_DELTA & 0xFFFF) << 16, lp)
|
|
308
|
+
|
|
309
|
+
def swipe(self, hwnd, x1, y1, x2, y2, duration_ms: int = 500) -> None:
|
|
310
|
+
raise NotImplementedError("postmessage 不支持连续滑动,请改用 sendinput")
|
|
311
|
+
|
|
312
|
+
def drag(self, hwnd, x1, y1, x2, y2, duration_ms: int = 300) -> None:
|
|
313
|
+
raise NotImplementedError("postmessage 不支持拖拽,请改用 sendinput")
|
|
314
|
+
|
|
315
|
+
def pinch(self, hwnd, cx, cy, start_dist, end_dist, duration_ms: int = 400) -> None:
|
|
316
|
+
raise NotImplementedError("postmessage 不支持双指缩放,请改用 sendinput")
|
|
317
|
+
|
|
318
|
+
# ---- keyboard ----
|
|
319
|
+
def key_down(self, vk: int) -> None:
|
|
320
|
+
raise NotImplementedError("postmessage 键盘输入需要目标窗口,请用 key_press/type_text")
|
|
321
|
+
|
|
322
|
+
def key_up(self, vk: int) -> None:
|
|
323
|
+
raise NotImplementedError("postmessage 键盘输入需要目标窗口,请用 key_press/type_text")
|
|
324
|
+
|
|
325
|
+
def key_press(self, vk: int) -> None:
|
|
326
|
+
raise NotImplementedError("postmessage 按键请通过 type_text 或指定 hwnd 的窗口消息")
|
|
327
|
+
|
|
328
|
+
def type_text(self, text: str) -> None:
|
|
329
|
+
raise NotImplementedError("postmessage 文本输入需要目标窗口句柄,请改用 sendinput")
|
|
330
|
+
|
|
331
|
+
def hotkey(self, modifier: int, vk: int) -> None:
|
|
332
|
+
raise NotImplementedError("postmessage 不支持组合键,请改用 sendinput")
|
|
333
|
+
|
|
334
|
+
|
|
253
335
|
# ---- vk parsing (port of Clicker::parseVkCode) --------------------------------
|
|
254
336
|
_NAMED_VK = {
|
|
255
337
|
"ENTER": 0x0D, "RETURN": 0x0D, "ESC": 0x1B, "ESCAPE": 0x1B, "TAB": 0x09,
|
|
@@ -85,6 +85,74 @@ class ControllerStopped(RuntimeError):
|
|
|
85
85
|
"""A controller lease was cancelled before execution."""
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class StrategyUnavailable(RuntimeError):
|
|
89
|
+
"""RPA-25: a capture/input strategy needs components that are not present.
|
|
90
|
+
|
|
91
|
+
Placeholders (minicap/minitouch/maatouch/desktop_dup) raise this so an
|
|
92
|
+
explicit opt-in never masquerades as working; controllers fall back to
|
|
93
|
+
their default strategy and record a warning.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# RPA-25: configurable capture/input channels, registered like recognizers.
|
|
98
|
+
# Names are disjoint per platform by convention (see ADB_*/WIN32_* tuples).
|
|
99
|
+
ADB_SCREENCAP = ("screencap", "raw_gzip", "minicap")
|
|
100
|
+
ADB_INPUT = ("adb_input", "minitouch", "maatouch")
|
|
101
|
+
WIN32_SCREENCAP = ("gdi_bitblt", "printwindow", "desktop_dup")
|
|
102
|
+
WIN32_INPUT = ("sendinput", "postmessage")
|
|
103
|
+
|
|
104
|
+
_CAPTURE_STRATEGIES: dict[str, Callable] = {}
|
|
105
|
+
_INPUT_STRATEGIES: dict[str, Callable] = {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _register(registry: dict, name: str, *, replace: bool):
|
|
109
|
+
if not isinstance(name, str) or not name.strip():
|
|
110
|
+
raise ValueError("strategy name must be non-empty")
|
|
111
|
+
|
|
112
|
+
def decorator(factory):
|
|
113
|
+
if not callable(factory):
|
|
114
|
+
raise TypeError("strategy factory must be callable")
|
|
115
|
+
if name in registry and not replace:
|
|
116
|
+
raise ValueError(f"Strategy already registered: {name}")
|
|
117
|
+
registry[name] = factory
|
|
118
|
+
return factory
|
|
119
|
+
return decorator
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def register_capture_strategy(name: str, *, replace: bool = False):
|
|
123
|
+
"""Register ``factory(controller) -> capture backend``."""
|
|
124
|
+
return _register(_CAPTURE_STRATEGIES, name, replace=replace)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def register_input_strategy(name: str, *, replace: bool = False):
|
|
128
|
+
"""Register ``factory(controller) -> click/keyboard backend``."""
|
|
129
|
+
return _register(_INPUT_STRATEGIES, name, replace=replace)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_capture_strategy(name: str) -> Callable:
|
|
133
|
+
try:
|
|
134
|
+
return _CAPTURE_STRATEGIES[name]
|
|
135
|
+
except KeyError:
|
|
136
|
+
raise KeyError(f"Unknown capture strategy: {name!r}; "
|
|
137
|
+
f"available: {sorted(_CAPTURE_STRATEGIES)}") from None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_input_strategy(name: str) -> Callable:
|
|
141
|
+
try:
|
|
142
|
+
return _INPUT_STRATEGIES[name]
|
|
143
|
+
except KeyError:
|
|
144
|
+
raise KeyError(f"Unknown input strategy: {name!r}; "
|
|
145
|
+
f"available: {sorted(_INPUT_STRATEGIES)}") from None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def registered_capture_strategies() -> list[str]:
|
|
149
|
+
return sorted(_CAPTURE_STRATEGIES)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def registered_input_strategies() -> list[str]:
|
|
153
|
+
return sorted(_INPUT_STRATEGIES)
|
|
154
|
+
|
|
155
|
+
|
|
88
156
|
_lease_condition = Condition(RLock())
|
|
89
157
|
_lease_locks = WeakValueDictionary()
|
|
90
158
|
_lease_owners = {}
|
|
@@ -177,6 +245,30 @@ class _LegacyController:
|
|
|
177
245
|
self._watch_thread = None
|
|
178
246
|
self._watch_stop = None
|
|
179
247
|
self._active_backends = []
|
|
248
|
+
# RPA-25: fallback records when a requested strategy is unavailable.
|
|
249
|
+
self.strategy_warnings: list[dict] = []
|
|
250
|
+
|
|
251
|
+
def _resolve_strategy(self, kind: str, requested: str, default: str):
|
|
252
|
+
"""Build a backend through the strategy registry (RPA-25).
|
|
253
|
+
|
|
254
|
+
Unknown names are errors (never silent misses). A placeholder raising
|
|
255
|
+
StrategyUnavailable falls back to the platform default and records a
|
|
256
|
+
warning, so an explicit opt-in never masquerades as working.
|
|
257
|
+
"""
|
|
258
|
+
registry = _CAPTURE_STRATEGIES if kind == "screencap" else _INPUT_STRATEGIES
|
|
259
|
+
try:
|
|
260
|
+
factory = registry[requested]
|
|
261
|
+
except KeyError:
|
|
262
|
+
raise KeyError(f"Unknown {kind} strategy: {requested!r}; "
|
|
263
|
+
f"available: {sorted(registry)}") from None
|
|
264
|
+
try:
|
|
265
|
+
return factory(self)
|
|
266
|
+
except StrategyUnavailable as exc:
|
|
267
|
+
entry = {"kind": kind, "requested": requested,
|
|
268
|
+
"fallback": default, "reason": str(exc)}
|
|
269
|
+
self.strategy_warnings.append(entry)
|
|
270
|
+
self.notify_change(source="strategy_fallback", **entry)
|
|
271
|
+
return registry[default](self)
|
|
180
272
|
|
|
181
273
|
@property
|
|
182
274
|
def connected(self):
|
|
@@ -484,24 +576,28 @@ class Win32Controller(_LegacyController):
|
|
|
484
576
|
|
|
485
577
|
def __init__(self, *, capture=None, clicker=None,
|
|
486
578
|
origin: tuple[int, int] | Callable[[], tuple[int, int]] | None = None,
|
|
487
|
-
target_hwnd=None, pixel_watch=None, resource="desktop"
|
|
579
|
+
target_hwnd=None, pixel_watch=None, resource="desktop",
|
|
580
|
+
screencap_method: str = "gdi_bitblt", input_method: str = "sendinput"):
|
|
488
581
|
self._capture = capture
|
|
489
582
|
self._clicker = clicker
|
|
490
583
|
self._origin = origin
|
|
491
584
|
self._captured_origin = None
|
|
585
|
+
# RPA-25: injected backends win; methods apply when backends are built.
|
|
586
|
+
self._screencap_method = screencap_method
|
|
587
|
+
self._input_method = input_method
|
|
492
588
|
# Window targets on one desktop share the same execution resource.
|
|
493
589
|
self._init_lifecycle(("win32", resource), target_hwnd, pixel_watch)
|
|
494
590
|
|
|
495
591
|
def _capture_backend(self):
|
|
496
592
|
if self._capture is None:
|
|
497
|
-
|
|
498
|
-
|
|
593
|
+
self._capture = self._resolve_strategy(
|
|
594
|
+
"screencap", self._screencap_method, "gdi_bitblt")
|
|
499
595
|
return self._capture
|
|
500
596
|
|
|
501
597
|
def _click_backend(self):
|
|
502
598
|
if self._clicker is None:
|
|
503
|
-
|
|
504
|
-
|
|
599
|
+
self._clicker = self._resolve_strategy(
|
|
600
|
+
"input", self._input_method, "sendinput")
|
|
505
601
|
return self._clicker
|
|
506
602
|
|
|
507
603
|
def _desktop_origin(self):
|
|
@@ -553,7 +649,8 @@ class ADBController(_LegacyController):
|
|
|
553
649
|
key_hold=False, hotkey=False)
|
|
554
650
|
|
|
555
651
|
def __init__(self, device=None, *, serial=None, adb="adb", capture=None, clicker=None,
|
|
556
|
-
target_hwnd=None, pixel_watch=None
|
|
652
|
+
target_hwnd=None, pixel_watch=None,
|
|
653
|
+
screencap_method: str = "screencap", input_method: str = "adb_input"):
|
|
557
654
|
self.device = device
|
|
558
655
|
self.serial = serial if serial is not None else getattr(device, "serial", None)
|
|
559
656
|
if not isinstance(self.serial, (str, type(None))):
|
|
@@ -561,6 +658,9 @@ class ADBController(_LegacyController):
|
|
|
561
658
|
self.adb = adb
|
|
562
659
|
self._capture = capture
|
|
563
660
|
self._clicker = clicker
|
|
661
|
+
# RPA-25: injected backends win; methods apply when backends are built.
|
|
662
|
+
self._screencap_method = screencap_method
|
|
663
|
+
self._input_method = input_method
|
|
564
664
|
self._init_lifecycle(("adb", self.serial or "*"), target_hwnd, pixel_watch)
|
|
565
665
|
|
|
566
666
|
def _connection_backends(self):
|
|
@@ -576,14 +676,14 @@ class ADBController(_LegacyController):
|
|
|
576
676
|
|
|
577
677
|
def _capture_backend(self):
|
|
578
678
|
if self._capture is None:
|
|
579
|
-
|
|
580
|
-
|
|
679
|
+
self._capture = self._resolve_strategy(
|
|
680
|
+
"screencap", self._screencap_method, "screencap")
|
|
581
681
|
return self._capture
|
|
582
682
|
|
|
583
683
|
def _click_backend(self):
|
|
584
684
|
if self._clicker is None:
|
|
585
|
-
|
|
586
|
-
|
|
685
|
+
self._clicker = self._resolve_strategy(
|
|
686
|
+
"input", self._input_method, "adb_input")
|
|
587
687
|
return self._clicker
|
|
588
688
|
|
|
589
689
|
def capture_window(self, hwnd):
|
|
@@ -592,3 +692,89 @@ class ADBController(_LegacyController):
|
|
|
592
692
|
|
|
593
693
|
def pinch(self, hwnd, cx, cy, start_dist, end_dist, duration_ms=400):
|
|
594
694
|
raise NotImplementedError("ADB input does not support multi-touch pinch")
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
# ---- RPA-25: built-in capture/input strategy factories ----
|
|
698
|
+
|
|
699
|
+
def _require_kind(controller, cls, kind):
|
|
700
|
+
if not isinstance(controller, cls):
|
|
701
|
+
raise ValueError(f"{kind} 策略只能用于 {cls.__name__}")
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@register_capture_strategy("gdi_bitblt")
|
|
705
|
+
def _win32_gdi_capture(controller):
|
|
706
|
+
_require_kind(controller, Win32Controller, "gdi_bitblt")
|
|
707
|
+
from autoclicker import capture
|
|
708
|
+
return capture
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@register_capture_strategy("printwindow")
|
|
712
|
+
def _win32_printwindow_capture(controller):
|
|
713
|
+
"""Prefer PrintWindow for a bound window; full-screen keeps GDI BitBlt."""
|
|
714
|
+
_require_kind(controller, Win32Controller, "printwindow")
|
|
715
|
+
from autoclicker import capture
|
|
716
|
+
|
|
717
|
+
class _PrintWindowBackend:
|
|
718
|
+
def capture_screen(self):
|
|
719
|
+
hwnd = controller.target_hwnd
|
|
720
|
+
return capture.capture_window(hwnd) if hwnd else capture.capture_screen()
|
|
721
|
+
|
|
722
|
+
def capture_window(self, hwnd):
|
|
723
|
+
return capture.capture_window(hwnd)
|
|
724
|
+
|
|
725
|
+
return _PrintWindowBackend()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@register_capture_strategy("desktop_dup")
|
|
729
|
+
def _win32_desktop_dup_capture(controller):
|
|
730
|
+
raise StrategyUnavailable("desktop_dup 需要 DXGI Desktop Duplication 实现(未内置)")
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@register_input_strategy("sendinput")
|
|
734
|
+
def _win32_sendinput(controller):
|
|
735
|
+
_require_kind(controller, Win32Controller, "sendinput")
|
|
736
|
+
from autoclicker.clicker import Clicker
|
|
737
|
+
return Clicker()
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@register_input_strategy("postmessage")
|
|
741
|
+
def _win32_postmessage(controller):
|
|
742
|
+
_require_kind(controller, Win32Controller, "postmessage")
|
|
743
|
+
from autoclicker.clicker import PostMessageClicker
|
|
744
|
+
return PostMessageClicker()
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
@register_capture_strategy("screencap")
|
|
748
|
+
def _adb_screencap(controller):
|
|
749
|
+
_require_kind(controller, ADBController, "screencap")
|
|
750
|
+
from autoclicker.adb import AdbCapture
|
|
751
|
+
return AdbCapture(controller._device())
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@register_capture_strategy("raw_gzip")
|
|
755
|
+
def _adb_raw_gzip_capture(controller):
|
|
756
|
+
_require_kind(controller, ADBController, "raw_gzip")
|
|
757
|
+
from autoclicker.adb import AdbCapture
|
|
758
|
+
return AdbCapture(controller._device(), screencap_method="raw_gzip")
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@register_capture_strategy("minicap")
|
|
762
|
+
def _adb_minicap_capture(controller):
|
|
763
|
+
raise StrategyUnavailable("minicap 需要向设备推送 minicap 组件(未内置)")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@register_input_strategy("adb_input")
|
|
767
|
+
def _adb_input(controller):
|
|
768
|
+
_require_kind(controller, ADBController, "adb_input")
|
|
769
|
+
from autoclicker.adb import AdbClicker
|
|
770
|
+
return AdbClicker(controller._device())
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@register_input_strategy("minitouch")
|
|
774
|
+
def _adb_minitouch(controller):
|
|
775
|
+
raise StrategyUnavailable("minitouch 需要向设备推送 minitouch 组件(未内置)")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@register_input_strategy("maatouch")
|
|
779
|
+
def _adb_maatouch(controller):
|
|
780
|
+
raise StrategyUnavailable("maatouch 需要设备端 MaaTouch agent(未内置)")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Versioned, non-mutating migrations for legacy and canonical task JSON.
|
|
2
2
|
|
|
3
3
|
0: implicit legacy fields; 1: script opt-in; 2: symbolic edges;
|
|
4
|
-
3: explicit variable_mode/subtask_scope (legacy/shared for existing tasks)
|
|
4
|
+
3: explicit variable_mode/subtask_scope (legacy/shared for existing tasks);
|
|
5
|
+
4: resource_paths bundle roots (empty for existing tasks).
|
|
5
6
|
"""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
@@ -9,7 +10,7 @@ import copy
|
|
|
9
10
|
import re
|
|
10
11
|
from typing import Callable
|
|
11
12
|
|
|
12
|
-
LATEST_SCHEMA_VERSION =
|
|
13
|
+
LATEST_SCHEMA_VERSION = 4
|
|
13
14
|
_MIGRATIONS: dict[int, Callable[[dict], dict]] = {}
|
|
14
15
|
|
|
15
16
|
|
|
@@ -166,6 +167,14 @@ def migrate_2_to_3(data: dict) -> dict:
|
|
|
166
167
|
return data
|
|
167
168
|
|
|
168
169
|
|
|
170
|
+
@migration(3)
|
|
171
|
+
def migrate_3_to_4(data: dict) -> dict:
|
|
172
|
+
# RPA-24: bundle roots are opt-in; existing tasks resolve paths as before.
|
|
173
|
+
data.setdefault("resource_paths", [])
|
|
174
|
+
data["schema_version"] = 4
|
|
175
|
+
return data
|
|
176
|
+
|
|
177
|
+
|
|
169
178
|
def migrate_to_latest(data: dict) -> dict:
|
|
170
179
|
"""Deep-copy and migrate, rejecting malformed or unsupported versions."""
|
|
171
180
|
if not isinstance(data, dict):
|