PyClickerRPA 0.1.0__py3-none-any.whl
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.
- autoclicker/__init__.py +14 -0
- autoclicker/app.py +21 -0
- autoclicker/capture.py +94 -0
- autoclicker/clicker.py +212 -0
- autoclicker/dpi.py +30 -0
- autoclicker/gui/__init__.py +1 -0
- autoclicker/gui/flow_connection.py +91 -0
- autoclicker/gui/flow_editor.py +340 -0
- autoclicker/gui/flow_node.py +107 -0
- autoclicker/gui/hotkeys.py +40 -0
- autoclicker/gui/main_window.py +651 -0
- autoclicker/gui/monitor.py +84 -0
- autoclicker/gui/preview_label.py +176 -0
- autoclicker/gui/qt_runner.py +71 -0
- autoclicker/gui/recorder.py +163 -0
- autoclicker/gui/scheduler_widget.py +160 -0
- autoclicker/gui/snippet_library.py +117 -0
- autoclicker/gui/task_widget.py +547 -0
- autoclicker/matcher.py +152 -0
- autoclicker/models.py +468 -0
- autoclicker/ocr.py +63 -0
- autoclicker/run.py +64 -0
- autoclicker/runner.py +666 -0
- autoclicker/windows.py +53 -0
- pyclickerrpa-0.1.0.dist-info/METADATA +90 -0
- pyclickerrpa-0.1.0.dist-info/RECORD +29 -0
- pyclickerrpa-0.1.0.dist-info/WHEEL +5 -0
- pyclickerrpa-0.1.0.dist-info/entry_points.txt +5 -0
- pyclickerrpa-0.1.0.dist-info/top_level.txt +1 -0
autoclicker/runner.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
"""Task execution engine — faithful port of taskrunner.cpp.
|
|
2
|
+
|
|
3
|
+
Qt-free: runs on a ``threading.Thread`` with a callbacks object and injectable
|
|
4
|
+
capture / clicker / matcher backends (so control flow is unit-testable without a
|
|
5
|
+
screen). Semantics mirror the C++ TaskRunner exactly: variables, loop/goto/
|
|
6
|
+
while/do-until/if-skip control flow, retry/repeat/until wrappers, subtasks.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from autoclicker.models import Step, StepType, Task, UntilCondition, RetryPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StepResult(Enum):
|
|
20
|
+
Success = 0
|
|
21
|
+
Fail = 1
|
|
22
|
+
Abort = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RunnerCallbacks:
|
|
26
|
+
"""Override to receive engine events (all no-ops by default)."""
|
|
27
|
+
def step_started(self, index: int, description: str) -> None: ...
|
|
28
|
+
def step_completed(self, index: int, success: bool) -> None: ...
|
|
29
|
+
def match_found(self, x: int, y: int, confidence: float) -> None: ...
|
|
30
|
+
def log(self, msg: str) -> None: ...
|
|
31
|
+
def var_changed(self, name: str, value: str) -> None: ...
|
|
32
|
+
def finished(self) -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_ms() -> float:
|
|
36
|
+
return time.monotonic() * 1000.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TaskRunner(threading.Thread):
|
|
40
|
+
def __init__(self, task: Task, target_hwnd=None, callbacks: RunnerCallbacks | None = None,
|
|
41
|
+
capture=None, clicker=None, matcher=None):
|
|
42
|
+
super().__init__(daemon=True)
|
|
43
|
+
self.task = task
|
|
44
|
+
self.target_hwnd = target_hwnd
|
|
45
|
+
self.cb = callbacks or RunnerCallbacks()
|
|
46
|
+
|
|
47
|
+
# Injectable backends (default to the real ones; tests pass fakes)
|
|
48
|
+
if capture is None:
|
|
49
|
+
from autoclicker import capture as capture
|
|
50
|
+
if clicker is None:
|
|
51
|
+
from autoclicker.clicker import Clicker
|
|
52
|
+
clicker = Clicker()
|
|
53
|
+
if matcher is None:
|
|
54
|
+
from autoclicker import matcher as matcher
|
|
55
|
+
self.capture = capture
|
|
56
|
+
self.clicker = clicker
|
|
57
|
+
self.matcher = matcher
|
|
58
|
+
|
|
59
|
+
self._running = False
|
|
60
|
+
self._paused = False
|
|
61
|
+
self._loop_counters: dict[int, int] = {}
|
|
62
|
+
self._vars: dict[str, str] = {}
|
|
63
|
+
self._last_condition = False
|
|
64
|
+
|
|
65
|
+
# Optional: save a screenshot whenever a step fails (debugging aid)
|
|
66
|
+
self.screenshot_on_fail = False
|
|
67
|
+
self.screenshot_dir = ".failures"
|
|
68
|
+
|
|
69
|
+
# ---- control ----
|
|
70
|
+
def stop(self) -> None:
|
|
71
|
+
self._running = False
|
|
72
|
+
|
|
73
|
+
def pause(self) -> None:
|
|
74
|
+
self._paused = True
|
|
75
|
+
|
|
76
|
+
def resume(self) -> None:
|
|
77
|
+
self._paused = False
|
|
78
|
+
|
|
79
|
+
def get_var(self, name: str) -> str:
|
|
80
|
+
return self._vars.get(name, "")
|
|
81
|
+
|
|
82
|
+
# ---- helpers ----
|
|
83
|
+
def _sleep_ms(self, ms: int) -> None:
|
|
84
|
+
"""Sleep in 100ms chunks, bailing out early if stopped."""
|
|
85
|
+
elapsed = 0
|
|
86
|
+
while self._running and elapsed < ms:
|
|
87
|
+
time.sleep(0.1)
|
|
88
|
+
elapsed += 100
|
|
89
|
+
|
|
90
|
+
def _wait_if_paused(self) -> None:
|
|
91
|
+
while self._paused and self._running:
|
|
92
|
+
time.sleep(0.1)
|
|
93
|
+
|
|
94
|
+
def _template_list(self, step: Step) -> list[str]:
|
|
95
|
+
if step.imagePaths:
|
|
96
|
+
return list(step.imagePaths)
|
|
97
|
+
if step.imagePath:
|
|
98
|
+
return [step.imagePath]
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def _screenshot(self):
|
|
102
|
+
if self.target_hwnd:
|
|
103
|
+
try:
|
|
104
|
+
import win32gui
|
|
105
|
+
alive = win32gui.IsWindow(self.target_hwnd)
|
|
106
|
+
except Exception:
|
|
107
|
+
alive = True
|
|
108
|
+
if alive:
|
|
109
|
+
return self.capture.capture_window(self.target_hwnd)
|
|
110
|
+
return self.capture.capture_screen()
|
|
111
|
+
|
|
112
|
+
def _save_failure_shot(self, index: int) -> None:
|
|
113
|
+
try:
|
|
114
|
+
import cv2
|
|
115
|
+
screen = self._screenshot()
|
|
116
|
+
if screen is None or screen.size == 0:
|
|
117
|
+
return
|
|
118
|
+
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
119
|
+
path = os.path.join(self.screenshot_dir, f"fail_step{index + 1}_{int(time.time())}.png")
|
|
120
|
+
cv2.imwrite(path, screen)
|
|
121
|
+
self.cb.log(f" 已保存失败截图: {path}")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.cb.log(f" 保存失败截图出错: {e}")
|
|
124
|
+
|
|
125
|
+
def _load_template(self, path: str, desc: str):
|
|
126
|
+
import cv2
|
|
127
|
+
if not path:
|
|
128
|
+
self.cb.log(f" 错误: 未指定图片路径 (步骤: {desc})")
|
|
129
|
+
return None
|
|
130
|
+
if not os.path.exists(path):
|
|
131
|
+
self.cb.log(f" 错误: 图片文件不存在: {path}\n 请检查路径是否正确,建议使用绝对路径")
|
|
132
|
+
return None
|
|
133
|
+
templ = cv2.imread(path)
|
|
134
|
+
if templ is None or templ.size == 0:
|
|
135
|
+
self.cb.log(f" 错误: 无法解码图片: {path}\n 文件存在但可能已损坏或格式不支持 (支持 PNG/JPG/BMP)")
|
|
136
|
+
return None
|
|
137
|
+
return templ
|
|
138
|
+
|
|
139
|
+
def _rand_offset(self, v: int, off: int) -> int:
|
|
140
|
+
return v + random.randint(-off, off) if off > 0 else v
|
|
141
|
+
|
|
142
|
+
# ---- run loop ----
|
|
143
|
+
def run(self) -> None:
|
|
144
|
+
self._running = True
|
|
145
|
+
self._loop_counters.clear()
|
|
146
|
+
self._vars.clear()
|
|
147
|
+
self._last_condition = False
|
|
148
|
+
self.cb.log(f"开始执行任务: {self.task.name} ({len(self.task.steps)} 步)")
|
|
149
|
+
self._run_steps()
|
|
150
|
+
self.cb.log("任务完成" if self._running else "任务已停止")
|
|
151
|
+
self._running = False
|
|
152
|
+
self.cb.finished()
|
|
153
|
+
|
|
154
|
+
def _run_steps(self, log_prefix: str = "") -> StepResult:
|
|
155
|
+
steps = self.task.steps
|
|
156
|
+
i = 0
|
|
157
|
+
while self._running and i < len(steps):
|
|
158
|
+
self._wait_if_paused()
|
|
159
|
+
if not self._running:
|
|
160
|
+
break
|
|
161
|
+
step = steps[i]
|
|
162
|
+
|
|
163
|
+
if step.type == StepType.GroupBegin and step.disabled:
|
|
164
|
+
depth = 1
|
|
165
|
+
skip = i + 1
|
|
166
|
+
while skip < len(steps) and depth > 0:
|
|
167
|
+
if steps[skip].type == StepType.GroupBegin:
|
|
168
|
+
depth += 1
|
|
169
|
+
elif steps[skip].type == StepType.GroupEnd:
|
|
170
|
+
depth -= 1
|
|
171
|
+
skip += 1
|
|
172
|
+
self.cb.log(f" 跳过禁用分组: {step.groupName}")
|
|
173
|
+
i = skip
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
self.cb.step_started(i, log_prefix + step.display_name())
|
|
177
|
+
result = self._execute_with_repeat(i)
|
|
178
|
+
self.cb.step_completed(i, result == StepResult.Success)
|
|
179
|
+
|
|
180
|
+
if result != StepResult.Success and self.screenshot_on_fail:
|
|
181
|
+
self._save_failure_shot(i)
|
|
182
|
+
|
|
183
|
+
if result == StepResult.Abort:
|
|
184
|
+
self.cb.log(f"任务中止 (步骤 {i + 1} 失败)")
|
|
185
|
+
return StepResult.Abort
|
|
186
|
+
if not self._running:
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if step.type == StepType.Loop:
|
|
190
|
+
key = i
|
|
191
|
+
self._loop_counters[key] = self._loop_counters.get(key, 0) + 1
|
|
192
|
+
if step.loopCount < 0 or self._loop_counters[key] < step.loopCount:
|
|
193
|
+
cnt = self._loop_counters[key]
|
|
194
|
+
i = step.loopTarget
|
|
195
|
+
self.cb.log(f" 循环回到步骤 {step.loopTarget + 1} (第 {cnt} 次)")
|
|
196
|
+
continue
|
|
197
|
+
else:
|
|
198
|
+
self._loop_counters.pop(key, None)
|
|
199
|
+
|
|
200
|
+
if step.type == StepType.DoUntil:
|
|
201
|
+
if not self._check_until(step.until):
|
|
202
|
+
self._loop_counters[i] = self._loop_counters.get(i, 0) + 1
|
|
203
|
+
cnt = self._loop_counters[i]
|
|
204
|
+
i = step.loopTarget
|
|
205
|
+
self.cb.log(f" 条件未满足,循环回到步骤 {step.loopTarget + 1} (第 {cnt} 次)")
|
|
206
|
+
continue
|
|
207
|
+
else:
|
|
208
|
+
self.cb.log(" 条件已满足,结束循环")
|
|
209
|
+
self._loop_counters.pop(i, None)
|
|
210
|
+
|
|
211
|
+
if step.type == StepType.WhileCondition:
|
|
212
|
+
met = False
|
|
213
|
+
if step.until.type != UntilCondition.NoneCond:
|
|
214
|
+
met = self._check_until(step.until)
|
|
215
|
+
elif step.varName:
|
|
216
|
+
met = self._compare(self._vars.get(step.varName, "0"), step.compareOp, step.compareValue)
|
|
217
|
+
if not met:
|
|
218
|
+
self.cb.log(f" While 条件为假,跳到步骤 {step.loopTarget + 1}")
|
|
219
|
+
i = step.loopTarget
|
|
220
|
+
continue
|
|
221
|
+
self.cb.log(" While 条件为真,继续执行")
|
|
222
|
+
|
|
223
|
+
if step.type == StepType.Goto:
|
|
224
|
+
self.cb.log(f" 跳转到步骤 {step.gotoTarget + 1}")
|
|
225
|
+
i = step.gotoTarget
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if step.type in (StepType.IfImageFound, StepType.IfTextFound, StepType.IfElse,
|
|
229
|
+
StepType.IfColorMatch, StepType.IfImageCount, StepType.IfVar):
|
|
230
|
+
if not self._last_condition:
|
|
231
|
+
self.cb.log(f" 条件为假,跳过 {step.elseSkip} 步")
|
|
232
|
+
i += step.elseSkip
|
|
233
|
+
|
|
234
|
+
i += 1
|
|
235
|
+
return StepResult.Success
|
|
236
|
+
|
|
237
|
+
# ---- wrappers ----
|
|
238
|
+
def _execute_with_repeat(self, index: int) -> StepResult:
|
|
239
|
+
step = self.task.steps[index]
|
|
240
|
+
has_repeat = step.repeat.times != 1
|
|
241
|
+
has_until = step.until.type != UntilCondition.NoneCond
|
|
242
|
+
if not has_repeat and not has_until:
|
|
243
|
+
return self._execute_with_retry(index)
|
|
244
|
+
|
|
245
|
+
start = _now_ms()
|
|
246
|
+
iteration = 0
|
|
247
|
+
last = StepResult.Success
|
|
248
|
+
while self._running:
|
|
249
|
+
last = self._execute_with_retry(index)
|
|
250
|
+
iteration += 1
|
|
251
|
+
if last == StepResult.Abort:
|
|
252
|
+
return StepResult.Abort
|
|
253
|
+
if has_until:
|
|
254
|
+
if self._check_until(step.until):
|
|
255
|
+
self.cb.log(f" 终止条件已满足 (第 {iteration} 次)")
|
|
256
|
+
return last
|
|
257
|
+
if _now_ms() - start >= step.until.timeoutMs:
|
|
258
|
+
self.cb.log(f" 终止条件超时 ({step.until.timeoutMs}ms)")
|
|
259
|
+
return last
|
|
260
|
+
if has_repeat and step.repeat.times > 0 and iteration >= step.repeat.times:
|
|
261
|
+
self.cb.log(f" 重复执行完成 ({iteration} 次)")
|
|
262
|
+
return last
|
|
263
|
+
self._wait_if_paused()
|
|
264
|
+
if not self._running:
|
|
265
|
+
break
|
|
266
|
+
self._sleep_ms(step.repeat.delayMs)
|
|
267
|
+
return last
|
|
268
|
+
|
|
269
|
+
def _execute_with_retry(self, index: int) -> StepResult:
|
|
270
|
+
step = self.task.steps[index]
|
|
271
|
+
policy = step.retry.policy
|
|
272
|
+
if policy in (RetryPolicy.NoneP, RetryPolicy.Skip):
|
|
273
|
+
r = self._execute_step(index)
|
|
274
|
+
if r == StepResult.Fail and policy == RetryPolicy.Skip:
|
|
275
|
+
self.cb.log(" 步骤失败,跳过")
|
|
276
|
+
return StepResult.Success
|
|
277
|
+
return r
|
|
278
|
+
if policy == RetryPolicy.Abort:
|
|
279
|
+
r = self._execute_step(index)
|
|
280
|
+
return StepResult.Abort if r == StepResult.Fail else r
|
|
281
|
+
# Retry
|
|
282
|
+
for attempt in range(step.retry.count + 1):
|
|
283
|
+
if not self._running:
|
|
284
|
+
return StepResult.Fail
|
|
285
|
+
if attempt > 0:
|
|
286
|
+
self.cb.log(f" 重试第 {attempt} 次...")
|
|
287
|
+
self._sleep_ms(step.retry.delayMs)
|
|
288
|
+
if self._execute_step(index) == StepResult.Success:
|
|
289
|
+
return StepResult.Success
|
|
290
|
+
self.cb.log(f" 重试 {step.retry.count} 次后仍失败")
|
|
291
|
+
return StepResult.Fail
|
|
292
|
+
|
|
293
|
+
def _compare(self, val: str, op: str, cmp: str) -> bool:
|
|
294
|
+
try:
|
|
295
|
+
a, b = float(val), float(cmp)
|
|
296
|
+
except ValueError:
|
|
297
|
+
a = b = None
|
|
298
|
+
if a is not None:
|
|
299
|
+
return {"==": a == b, "!=": a != b, ">": a > b,
|
|
300
|
+
"<": a < b, ">=": a >= b, "<=": a <= b}.get(op, False)
|
|
301
|
+
return {"==": val == cmp, "!=": val != cmp}.get(op, False)
|
|
302
|
+
|
|
303
|
+
def _check_until(self, cfg) -> bool:
|
|
304
|
+
import cv2
|
|
305
|
+
if cfg.type == UntilCondition.ImageFound:
|
|
306
|
+
templ = cv2.imread(cfg.image) if cfg.image else None
|
|
307
|
+
if templ is None:
|
|
308
|
+
return False
|
|
309
|
+
return bool(self.matcher.find_image(self._screenshot(), templ, cfg.threshold))
|
|
310
|
+
if cfg.type == UntilCondition.ImageGone:
|
|
311
|
+
templ = cv2.imread(cfg.image) if cfg.image else None
|
|
312
|
+
if templ is None:
|
|
313
|
+
return True
|
|
314
|
+
return not self.matcher.find_image(self._screenshot(), templ, cfg.threshold)
|
|
315
|
+
if cfg.type == UntilCondition.TextFound:
|
|
316
|
+
return bool(self.matcher.find_text(self._screenshot(), cfg.text))
|
|
317
|
+
if cfg.type == UntilCondition.ColorMatch:
|
|
318
|
+
screen = self._screenshot()
|
|
319
|
+
if screen.size == 0 or cfg.x >= screen.shape[1] or cfg.y >= screen.shape[0]:
|
|
320
|
+
return False
|
|
321
|
+
return self._color_at_matches(screen, cfg.x, cfg.y, cfg.color, cfg.colorTolerance)
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _parse_color(name: str):
|
|
326
|
+
"""Parse #RRGGBB (or #RGB) into (r, g, b); None if invalid."""
|
|
327
|
+
if not name:
|
|
328
|
+
return None
|
|
329
|
+
s = name.strip().lstrip("#")
|
|
330
|
+
if len(s) == 3:
|
|
331
|
+
s = "".join(c * 2 for c in s)
|
|
332
|
+
if len(s) != 6:
|
|
333
|
+
return None
|
|
334
|
+
try:
|
|
335
|
+
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
|
|
336
|
+
except ValueError:
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def _color_at_matches(self, screen, x: int, y: int, color: str, tol: int) -> bool:
|
|
340
|
+
rgb = self._parse_color(color)
|
|
341
|
+
if rgb is None:
|
|
342
|
+
return False
|
|
343
|
+
b, g, r = (int(v) for v in screen[y, x][:3]) # OpenCV is BGR
|
|
344
|
+
return abs(r - rgb[0]) <= tol and abs(g - rgb[1]) <= tol and abs(b - rgb[2]) <= tol
|
|
345
|
+
|
|
346
|
+
def _run_subtask(self, path: str) -> StepResult:
|
|
347
|
+
sub = Task.load_from_file(path) if os.path.exists(path) else Task()
|
|
348
|
+
if not sub.steps:
|
|
349
|
+
self.cb.log(f" 子任务加载失败: {path}")
|
|
350
|
+
return StepResult.Fail
|
|
351
|
+
self.cb.log(f" >>> 进入子任务: {sub.name}")
|
|
352
|
+
saved_task, saved_counters = self.task, self._loop_counters
|
|
353
|
+
self.task, self._loop_counters = sub, {}
|
|
354
|
+
result = self._run_steps("[子] ")
|
|
355
|
+
self.task, self._loop_counters = saved_task, saved_counters
|
|
356
|
+
self.cb.log(" <<< 子任务结束")
|
|
357
|
+
return result
|
|
358
|
+
|
|
359
|
+
# ---- per-step execution (port of executeStep) ----
|
|
360
|
+
def _execute_step(self, index: int) -> StepResult: # noqa: C901 - faithful 1:1 port
|
|
361
|
+
step = self.task.steps[index]
|
|
362
|
+
t = step.type
|
|
363
|
+
|
|
364
|
+
if t in (StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone, StepType.MergeImage):
|
|
365
|
+
return self._exec_image_poll(step, t)
|
|
366
|
+
if t == StepType.ClickText:
|
|
367
|
+
return self._exec_click_text(step)
|
|
368
|
+
if t == StepType.ClickPosition:
|
|
369
|
+
cx, cy = step.x, step.y
|
|
370
|
+
if step.varName == "use_var":
|
|
371
|
+
cx = int(self._vars.get("last_x", "0"))
|
|
372
|
+
cy = int(self._vars.get("last_y", "0"))
|
|
373
|
+
cx, cy = self._rand_offset(cx, step.randomOffset), self._rand_offset(cy, step.randomOffset)
|
|
374
|
+
self.cb.log(f" 点击 ({cx}, {cy})")
|
|
375
|
+
self.clicker.click(self.target_hwnd, cx, cy)
|
|
376
|
+
return StepResult.Success
|
|
377
|
+
if t == StepType.Wait:
|
|
378
|
+
self.cb.log(f" 等待 {step.waitMs}ms")
|
|
379
|
+
self._sleep_ms(step.waitMs)
|
|
380
|
+
return StepResult.Success
|
|
381
|
+
if t == StepType.RandomWait:
|
|
382
|
+
lo, hi = step.waitMs, step.waitMsMax
|
|
383
|
+
if hi <= lo:
|
|
384
|
+
hi = lo + 1
|
|
385
|
+
actual = random.randint(lo, hi - 1)
|
|
386
|
+
self.cb.log(f" 随机等待 {actual}ms (范围 {lo}~{hi})")
|
|
387
|
+
self._sleep_ms(actual)
|
|
388
|
+
return StepResult.Success
|
|
389
|
+
if t == StepType.ClickRelative:
|
|
390
|
+
bx = int(self._vars.get("last_x", "0")) + step.x
|
|
391
|
+
by = int(self._vars.get("last_y", "0")) + step.y
|
|
392
|
+
cx, cy = self._rand_offset(bx, step.randomOffset), self._rand_offset(by, step.randomOffset)
|
|
393
|
+
self.cb.log(f" 相对点击 last+({step.x},{step.y}) = ({cx}, {cy})")
|
|
394
|
+
self.clicker.click(self.target_hwnd, cx, cy)
|
|
395
|
+
return StepResult.Success
|
|
396
|
+
if t == StepType.ReadText:
|
|
397
|
+
return self._exec_read_text(step)
|
|
398
|
+
if t in (StepType.Loop, StepType.DoUntil, StepType.Goto,
|
|
399
|
+
StepType.GroupBegin, StepType.GroupEnd, StepType.WhileCondition):
|
|
400
|
+
return StepResult.Success
|
|
401
|
+
if t == StepType.IfImageFound:
|
|
402
|
+
return self._exec_if_image(step)
|
|
403
|
+
if t == StepType.IfTextFound:
|
|
404
|
+
self._last_condition = bool(self.matcher.find_text(self._screenshot(), step.text))
|
|
405
|
+
self.cb.log(f" 条件(文字): {'true' if self._last_condition else 'false'}")
|
|
406
|
+
return StepResult.Success
|
|
407
|
+
if t == StepType.IfElse:
|
|
408
|
+
self.cb.log(f" If/Else 分支 (上次条件: {'true' if self._last_condition else 'false'})")
|
|
409
|
+
return StepResult.Success
|
|
410
|
+
if t == StepType.CallSub:
|
|
411
|
+
if not step.subTaskPath:
|
|
412
|
+
self.cb.log(" 错误: 未指定子任务路径")
|
|
413
|
+
return StepResult.Fail
|
|
414
|
+
return self._run_subtask(step.subTaskPath)
|
|
415
|
+
if t == StepType.SetVar:
|
|
416
|
+
value = step.varValue
|
|
417
|
+
if value.startswith("++"):
|
|
418
|
+
value = str(int(self._vars.get(step.varName, "0")) + 1)
|
|
419
|
+
elif value.startswith("--"):
|
|
420
|
+
value = str(int(self._vars.get(step.varName, "0")) - 1)
|
|
421
|
+
self._vars[step.varName] = value
|
|
422
|
+
self.cb.var_changed(step.varName, value)
|
|
423
|
+
self.cb.log(f" 变量 {step.varName} = {value}")
|
|
424
|
+
return StepResult.Success
|
|
425
|
+
if t == StepType.KeyPress:
|
|
426
|
+
from autoclicker.clicker import parse_vk
|
|
427
|
+
vk = parse_vk(step.keyName)
|
|
428
|
+
if vk == 0:
|
|
429
|
+
self.cb.log(f' 错误: 无效按键 "{step.keyName}"')
|
|
430
|
+
return StepResult.Fail
|
|
431
|
+
self.cb.log(f" 按键: {step.keyName}")
|
|
432
|
+
self.clicker.key_press(vk)
|
|
433
|
+
return StepResult.Success
|
|
434
|
+
if t == StepType.TypeText:
|
|
435
|
+
self.cb.log(f' 输入文字: "{step.text}"')
|
|
436
|
+
self.clicker.type_text(step.text)
|
|
437
|
+
return StepResult.Success
|
|
438
|
+
if t == StepType.Hotkey:
|
|
439
|
+
from autoclicker.clicker import parse_vk
|
|
440
|
+
mod, vk = parse_vk(step.modifierKey), parse_vk(step.keyName)
|
|
441
|
+
if vk == 0:
|
|
442
|
+
self.cb.log(" 错误: 无效按键")
|
|
443
|
+
return StepResult.Fail
|
|
444
|
+
self.cb.log(f" 快捷键: {step.modifierKey}+{step.keyName}")
|
|
445
|
+
self.clicker.hotkey(mod, vk)
|
|
446
|
+
return StepResult.Success
|
|
447
|
+
if t == StepType.LongPress:
|
|
448
|
+
self.cb.log(f" 长按 ({step.x}, {step.y}) {step.holdMs}ms")
|
|
449
|
+
self.clicker.long_press(self.target_hwnd, step.x, step.y, step.holdMs)
|
|
450
|
+
return StepResult.Success
|
|
451
|
+
if t == StepType.Drag:
|
|
452
|
+
self.cb.log(f" 拖拽 ({step.x},{step.y}) → ({step.x2},{step.y2}) {step.durationMs}ms")
|
|
453
|
+
self.clicker.drag(self.target_hwnd, step.x, step.y, step.x2, step.y2, step.durationMs)
|
|
454
|
+
return StepResult.Success
|
|
455
|
+
if t == StepType.Pinch:
|
|
456
|
+
self.cb.log(f" 捏合 中心({step.x},{step.y}) {step.pinchStartDist}→{step.pinchEndDist}")
|
|
457
|
+
self.clicker.pinch(self.target_hwnd, step.x, step.y, step.pinchStartDist, step.pinchEndDist, step.durationMs)
|
|
458
|
+
return StepResult.Success
|
|
459
|
+
if t == StepType.Scroll:
|
|
460
|
+
self.cb.log(f" 滚轮 ({step.x},{step.y}) {step.scrollAmount}格")
|
|
461
|
+
self.clicker.scroll(self.target_hwnd, step.x, step.y, step.scrollAmount)
|
|
462
|
+
return StepResult.Success
|
|
463
|
+
if t == StepType.IfColorMatch:
|
|
464
|
+
screen = self._screenshot()
|
|
465
|
+
if screen.size == 0 or step.x >= screen.shape[1] or step.y >= screen.shape[0]:
|
|
466
|
+
self._last_condition = False
|
|
467
|
+
return StepResult.Success
|
|
468
|
+
self._last_condition = self._color_at_matches(screen, step.x, step.y, step.targetColor, step.colorTolerance)
|
|
469
|
+
self.cb.log(f" 条件(颜色): ({step.x},{step.y}) vs {step.targetColor} → "
|
|
470
|
+
f"{'true' if self._last_condition else 'false'}")
|
|
471
|
+
return StepResult.Success
|
|
472
|
+
if t == StepType.MultiDrag:
|
|
473
|
+
return self._exec_multidrag(step)
|
|
474
|
+
if t == StepType.IfImageCount:
|
|
475
|
+
return self._exec_if_image_count(step)
|
|
476
|
+
if t == StepType.IfVar:
|
|
477
|
+
val = self._vars.get(step.varName, "0")
|
|
478
|
+
self._last_condition = self._compare(val, step.compareOp, step.compareValue)
|
|
479
|
+
self.cb.log(f" 条件(变量): {step.varName}={val} {step.compareOp} {step.compareValue} → "
|
|
480
|
+
f"{'true' if self._last_condition else 'false'}")
|
|
481
|
+
return StepResult.Success
|
|
482
|
+
if t == StepType.ClickAll:
|
|
483
|
+
return self._exec_click_all(step)
|
|
484
|
+
return StepResult.Fail
|
|
485
|
+
|
|
486
|
+
# ---- step helpers ----
|
|
487
|
+
def _exec_image_poll(self, step: Step, t: StepType) -> StepResult:
|
|
488
|
+
templates = self._template_list(step)
|
|
489
|
+
if not templates:
|
|
490
|
+
self.cb.log(" 错误: 未指定图片路径")
|
|
491
|
+
return StepResult.Fail
|
|
492
|
+
templs = [x for x in (self._load_template(p, step.display_name()) for p in templates) if x is not None]
|
|
493
|
+
if not templs:
|
|
494
|
+
return StepResult.Fail
|
|
495
|
+
|
|
496
|
+
if t == StepType.MergeImage:
|
|
497
|
+
return self._exec_merge(step, templs)
|
|
498
|
+
|
|
499
|
+
start = _now_ms()
|
|
500
|
+
while self._running and _now_ms() - start < step.timeoutMs:
|
|
501
|
+
screen = self._screenshot()
|
|
502
|
+
if t == StepType.WaitImageGone:
|
|
503
|
+
any_found = any(self.matcher.find_image_roi(screen, tm, step.threshold, step.roiRect, step.multiScale) for tm in templs)
|
|
504
|
+
if not any_found:
|
|
505
|
+
self.cb.log(f" 图片已消失 ({int(_now_ms() - start)}ms)")
|
|
506
|
+
return StepResult.Success
|
|
507
|
+
else:
|
|
508
|
+
for tm in templs:
|
|
509
|
+
results = self.matcher.find_image_roi(screen, tm, step.threshold, step.roiRect, step.multiScale)
|
|
510
|
+
if results:
|
|
511
|
+
if t == StepType.WaitImage:
|
|
512
|
+
self.cb.log(f" 图片已出现 ({int(_now_ms() - start)}ms)")
|
|
513
|
+
return StepResult.Success
|
|
514
|
+
best = results[0]
|
|
515
|
+
cx = self._rand_offset(best.cx + step.x, step.randomOffset)
|
|
516
|
+
cy = self._rand_offset(best.cy + step.y, step.randomOffset)
|
|
517
|
+
self.cb.match_found(cx, cy, best.confidence)
|
|
518
|
+
self.cb.log(f" 匹配成功 ({cx}, {cy}) 置信度: {best.confidence:.3f}")
|
|
519
|
+
self.clicker.click(self.target_hwnd, cx, cy)
|
|
520
|
+
self._vars["last_x"] = str(cx)
|
|
521
|
+
self._vars["last_y"] = str(cy)
|
|
522
|
+
return StepResult.Success
|
|
523
|
+
self._sleep_ms(500)
|
|
524
|
+
self.cb.log(" 超时: 图片未出现" if t == StepType.WaitImage
|
|
525
|
+
else (" 超时: 图片仍存在" if t == StepType.WaitImageGone else " 超时: 未找到图片"))
|
|
526
|
+
return StepResult.Fail
|
|
527
|
+
|
|
528
|
+
def _exec_merge(self, step: Step, templs) -> StepResult:
|
|
529
|
+
merge_count = 0
|
|
530
|
+
start = _now_ms()
|
|
531
|
+
while self._running and _now_ms() - start < step.timeoutMs:
|
|
532
|
+
screen = self._screenshot()
|
|
533
|
+
all_m = []
|
|
534
|
+
for tm in templs:
|
|
535
|
+
all_m += self.matcher.find_image_roi(screen, tm, step.threshold, step.roiRect, step.multiScale)
|
|
536
|
+
if len(all_m) < 2:
|
|
537
|
+
self.cb.log(f" 合成完成: 剩余 {len(all_m)} 个目标, 共合成 {merge_count} 次")
|
|
538
|
+
return StepResult.Success
|
|
539
|
+
a, b = all_m[0], all_m[1]
|
|
540
|
+
self.cb.log(f" 合成拖拽: ({a.cx},{a.cy}) → ({b.cx},{b.cy}) [找到 {len(all_m)} 个]")
|
|
541
|
+
self.clicker.drag(self.target_hwnd, a.cx, a.cy, b.cx, b.cy, step.durationMs)
|
|
542
|
+
merge_count += 1
|
|
543
|
+
self._sleep_ms(step.mergeDelayMs)
|
|
544
|
+
self.cb.log(f" 合成超时, 已合成 {merge_count} 次")
|
|
545
|
+
return StepResult.Success if merge_count > 0 else StepResult.Fail
|
|
546
|
+
|
|
547
|
+
def _exec_click_text(self, step: Step) -> StepResult:
|
|
548
|
+
start = _now_ms()
|
|
549
|
+
first = True
|
|
550
|
+
while self._running and _now_ms() - start < step.timeoutMs:
|
|
551
|
+
screen = self._screenshot()
|
|
552
|
+
all_ocr = self.matcher.ocr_all(screen)
|
|
553
|
+
if first:
|
|
554
|
+
self.cb.log(f" OCR返回: {len(all_ocr)} 处文字")
|
|
555
|
+
first = False
|
|
556
|
+
results = self.matcher.match_text(all_ocr, step.text, True)
|
|
557
|
+
if results:
|
|
558
|
+
best = results[0]
|
|
559
|
+
cx = self._rand_offset(best.cx + step.x, step.randomOffset)
|
|
560
|
+
cy = self._rand_offset(best.cy + step.y, step.randomOffset)
|
|
561
|
+
self.cb.match_found(cx, cy, best.confidence)
|
|
562
|
+
self.cb.log(f' OCR匹配: "{best.text}" ({cx}, {cy})')
|
|
563
|
+
self.clicker.click(self.target_hwnd, cx, cy)
|
|
564
|
+
self._vars["last_x"] = str(best.cx)
|
|
565
|
+
self._vars["last_y"] = str(best.cy)
|
|
566
|
+
self._vars["last_text"] = best.text
|
|
567
|
+
return StepResult.Success
|
|
568
|
+
self._sleep_ms(500)
|
|
569
|
+
self.cb.log(f' 超时: 未找到文字 "{step.text}"')
|
|
570
|
+
return StepResult.Fail
|
|
571
|
+
|
|
572
|
+
def _exec_read_text(self, step: Step) -> StepResult:
|
|
573
|
+
import re
|
|
574
|
+
screen = self._screenshot()
|
|
575
|
+
region, _, _ = self.matcher.apply_roi(screen, step.roiRect)
|
|
576
|
+
texts = [r.text for r in self.matcher.ocr_all(region)]
|
|
577
|
+
if step.text:
|
|
578
|
+
texts = [t for t in texts if step.text in t]
|
|
579
|
+
joined = " ".join(texts)
|
|
580
|
+
value = joined
|
|
581
|
+
if step.varValue == "number":
|
|
582
|
+
m = re.search(r"-?\d+(?:\.\d+)?", joined)
|
|
583
|
+
value = m.group(0) if m else ""
|
|
584
|
+
if step.varName:
|
|
585
|
+
self._vars[step.varName] = value
|
|
586
|
+
self.cb.var_changed(step.varName, value)
|
|
587
|
+
self._vars["last_text"] = joined
|
|
588
|
+
self.cb.log(f' 读取文字→{step.varName} = "{value}"')
|
|
589
|
+
return StepResult.Success
|
|
590
|
+
|
|
591
|
+
def _exec_if_image(self, step: Step) -> StepResult:
|
|
592
|
+
templates = self._template_list(step)
|
|
593
|
+
if not templates:
|
|
594
|
+
self._last_condition = False
|
|
595
|
+
return StepResult.Success
|
|
596
|
+
screen = self._screenshot()
|
|
597
|
+
self._last_condition = False
|
|
598
|
+
for p in templates:
|
|
599
|
+
templ = self._load_template(p, step.display_name())
|
|
600
|
+
if templ is None:
|
|
601
|
+
continue
|
|
602
|
+
if self.matcher.find_image_roi(screen, templ, step.threshold, step.roiRect, step.multiScale):
|
|
603
|
+
self._last_condition = True
|
|
604
|
+
break
|
|
605
|
+
self.cb.log(f" 条件(图片): {'true' if self._last_condition else 'false'}")
|
|
606
|
+
return StepResult.Success
|
|
607
|
+
|
|
608
|
+
def _exec_if_image_count(self, step: Step) -> StepResult:
|
|
609
|
+
templates = self._template_list(step)
|
|
610
|
+
if not templates:
|
|
611
|
+
self._last_condition = False
|
|
612
|
+
return StepResult.Success
|
|
613
|
+
screen = self._screenshot()
|
|
614
|
+
total = 0
|
|
615
|
+
for p in templates:
|
|
616
|
+
templ = self._load_template(p, step.display_name())
|
|
617
|
+
if templ is None:
|
|
618
|
+
continue
|
|
619
|
+
total += len(self.matcher.find_image_roi(screen, templ, step.threshold, step.roiRect, step.multiScale))
|
|
620
|
+
self._last_condition = total >= step.minMatchCount
|
|
621
|
+
self.cb.log(f" 条件(图片数量): 找到 {total} 个, 需要≥{step.minMatchCount} → "
|
|
622
|
+
f"{'true' if self._last_condition else 'false'}")
|
|
623
|
+
return StepResult.Success
|
|
624
|
+
|
|
625
|
+
def _exec_click_all(self, step: Step) -> StepResult:
|
|
626
|
+
templates = self._template_list(step)
|
|
627
|
+
if not templates:
|
|
628
|
+
self.cb.log(" 错误: 未指定图片路径")
|
|
629
|
+
return StepResult.Fail
|
|
630
|
+
screen = self._screenshot()
|
|
631
|
+
all_m = []
|
|
632
|
+
for p in templates:
|
|
633
|
+
templ = self._load_template(p, step.display_name())
|
|
634
|
+
if templ is None:
|
|
635
|
+
continue
|
|
636
|
+
all_m += self.matcher.find_image_roi(screen, templ, step.threshold, step.roiRect, step.multiScale)
|
|
637
|
+
if not all_m:
|
|
638
|
+
self.cb.log(" 未找到任何匹配")
|
|
639
|
+
return StepResult.Fail
|
|
640
|
+
self.cb.log(f" 找到 {len(all_m)} 个匹配, 逐个点击")
|
|
641
|
+
for i, m in enumerate(all_m):
|
|
642
|
+
if not self._running:
|
|
643
|
+
break
|
|
644
|
+
cx = self._rand_offset(m.cx + step.x, step.randomOffset)
|
|
645
|
+
cy = self._rand_offset(m.cy + step.y, step.randomOffset)
|
|
646
|
+
self.cb.log(f" 点击 {i + 1}/{len(all_m)}: ({cx}, {cy})")
|
|
647
|
+
self.clicker.click(self.target_hwnd, cx, cy)
|
|
648
|
+
if i < len(all_m) - 1:
|
|
649
|
+
self._sleep_ms(step.clickDelayMs)
|
|
650
|
+
self._vars["last_click_count"] = str(len(all_m))
|
|
651
|
+
return StepResult.Success
|
|
652
|
+
|
|
653
|
+
def _exec_multidrag(self, step: Step) -> StepResult:
|
|
654
|
+
if len(step.dragPoints) < 2:
|
|
655
|
+
self.cb.log(" 错误: 多点拖拽至少需要2个点")
|
|
656
|
+
return StepResult.Fail
|
|
657
|
+
pts = step.dragPoints
|
|
658
|
+
for i in range(len(pts) - 1):
|
|
659
|
+
if not self._running:
|
|
660
|
+
break
|
|
661
|
+
(x1, y1), (x2, y2) = pts[i], pts[i + 1]
|
|
662
|
+
self.cb.log(f" 拖拽 {i + 1}/{len(pts) - 1}: ({x1},{y1}) → ({x2},{y2})")
|
|
663
|
+
self.clicker.drag(self.target_hwnd, x1, y1, x2, y2, step.durationMs)
|
|
664
|
+
if i < len(pts) - 2:
|
|
665
|
+
self._sleep_ms(step.dragStepDelay)
|
|
666
|
+
return StepResult.Success
|