PyClickerRPA 0.6.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.
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PKG-INFO +1 -1
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/PKG-INFO +1 -1
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/SOURCES.txt +23 -0
- pyclickerrpa-0.9.0/autoclicker/actions.py +450 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/adb.py +36 -11
- pyclickerrpa-0.9.0/autoclicker/capture.py +165 -0
- pyclickerrpa-0.9.0/autoclicker/clicker.py +280 -0
- pyclickerrpa-0.9.0/autoclicker/controllers.py +594 -0
- pyclickerrpa-0.9.0/autoclicker/expr.py +177 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/flow_connection.py +3 -1
- pyclickerrpa-0.9.0/autoclicker/gui/flow_editor.py +602 -0
- pyclickerrpa-0.9.0/autoclicker/gui/flow_node.py +149 -0
- pyclickerrpa-0.9.0/autoclicker/gui/hotkeys.py +125 -0
- pyclickerrpa-0.9.0/autoclicker/gui/log_panel.py +39 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/main_window.py +1183 -1013
- pyclickerrpa-0.6.0/autoclicker/gui/task_widget.py → pyclickerrpa-0.9.0/autoclicker/gui/property_panel.py +875 -911
- pyclickerrpa-0.9.0/autoclicker/gui/qt_runner.py +248 -0
- pyclickerrpa-0.9.0/autoclicker/gui/queue_panel.py +65 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/recorder.py +222 -33
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/snippet_library.py +13 -4
- pyclickerrpa-0.9.0/autoclicker/gui/step_list_widget.py +257 -0
- pyclickerrpa-0.9.0/autoclicker/gui/task_widget.py +9 -0
- pyclickerrpa-0.9.0/autoclicker/gui/var_panel.py +32 -0
- pyclickerrpa-0.9.0/autoclicker/matcher.py +430 -0
- pyclickerrpa-0.9.0/autoclicker/migrations.py +187 -0
- pyclickerrpa-0.9.0/autoclicker/models.py +1417 -0
- pyclickerrpa-0.9.0/autoclicker/ocr.py +300 -0
- pyclickerrpa-0.9.0/autoclicker/recognizers.py +523 -0
- pyclickerrpa-0.9.0/autoclicker/reports.py +150 -0
- pyclickerrpa-0.9.0/autoclicker/resources.py +86 -0
- pyclickerrpa-0.9.0/autoclicker/run.py +127 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/runner.py +510 -341
- pyclickerrpa-0.9.0/autoclicker/sandbox.py +443 -0
- pyclickerrpa-0.9.0/autoclicker/task_queue.py +351 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__main__.py +11 -4
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/assertions.py +99 -23
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/case.py +25 -2
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/loader.py +5 -2
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/report.py +70 -22
- pyclickerrpa-0.9.0/autoclicker/testkit/runner.py +309 -0
- pyclickerrpa-0.9.0/autoclicker/testkit/visual.py +135 -0
- pyclickerrpa-0.9.0/autoclicker/web/run_manager.py +380 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/server.py +88 -45
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/app.js +25 -5
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/index.html +2 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/style.css +1 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/pyproject.toml +1 -1
- pyclickerrpa-0.9.0/tests/test_actions.py +612 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_adb.py +23 -8
- pyclickerrpa-0.9.0/tests/test_engine_integration.py +357 -0
- pyclickerrpa-0.9.0/tests/test_engine_services.py +867 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_flow_branch.py +26 -18
- pyclickerrpa-0.9.0/tests/test_listener_lifecycle.py +384 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_matcher.py +88 -1
- pyclickerrpa-0.9.0/tests/test_migrations.py +128 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_models.py +4 -2
- pyclickerrpa-0.9.0/tests/test_phase4_integration.py +985 -0
- pyclickerrpa-0.9.0/tests/test_platform_recognition.py +1552 -0
- pyclickerrpa-0.9.0/tests/test_queue_entrypoints.py +289 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_recorder_gui.py +181 -5
- pyclickerrpa-0.9.0/tests/test_sandbox.py +738 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_structured_flow.py +120 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_testkit.py +34 -0
- pyclickerrpa-0.9.0/tests/test_testkit_contracts.py +393 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_web.py +22 -10
- pyclickerrpa-0.6.0/autoclicker/capture.py +0 -110
- pyclickerrpa-0.6.0/autoclicker/clicker.py +0 -212
- pyclickerrpa-0.6.0/autoclicker/expr.py +0 -94
- pyclickerrpa-0.6.0/autoclicker/gui/flow_editor.py +0 -582
- pyclickerrpa-0.6.0/autoclicker/gui/flow_node.py +0 -175
- pyclickerrpa-0.6.0/autoclicker/gui/hotkeys.py +0 -40
- pyclickerrpa-0.6.0/autoclicker/gui/qt_runner.py +0 -91
- pyclickerrpa-0.6.0/autoclicker/matcher.py +0 -262
- pyclickerrpa-0.6.0/autoclicker/models.py +0 -622
- pyclickerrpa-0.6.0/autoclicker/ocr.py +0 -107
- pyclickerrpa-0.6.0/autoclicker/run.py +0 -73
- pyclickerrpa-0.6.0/autoclicker/testkit/runner.py +0 -147
- pyclickerrpa-0.6.0/autoclicker/testkit/visual.py +0 -94
- pyclickerrpa-0.6.0/autoclicker/web/run_manager.py +0 -181
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/LICENSE +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/README.md +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/diagnose.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/lint_dialog.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/lint.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/templatize.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/windows.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/setup.cfg +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_diagnose.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_diagnose_gui.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_lint.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_runner.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_structured_gui.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_templatize.py +0 -0
- {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.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.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -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,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
|
|
68
90
|
tests/test_structured_flow.py
|
|
69
91
|
tests/test_structured_gui.py
|
|
70
92
|
tests/test_templatize.py
|
|
71
93
|
tests/test_testkit.py
|
|
94
|
+
tests/test_testkit_contracts.py
|
|
72
95
|
tests/test_testkit_gui.py
|
|
73
96
|
tests/test_web.py
|
|
@@ -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
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import subprocess
|
|
9
|
+
from math import isfinite
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
11
12
|
|
|
@@ -33,9 +34,12 @@ def _vk_to_android(vk: int) -> int | None:
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class AdbDevice:
|
|
36
|
-
def __init__(self, serial: str | None = None, adb: str = "adb"):
|
|
37
|
+
def __init__(self, serial: str | None = None, adb: str = "adb", *, timeout_s: float = 10):
|
|
38
|
+
if isinstance(timeout_s, bool) or not isfinite(timeout_s) or timeout_s <= 0:
|
|
39
|
+
raise ValueError("timeout_s must be finite and positive")
|
|
37
40
|
self.serial = serial
|
|
38
41
|
self.adb = adb
|
|
42
|
+
self.timeout_s = timeout_s
|
|
39
43
|
|
|
40
44
|
def _cmd(self, args: list[str]) -> list[str]:
|
|
41
45
|
base = [self.adb]
|
|
@@ -46,13 +50,29 @@ class AdbDevice:
|
|
|
46
50
|
def _run(self, args: list[str]) -> bytes:
|
|
47
51
|
try:
|
|
48
52
|
proc = subprocess.run(self._cmd(args), stdout=subprocess.PIPE,
|
|
49
|
-
stderr=subprocess.PIPE)
|
|
53
|
+
stderr=subprocess.PIPE, timeout=self.timeout_s)
|
|
54
|
+
except subprocess.TimeoutExpired as e:
|
|
55
|
+
raise RuntimeError(f"adb {' '.join(args)} 超时 ({self.timeout_s}s)") from e
|
|
50
56
|
except FileNotFoundError as e:
|
|
51
57
|
raise RuntimeError(f"找不到 adb 可执行文件 '{self.adb}',请安装 Android Platform Tools 并加入 PATH") from e
|
|
52
58
|
if proc.returncode != 0:
|
|
53
59
|
raise RuntimeError(f"adb {' '.join(args)} 失败: {proc.stderr.decode('utf-8', 'replace').strip()}")
|
|
54
60
|
return proc.stdout
|
|
55
61
|
|
|
62
|
+
def connect(self):
|
|
63
|
+
state = self._run(["get-state"]).decode("utf-8", "replace").strip()
|
|
64
|
+
if state != "device":
|
|
65
|
+
raise RuntimeError(f"ADB device is not connected: {state!r}")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def disconnect(self):
|
|
69
|
+
# Release this local CLI handle, never 'adb disconnect' a shared device.
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def reconnect(self):
|
|
73
|
+
self.disconnect()
|
|
74
|
+
return self.connect()
|
|
75
|
+
|
|
56
76
|
# ---- capture ----
|
|
57
77
|
def screencap(self) -> np.ndarray:
|
|
58
78
|
"""截图 -> BGR ndarray(exec-out 避免 stdout CRLF 转换的坑)。"""
|
|
@@ -82,10 +102,14 @@ class AdbDevice:
|
|
|
82
102
|
self._run(["shell", "input", "keyevent", str(int(code))])
|
|
83
103
|
|
|
84
104
|
@staticmethod
|
|
85
|
-
def list_devices(adb: str = "adb") -> list[str]:
|
|
105
|
+
def list_devices(adb: str = "adb", *, timeout_s: float = 10) -> list[str]:
|
|
106
|
+
if isinstance(timeout_s, bool) or not isfinite(timeout_s) or timeout_s <= 0:
|
|
107
|
+
raise ValueError("timeout_s must be finite and positive")
|
|
86
108
|
try:
|
|
87
109
|
out = subprocess.run([adb, "devices"], stdout=subprocess.PIPE,
|
|
88
|
-
stderr=subprocess.PIPE).stdout.decode("utf-8", "replace")
|
|
110
|
+
stderr=subprocess.PIPE, timeout=timeout_s).stdout.decode("utf-8", "replace")
|
|
111
|
+
except subprocess.TimeoutExpired as e:
|
|
112
|
+
raise RuntimeError(f"adb devices 超时 ({timeout_s}s)") from e
|
|
89
113
|
except FileNotFoundError:
|
|
90
114
|
return []
|
|
91
115
|
serials = []
|
|
@@ -121,7 +145,7 @@ class AdbClicker:
|
|
|
121
145
|
self.device.tap(x, y)
|
|
122
146
|
|
|
123
147
|
def right_click(self, hwnd, x: int, y: int) -> None:
|
|
124
|
-
|
|
148
|
+
raise NotImplementedError("ADB input does not support right click; use click for a tap")
|
|
125
149
|
|
|
126
150
|
def long_press(self, hwnd, x: int, y: int, hold_ms: int) -> None:
|
|
127
151
|
self.device.long_press(x, y, hold_ms)
|
|
@@ -141,20 +165,21 @@ class AdbClicker:
|
|
|
141
165
|
|
|
142
166
|
def key_press(self, vk: int) -> None:
|
|
143
167
|
code = _vk_to_android(vk)
|
|
144
|
-
if code is
|
|
145
|
-
|
|
168
|
+
if code is None:
|
|
169
|
+
raise NotImplementedError(f"ADB has no mapping for virtual key {vk!r}")
|
|
170
|
+
self.device.keyevent(code)
|
|
146
171
|
|
|
147
172
|
def key_down(self, vk: int) -> None:
|
|
148
|
-
|
|
173
|
+
raise NotImplementedError("ADB input does not support held keys")
|
|
149
174
|
|
|
150
175
|
def key_up(self, vk: int) -> None:
|
|
151
|
-
|
|
176
|
+
raise NotImplementedError("ADB input does not support held keys")
|
|
152
177
|
|
|
153
178
|
def hotkey(self, modifier: int, vk: int) -> None:
|
|
154
|
-
|
|
179
|
+
raise NotImplementedError("ADB input does not support hotkeys")
|
|
155
180
|
|
|
156
181
|
def pinch(self, hwnd, cx, cy, start_dist, end_dist, duration_ms: int = 400) -> None:
|
|
157
|
-
|
|
182
|
+
raise NotImplementedError("ADB input does not support multi-touch pinch")
|
|
158
183
|
|
|
159
184
|
|
|
160
185
|
def make_adb_backends(serial: str | None = None, adb: str = "adb"):
|