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.
Files changed (112) hide show
  1. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PKG-INFO +1 -1
  2. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/PKG-INFO +1 -1
  3. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/SOURCES.txt +23 -0
  4. pyclickerrpa-0.9.0/autoclicker/actions.py +450 -0
  5. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/adb.py +36 -11
  6. pyclickerrpa-0.9.0/autoclicker/capture.py +165 -0
  7. pyclickerrpa-0.9.0/autoclicker/clicker.py +280 -0
  8. pyclickerrpa-0.9.0/autoclicker/controllers.py +594 -0
  9. pyclickerrpa-0.9.0/autoclicker/expr.py +177 -0
  10. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/flow_connection.py +3 -1
  11. pyclickerrpa-0.9.0/autoclicker/gui/flow_editor.py +602 -0
  12. pyclickerrpa-0.9.0/autoclicker/gui/flow_node.py +149 -0
  13. pyclickerrpa-0.9.0/autoclicker/gui/hotkeys.py +125 -0
  14. pyclickerrpa-0.9.0/autoclicker/gui/log_panel.py +39 -0
  15. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/main_window.py +1183 -1013
  16. pyclickerrpa-0.6.0/autoclicker/gui/task_widget.py → pyclickerrpa-0.9.0/autoclicker/gui/property_panel.py +875 -911
  17. pyclickerrpa-0.9.0/autoclicker/gui/qt_runner.py +248 -0
  18. pyclickerrpa-0.9.0/autoclicker/gui/queue_panel.py +65 -0
  19. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/recorder.py +222 -33
  20. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/snippet_library.py +13 -4
  21. pyclickerrpa-0.9.0/autoclicker/gui/step_list_widget.py +257 -0
  22. pyclickerrpa-0.9.0/autoclicker/gui/task_widget.py +9 -0
  23. pyclickerrpa-0.9.0/autoclicker/gui/var_panel.py +32 -0
  24. pyclickerrpa-0.9.0/autoclicker/matcher.py +430 -0
  25. pyclickerrpa-0.9.0/autoclicker/migrations.py +187 -0
  26. pyclickerrpa-0.9.0/autoclicker/models.py +1417 -0
  27. pyclickerrpa-0.9.0/autoclicker/ocr.py +300 -0
  28. pyclickerrpa-0.9.0/autoclicker/recognizers.py +523 -0
  29. pyclickerrpa-0.9.0/autoclicker/reports.py +150 -0
  30. pyclickerrpa-0.9.0/autoclicker/resources.py +86 -0
  31. pyclickerrpa-0.9.0/autoclicker/run.py +127 -0
  32. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/runner.py +510 -341
  33. pyclickerrpa-0.9.0/autoclicker/sandbox.py +443 -0
  34. pyclickerrpa-0.9.0/autoclicker/task_queue.py +351 -0
  35. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__main__.py +11 -4
  36. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/assertions.py +99 -23
  37. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/case.py +25 -2
  38. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/loader.py +5 -2
  39. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/report.py +70 -22
  40. pyclickerrpa-0.9.0/autoclicker/testkit/runner.py +309 -0
  41. pyclickerrpa-0.9.0/autoclicker/testkit/visual.py +135 -0
  42. pyclickerrpa-0.9.0/autoclicker/web/run_manager.py +380 -0
  43. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/server.py +88 -45
  44. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/app.js +25 -5
  45. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/index.html +2 -0
  46. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/static/style.css +1 -0
  47. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/pyproject.toml +1 -1
  48. pyclickerrpa-0.9.0/tests/test_actions.py +612 -0
  49. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_adb.py +23 -8
  50. pyclickerrpa-0.9.0/tests/test_engine_integration.py +357 -0
  51. pyclickerrpa-0.9.0/tests/test_engine_services.py +867 -0
  52. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_flow_branch.py +26 -18
  53. pyclickerrpa-0.9.0/tests/test_listener_lifecycle.py +384 -0
  54. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_matcher.py +88 -1
  55. pyclickerrpa-0.9.0/tests/test_migrations.py +128 -0
  56. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_models.py +4 -2
  57. pyclickerrpa-0.9.0/tests/test_phase4_integration.py +985 -0
  58. pyclickerrpa-0.9.0/tests/test_platform_recognition.py +1552 -0
  59. pyclickerrpa-0.9.0/tests/test_queue_entrypoints.py +289 -0
  60. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_recorder_gui.py +181 -5
  61. pyclickerrpa-0.9.0/tests/test_sandbox.py +738 -0
  62. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_structured_flow.py +120 -0
  63. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_testkit.py +34 -0
  64. pyclickerrpa-0.9.0/tests/test_testkit_contracts.py +393 -0
  65. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_web.py +22 -10
  66. pyclickerrpa-0.6.0/autoclicker/capture.py +0 -110
  67. pyclickerrpa-0.6.0/autoclicker/clicker.py +0 -212
  68. pyclickerrpa-0.6.0/autoclicker/expr.py +0 -94
  69. pyclickerrpa-0.6.0/autoclicker/gui/flow_editor.py +0 -582
  70. pyclickerrpa-0.6.0/autoclicker/gui/flow_node.py +0 -175
  71. pyclickerrpa-0.6.0/autoclicker/gui/hotkeys.py +0 -40
  72. pyclickerrpa-0.6.0/autoclicker/gui/qt_runner.py +0 -91
  73. pyclickerrpa-0.6.0/autoclicker/matcher.py +0 -262
  74. pyclickerrpa-0.6.0/autoclicker/models.py +0 -622
  75. pyclickerrpa-0.6.0/autoclicker/ocr.py +0 -107
  76. pyclickerrpa-0.6.0/autoclicker/run.py +0 -73
  77. pyclickerrpa-0.6.0/autoclicker/testkit/runner.py +0 -147
  78. pyclickerrpa-0.6.0/autoclicker/testkit/visual.py +0 -94
  79. pyclickerrpa-0.6.0/autoclicker/web/run_manager.py +0 -181
  80. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/LICENSE +0 -0
  81. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
  82. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
  83. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/requires.txt +0 -0
  84. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
  85. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/README.md +0 -0
  86. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/__init__.py +0 -0
  87. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/app.py +0 -0
  88. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/diagnose.py +0 -0
  89. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/dpi.py +0 -0
  90. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/__init__.py +0 -0
  91. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/lint_dialog.py +0 -0
  92. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/monitor.py +0 -0
  93. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/preview_label.py +0 -0
  94. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/scheduler_widget.py +0 -0
  95. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/gui/testkit_widget.py +0 -0
  96. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/lint.py +0 -0
  97. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/templatize.py +0 -0
  98. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/__init__.py +0 -0
  99. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/testkit/pytest_plugin.py +0 -0
  100. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/__init__.py +0 -0
  101. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/__main__.py +0 -0
  102. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/scheduler.py +0 -0
  103. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/web/serialize.py +0 -0
  104. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/autoclicker/windows.py +0 -0
  105. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/setup.cfg +0 -0
  106. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_diagnose.py +0 -0
  107. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_diagnose_gui.py +0 -0
  108. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_lint.py +0 -0
  109. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_runner.py +0 -0
  110. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_structured_gui.py +0 -0
  111. {pyclickerrpa-0.6.0 → pyclickerrpa-0.9.0}/tests/test_templatize.py +0 -0
  112. {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.6.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.6.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
- self.device.tap(x, y) # Android 无右键,退化为点击
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 not None:
145
- self.device.keyevent(code)
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
- self.key_press(vk)
173
+ raise NotImplementedError("ADB input does not support held keys")
149
174
 
150
175
  def key_up(self, vk: int) -> None:
151
- pass
176
+ raise NotImplementedError("ADB input does not support held keys")
152
177
 
153
178
  def hotkey(self, modifier: int, vk: int) -> None:
154
- self.key_press(vk) # Android input 无组合键,尽力而为
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
- pass # adb input 不支持多点触控,跳过
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"):