kotonebot 0.3.1__py3-none-any.whl → 0.5.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.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -302
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +49 -56
- kotonebot/backend/context/task_action.py +183 -175
- kotonebot/backend/core.py +129 -126
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/loop.py +12 -88
- kotonebot/backend/ocr.py +535 -529
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +528 -502
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -12
- kotonebot/client/host/adb_common.py +107 -94
- kotonebot/client/host/custom.py +118 -114
- kotonebot/client/host/leidian_host.py +196 -201
- kotonebot/client/host/mumu12_host.py +353 -358
- kotonebot/client/host/protocol.py +214 -213
- kotonebot/client/host/windows_common.py +58 -55
- kotonebot/client/implements/__init__.py +71 -7
- kotonebot/client/implements/adb.py +89 -85
- kotonebot/client/implements/adb_raw.py +162 -158
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -192
- kotonebot/client/implements/uiautomator2.py +85 -81
- kotonebot/client/implements/windows.py +176 -168
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/config/base_config.py +96 -96
- kotonebot/config/manager.py +36 -36
- kotonebot/errors.py +76 -71
- kotonebot/interop/win/__init__.py +10 -0
- kotonebot/interop/win/_mouse.py +311 -0
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -469
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +17 -17
- kotonebot/primitives/geometry.py +862 -290
- kotonebot/primitives/visual.py +63 -63
- kotonebot/tools/mirror.py +354 -354
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -87
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -53
- kotonebot/ui/user.py +148 -143
- kotonebot/util.py +436 -409
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
- kotonebot-0.5.0.dist-info/RECORD +71 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot-0.3.1.dist-info/RECORD +0 -70
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
kotonebot/client/device.py
CHANGED
|
@@ -1,503 +1,529 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from ..backend.debug import result
|
|
12
|
-
from ..errors import UnscalableResolutionError
|
|
13
|
-
from kotonebot.backend.core import HintBox
|
|
14
|
-
from kotonebot.primitives import Rect, Point, is_point
|
|
15
|
-
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable, AndroidCommandable, WindowsCommandable
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self.
|
|
23
|
-
self.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
else
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
self.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"""
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
1
|
+
from typing_extensions import deprecated
|
|
2
|
+
from typing import Callable, Literal, overload, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
from cv2.typing import MatLike
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
9
|
+
|
|
10
|
+
from kotonebot import logging
|
|
11
|
+
from ..backend.debug import result
|
|
12
|
+
from ..errors import UnscalableResolutionError
|
|
13
|
+
from kotonebot.backend.core import HintBox
|
|
14
|
+
from kotonebot.primitives import Rect, Point, is_point
|
|
15
|
+
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable, AndroidCommandable, WindowsCommandable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
LogLevel = Literal['info', 'debug', 'verbose', 'silent']
|
|
19
|
+
|
|
20
|
+
class HookContextManager:
|
|
21
|
+
def __init__(self, device: 'Device', func: Callable[[MatLike], MatLike]):
|
|
22
|
+
self.device = device
|
|
23
|
+
self.func = func
|
|
24
|
+
self.old_func = device.screenshot_hook_after
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
self.device.screenshot_hook_after = self.func
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
31
|
+
self.device.screenshot_hook_after = self.old_func
|
|
32
|
+
|
|
33
|
+
class Device:
|
|
34
|
+
def __init__(self, platform: str = 'unknown') -> None:
|
|
35
|
+
self.screenshot_hook_after: Callable[[MatLike], MatLike] | None = None
|
|
36
|
+
"""截图后调用的函数"""
|
|
37
|
+
self.screenshot_hook_before: Callable[[], MatLike | None] | None = None
|
|
38
|
+
"""截图前调用的函数。返回修改后的截图。"""
|
|
39
|
+
self.click_hooks_before: list[Callable[[int, int], tuple[int, int]]] = []
|
|
40
|
+
"""点击前调用的函数。返回修改后的点击坐标。"""
|
|
41
|
+
self.last_find: Rect | ClickableObjectProtocol | None = None
|
|
42
|
+
"""上次 image 对象或 ocr 对象的寻找结果"""
|
|
43
|
+
self.orientation: Literal['portrait', 'landscape'] = 'portrait'
|
|
44
|
+
"""
|
|
45
|
+
设备当前方向。默认为竖屏。注意此属性并非用于检测设备方向。
|
|
46
|
+
如果需要检测设备方向,请使用 `self.detect_orientation()` 方法。
|
|
47
|
+
|
|
48
|
+
横屏时为 'landscape',竖屏时为 'portrait'。
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
self._touch: Touchable
|
|
52
|
+
self._screenshot: Screenshotable
|
|
53
|
+
|
|
54
|
+
self.platform: str = platform
|
|
55
|
+
"""
|
|
56
|
+
设备平台名称。
|
|
57
|
+
"""
|
|
58
|
+
self.target_resolution: tuple[int, int] | None = None
|
|
59
|
+
"""
|
|
60
|
+
目标分辨率。
|
|
61
|
+
|
|
62
|
+
若设置,则在截图、点击、滑动等时会缩放到目标分辨率。
|
|
63
|
+
仅支持等比例缩放,若无法等比例缩放,则会抛出异常 `UnscalableResolutionError`。
|
|
64
|
+
"""
|
|
65
|
+
self.match_rotation: bool = True
|
|
66
|
+
"""
|
|
67
|
+
分辨率缩放是否自动匹配旋转。
|
|
68
|
+
|
|
69
|
+
当目标与真实分辨率的宽高比不一致时,是否允许通过旋转(交换宽高)后再进行匹配。
|
|
70
|
+
为 True 则忽略方向差异,只要宽高比一致就视为可缩放;False 则必须匹配旋转。
|
|
71
|
+
|
|
72
|
+
例如,当目标分辨率为 1920x1080,而真实分辨率为 1080x1920 时,
|
|
73
|
+
``match_rotation`` 为 True 则认为可以缩放,为 False 则会抛出异常。
|
|
74
|
+
"""
|
|
75
|
+
self.aspect_ratio_tolerance: float = 0.1
|
|
76
|
+
"""
|
|
77
|
+
宽高比容差阈值。
|
|
78
|
+
|
|
79
|
+
判断两分辨率宽高比差异是否接受的阈值。
|
|
80
|
+
该值越小,对比例一致性的要求越严格。
|
|
81
|
+
默认为 0.1(即 10% 容差)。
|
|
82
|
+
"""
|
|
83
|
+
self.log_level: LogLevel = 'debug'
|
|
84
|
+
"""默认日志级别。"""
|
|
85
|
+
|
|
86
|
+
def _scale_pos_real_to_target(self, real_x: int, real_y: int) -> tuple[int, int]:
|
|
87
|
+
"""将真实屏幕坐标缩放到目标逻辑坐标"""
|
|
88
|
+
if self.target_resolution is None:
|
|
89
|
+
return real_x, real_y
|
|
90
|
+
|
|
91
|
+
real_w, real_h = self.screen_size
|
|
92
|
+
target_w, target_h = self.target_resolution
|
|
93
|
+
|
|
94
|
+
# 校验分辨率是否可缩放并获取调整后的目标分辨率
|
|
95
|
+
adjusted_target_w, adjusted_target_h = self.__assert_scalable((real_w, real_h), (target_w, target_h))
|
|
96
|
+
|
|
97
|
+
scale_w = adjusted_target_w / real_w
|
|
98
|
+
scale_h = adjusted_target_h / real_h
|
|
99
|
+
|
|
100
|
+
return int(real_x * scale_w), int(real_y * scale_h)
|
|
101
|
+
|
|
102
|
+
def _scale_pos_target_to_real(self, target_x: int, target_y: int) -> tuple[int, int]:
|
|
103
|
+
"""将目标逻辑坐标缩放到真实屏幕坐标"""
|
|
104
|
+
if self.target_resolution is None:
|
|
105
|
+
return target_x, target_y # 输入坐标已是真实坐标
|
|
106
|
+
|
|
107
|
+
real_w, real_h = self.screen_size
|
|
108
|
+
target_w, target_h = self.target_resolution
|
|
109
|
+
|
|
110
|
+
# 校验分辨率是否可缩放并获取调整后的目标分辨率
|
|
111
|
+
adjusted_target_w, adjusted_target_h = self.__assert_scalable((real_w, real_h), (target_w, target_h))
|
|
112
|
+
|
|
113
|
+
scale_to_real_w = real_w / adjusted_target_w
|
|
114
|
+
scale_to_real_h = real_h / adjusted_target_h
|
|
115
|
+
|
|
116
|
+
return int(target_x * scale_to_real_w), int(target_y * scale_to_real_h)
|
|
117
|
+
|
|
118
|
+
def __scale_image (self, img: MatLike) -> MatLike:
|
|
119
|
+
if self.target_resolution is None:
|
|
120
|
+
return img
|
|
121
|
+
|
|
122
|
+
target_w, target_h = self.target_resolution
|
|
123
|
+
h, w = img.shape[:2]
|
|
124
|
+
|
|
125
|
+
# 校验分辨率是否可缩放并获取调整后的目标分辨率
|
|
126
|
+
adjusted_target = self.__assert_scalable((w, h), (target_w, target_h))
|
|
127
|
+
|
|
128
|
+
return cv2.resize(img, adjusted_target)
|
|
129
|
+
|
|
130
|
+
def __log(self, message: str, level: LogLevel | None = None, *args):
|
|
131
|
+
"""以指定的日志级别输出日志。
|
|
132
|
+
|
|
133
|
+
:param message: 要输出的日志信息。
|
|
134
|
+
:param level: 要使用的日志级别。可以是 'info', 'debug', 'verbose', 'silent' 中的一个,或者是 None。
|
|
135
|
+
如果为 None,则使用实例的 `log_level` 属性。
|
|
136
|
+
"""
|
|
137
|
+
effective_level = level if level is not None else self.log_level
|
|
138
|
+
|
|
139
|
+
if effective_level == 'info':
|
|
140
|
+
logger.info(message, *args)
|
|
141
|
+
elif effective_level == 'debug':
|
|
142
|
+
logger.debug(message, *args)
|
|
143
|
+
elif effective_level == 'verbose':
|
|
144
|
+
logger.verbose(message, *args)
|
|
145
|
+
elif effective_level == 'silent':
|
|
146
|
+
pass # Do nothing
|
|
147
|
+
|
|
148
|
+
@overload
|
|
149
|
+
def click(self, *, log: "LogLevel | None" = None) -> None:
|
|
150
|
+
"""
|
|
151
|
+
点击上次 `image` 对象或 `ocr` 对象的寻找结果(仅包括返回单个结果的函数)。
|
|
152
|
+
(不包括 `image.raw()` 和 `ocr.raw()` 的结果。)
|
|
153
|
+
|
|
154
|
+
如果没有上次寻找结果或上次寻找结果为空,会抛出异常 ValueError。
|
|
155
|
+
"""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
@overload
|
|
159
|
+
def click(self, x: int, y: int, *, log: "LogLevel | None" = None) -> None:
|
|
160
|
+
"""
|
|
161
|
+
点击屏幕上的某个点
|
|
162
|
+
"""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
@overload
|
|
166
|
+
def click(self, point: Point, *, log: "LogLevel | None" = None) -> None:
|
|
167
|
+
"""
|
|
168
|
+
点击屏幕上的某个点
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def click(self, rect: Rect, *, log: "LogLevel | None" = None) -> None:
|
|
174
|
+
"""
|
|
175
|
+
从屏幕上的某个矩形区域随机选择一个点并点击
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
@overload
|
|
180
|
+
def click(self, clickable: ClickableObjectProtocol, *, log: "LogLevel | None" = None) -> None:
|
|
181
|
+
"""
|
|
182
|
+
点击屏幕上的某个可点击对象
|
|
183
|
+
"""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
def click(self, *args, **kwargs) -> None:
|
|
187
|
+
log: LogLevel | None = kwargs.pop('log', None)
|
|
188
|
+
arg1 = args[0] if len(args) > 0 else None
|
|
189
|
+
arg2 = args[1] if len(args) > 1 else None
|
|
190
|
+
if arg1 is None:
|
|
191
|
+
self.__click_last(log=log)
|
|
192
|
+
elif isinstance(arg1, Rect):
|
|
193
|
+
self.__click_rect(arg1, log=log)
|
|
194
|
+
elif is_point(arg1):
|
|
195
|
+
self.__click_point_tuple(arg1, log=log)
|
|
196
|
+
elif isinstance(arg1, int) and isinstance(arg2, int):
|
|
197
|
+
self.__click_point(arg1, arg2, log=log)
|
|
198
|
+
elif isinstance(arg1, ClickableObjectProtocol):
|
|
199
|
+
self.__click_clickable(arg1, log=log)
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError(f"Invalid arguments: {arg1}, {arg2}")
|
|
202
|
+
|
|
203
|
+
def __click_last(self, *, log: "LogLevel | None" = None) -> None:
|
|
204
|
+
if self.last_find is None:
|
|
205
|
+
raise ValueError("No last find result. Make sure you are not calling the 'raw' functions.")
|
|
206
|
+
self.click(self.last_find, log=log)
|
|
207
|
+
|
|
208
|
+
def __click_rect(self, rect: Rect, *, log: "LogLevel | None" = None) -> None:
|
|
209
|
+
# 从矩形中心的 60% 内部随机选择一点
|
|
210
|
+
x = rect.x1 + rect.w // 2 + np.random.randint(-int(rect.w * 0.3), int(rect.w * 0.3))
|
|
211
|
+
y = rect.y1 + rect.h // 2 + np.random.randint(-int(rect.h * 0.3), int(rect.h * 0.3))
|
|
212
|
+
x = int(x)
|
|
213
|
+
y = int(y)
|
|
214
|
+
self.click(x, y, log=log)
|
|
215
|
+
|
|
216
|
+
def __click_point(self, x: int, y: int, *, log: "LogLevel | None" = None) -> None:
|
|
217
|
+
for hook in self.click_hooks_before:
|
|
218
|
+
logger.debug(f"Executing click hook before: ({x}, {y})")
|
|
219
|
+
x, y = hook(x, y)
|
|
220
|
+
logger.debug(f"Click hook before result: ({x}, {y})")
|
|
221
|
+
if self.target_resolution is not None:
|
|
222
|
+
# 输入坐标为逻辑坐标,需要转换为真实坐标
|
|
223
|
+
real_x, real_y = self._scale_pos_target_to_real(x, y)
|
|
224
|
+
else:
|
|
225
|
+
real_x, real_y = x, y
|
|
226
|
+
|
|
227
|
+
log_message = f"Click: {x}, {y}%s"
|
|
228
|
+
log_details = f"(Physical: {real_x}, {real_y})" if self.target_resolution is not None else ""
|
|
229
|
+
self.__log(log_message, log, log_details)
|
|
230
|
+
|
|
231
|
+
from ..backend.context import ContextStackVars
|
|
232
|
+
if ContextStackVars.current() is not None:
|
|
233
|
+
image = ContextStackVars.ensure_current()._screenshot
|
|
234
|
+
else:
|
|
235
|
+
image = np.array([])
|
|
236
|
+
if image is not None and image.size > 0:
|
|
237
|
+
cv2.circle(image, (x, y), 10, (0, 0, 255), -1)
|
|
238
|
+
message = f"Point: ({x}, {y})"
|
|
239
|
+
if self.target_resolution is not None:
|
|
240
|
+
message += f" physical: ({real_x}, {real_y})"
|
|
241
|
+
result("device.click", image, message)
|
|
242
|
+
self._touch.click(real_x, real_y)
|
|
243
|
+
|
|
244
|
+
def __click_point_tuple(self, point: Point, *, log: "LogLevel | None" = None) -> None:
|
|
245
|
+
self.click(point[0], point[1], log=log)
|
|
246
|
+
|
|
247
|
+
def __click_clickable(self, clickable: ClickableObjectProtocol, *, log: "LogLevel | None" = None) -> None:
|
|
248
|
+
self.click(clickable.rect, log=log)
|
|
249
|
+
|
|
250
|
+
def click_center(self, *, log: "LogLevel | None" = None) -> None:
|
|
251
|
+
"""
|
|
252
|
+
点击屏幕中心。
|
|
253
|
+
|
|
254
|
+
此方法会受到 `self.orientation` 的影响。
|
|
255
|
+
调用前确保 `orientation` 属性与设备方向一致,
|
|
256
|
+
否则点击位置会不正确。
|
|
257
|
+
"""
|
|
258
|
+
size = self.target_resolution or self.screen_size
|
|
259
|
+
x, y = size[0] // 2, size[1] // 2
|
|
260
|
+
self.click(x, y, log=log)
|
|
261
|
+
|
|
262
|
+
@overload
|
|
263
|
+
def double_click(self, x: int, y: int, interval: float = 0.4, *, log: "LogLevel | None" = None) -> None:
|
|
264
|
+
"""
|
|
265
|
+
双击屏幕上的某个点
|
|
266
|
+
"""
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
@overload
|
|
270
|
+
def double_click(self, rect: Rect, interval: float = 0.4, *, log: "LogLevel | None" = None) -> None:
|
|
271
|
+
"""
|
|
272
|
+
双击屏幕上的某个矩形区域
|
|
273
|
+
"""
|
|
274
|
+
...
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def double_click(self, clickable: ClickableObjectProtocol, interval: float = 0.4, *, log: "LogLevel | None" = None) -> None:
|
|
278
|
+
"""
|
|
279
|
+
双击屏幕上的某个可点击对象
|
|
280
|
+
"""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
def double_click(self, *args, **kwargs) -> None:
|
|
284
|
+
from kotonebot import sleep
|
|
285
|
+
arg0 = args[0]
|
|
286
|
+
log = kwargs.get('log', None)
|
|
287
|
+
if isinstance(arg0, Rect) or isinstance(arg0, ClickableObjectProtocol):
|
|
288
|
+
rect = arg0
|
|
289
|
+
interval = kwargs.get('interval', 0.4)
|
|
290
|
+
self.click(rect, log=log)
|
|
291
|
+
sleep(interval)
|
|
292
|
+
self.click(rect, log=log)
|
|
293
|
+
else:
|
|
294
|
+
x = args[0]
|
|
295
|
+
y = args[1]
|
|
296
|
+
interval = kwargs.get('interval', 0.4)
|
|
297
|
+
self.click(x, y, log=log)
|
|
298
|
+
sleep(interval)
|
|
299
|
+
self.click(x, y, log=log)
|
|
300
|
+
|
|
301
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None, *, log: "LogLevel | None" = None) -> None:
|
|
302
|
+
"""
|
|
303
|
+
滑动屏幕
|
|
304
|
+
"""
|
|
305
|
+
if self.target_resolution is not None:
|
|
306
|
+
# 输入坐标为逻辑坐标,需要转换为真实坐标
|
|
307
|
+
real_x1, real_y1 = self._scale_pos_target_to_real(x1, y1)
|
|
308
|
+
real_x2, real_y2 = self._scale_pos_target_to_real(x2, y2)
|
|
309
|
+
log_message = f"Swipe: from ({x1}, {y1}) to ({x2}, {y2}) (Physical: from ({real_x1}, {real_y1}) to ({real_x2}, {real_y2}))"
|
|
310
|
+
else:
|
|
311
|
+
real_x1, real_y1 = x1, y1
|
|
312
|
+
real_x2, real_y2 = x2, y2
|
|
313
|
+
log_message = f"Swipe: from ({x1}, {y1}) to ({x2}, {y2})"
|
|
314
|
+
|
|
315
|
+
self.__log(log_message, log)
|
|
316
|
+
|
|
317
|
+
self._touch.swipe(real_x1, real_y1, real_x2, real_y2, duration)
|
|
318
|
+
|
|
319
|
+
def swipe_scaled(self, x1: float, y1: float, x2: float, y2: float, duration: float|None = None, *, log: "LogLevel | None" = None) -> None:
|
|
320
|
+
"""
|
|
321
|
+
滑动屏幕,参数为屏幕坐标的百分比。
|
|
322
|
+
|
|
323
|
+
如果设置了 `self.target_resolution`,则参数为逻辑坐标百分比。
|
|
324
|
+
否则为真实坐标百分比。
|
|
325
|
+
|
|
326
|
+
:param x1: 起始点 x 坐标百分比。范围 [0, 1]
|
|
327
|
+
:param y1: 起始点 y 坐标百分比。范围 [0, 1]
|
|
328
|
+
:param x2: 结束点 x 坐标百分比。范围 [0, 1]
|
|
329
|
+
:param y2: 结束点 y 坐标百分比。范围 [0, 1]
|
|
330
|
+
:param duration: 滑动持续时间,单位秒。None 表示使用默认值。
|
|
331
|
+
"""
|
|
332
|
+
w, h = self.target_resolution or self.screen_size
|
|
333
|
+
self.swipe(int(w * x1), int(h * y1), int(w * x2), int(h * y2), duration, log=log)
|
|
334
|
+
|
|
335
|
+
def screenshot(self) -> MatLike:
|
|
336
|
+
"""
|
|
337
|
+
截图
|
|
338
|
+
"""
|
|
339
|
+
if self.screenshot_hook_before is not None:
|
|
340
|
+
logger.debug("execute screenshot hook before")
|
|
341
|
+
img = self.screenshot_hook_before()
|
|
342
|
+
if img is not None:
|
|
343
|
+
logger.debug("screenshot hook before returned image")
|
|
344
|
+
return img
|
|
345
|
+
img = self.screenshot_raw()
|
|
346
|
+
img = self.__scale_image(img)
|
|
347
|
+
if self.screenshot_hook_after is not None:
|
|
348
|
+
img = self.screenshot_hook_after(img)
|
|
349
|
+
return img
|
|
350
|
+
|
|
351
|
+
def screenshot_raw(self) -> MatLike:
|
|
352
|
+
"""
|
|
353
|
+
截图,不调用任何 Hook。
|
|
354
|
+
"""
|
|
355
|
+
return self._screenshot.screenshot()
|
|
356
|
+
|
|
357
|
+
def hook(self, func: Callable[[MatLike], MatLike]) -> HookContextManager:
|
|
358
|
+
"""
|
|
359
|
+
注册 Hook,在截图前将会调用此函数,对截图进行处理
|
|
360
|
+
"""
|
|
361
|
+
return HookContextManager(self, func)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def screen_size(self) -> tuple[int, int]:
|
|
365
|
+
"""
|
|
366
|
+
真实屏幕尺寸。格式为 `(width, height)`。
|
|
367
|
+
|
|
368
|
+
**注意**: 此属性返回的分辨率会随设备方向变化。
|
|
369
|
+
如果 `self.orientation` 为 `landscape`,则返回的分辨率是横屏下的分辨率,
|
|
370
|
+
否则返回竖屏下的分辨率。
|
|
371
|
+
|
|
372
|
+
`self.orientation` 属性默认为竖屏。如果需要自动检测,
|
|
373
|
+
调用 `self.detect_orientation()` 方法。
|
|
374
|
+
如果已知方向,也可以直接设置 `self.orientation` 属性。
|
|
375
|
+
|
|
376
|
+
即使设置了 `self.target_resolution`,返回的分辨率仍然是真实分辨率。
|
|
377
|
+
"""
|
|
378
|
+
size = self._screenshot.screen_size
|
|
379
|
+
if self.orientation == 'landscape':
|
|
380
|
+
size = sorted(size, reverse=True)
|
|
381
|
+
else:
|
|
382
|
+
size = sorted(size, reverse=False)
|
|
383
|
+
return size[0], size[1]
|
|
384
|
+
|
|
385
|
+
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
|
|
386
|
+
"""
|
|
387
|
+
检测当前设备方向并设置 `self.orientation` 属性。
|
|
388
|
+
|
|
389
|
+
:return: 检测到的方向,如果无法检测到则返回 None。
|
|
390
|
+
"""
|
|
391
|
+
return self._screenshot.detect_orientation()
|
|
392
|
+
|
|
393
|
+
def __aspect_ratio_compatible(self, src_size: tuple[int, int], tgt_size: tuple[int, int]) -> bool:
|
|
394
|
+
"""
|
|
395
|
+
判断两个尺寸在宽高比意义上是否兼容
|
|
396
|
+
|
|
397
|
+
若 ``self.match_rotation`` 为 True,忽略方向(长边/短边)进行比较。
|
|
398
|
+
判断标准由 ``self.aspect_ratio_tolerance`` 决定(默认 0.1)。
|
|
399
|
+
"""
|
|
400
|
+
src_w, src_h = src_size
|
|
401
|
+
tgt_w, tgt_h = tgt_size
|
|
402
|
+
|
|
403
|
+
# 尺寸必须为正
|
|
404
|
+
if src_w <= 0 or src_h <= 0:
|
|
405
|
+
raise ValueError(f"Source size dimensions must be positive for scaling: {src_size}")
|
|
406
|
+
if tgt_w <= 0 or tgt_h <= 0:
|
|
407
|
+
raise ValueError(f"Target size dimensions must be positive for scaling: {tgt_size}")
|
|
408
|
+
|
|
409
|
+
tolerant = self.aspect_ratio_tolerance
|
|
410
|
+
|
|
411
|
+
# 直接比较宽高比
|
|
412
|
+
if abs((tgt_w / src_w) - (tgt_h / src_h)) <= tolerant:
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
# 尝试忽略方向差异
|
|
416
|
+
if self.match_rotation:
|
|
417
|
+
ratio_src = max(src_w, src_h) / min(src_w, src_h)
|
|
418
|
+
ratio_tgt = max(tgt_w, tgt_h) / min(tgt_w, tgt_h)
|
|
419
|
+
return abs(ratio_src - ratio_tgt) <= tolerant
|
|
420
|
+
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def __assert_scalable(self, source: tuple[int, int], target: tuple[int, int]) -> tuple[int, int]:
|
|
424
|
+
"""
|
|
425
|
+
校验分辨率是否可缩放,并返回调整后的目标分辨率。
|
|
426
|
+
|
|
427
|
+
当 match_rotation 为 True 且源分辨率与目标分辨率的旋转方向不一致时,
|
|
428
|
+
自动交换目标分辨率的宽高,使其与源分辨率的方向保持一致。
|
|
429
|
+
|
|
430
|
+
:param src_size: 源分辨率 (width, height)
|
|
431
|
+
:param tgt_size: 目标分辨率 (width, height)
|
|
432
|
+
:return: 调整后的目标分辨率 (width, height)
|
|
433
|
+
:raises UnscalableResolutionError: 若宽高比不兼容
|
|
434
|
+
"""
|
|
435
|
+
# 智能调整目标分辨率方向
|
|
436
|
+
adjusted_tgt_size = target
|
|
437
|
+
if self.match_rotation:
|
|
438
|
+
src_w, src_h = source
|
|
439
|
+
tgt_w, tgt_h = target
|
|
440
|
+
|
|
441
|
+
# 判断源分辨率和目标分辨率的方向
|
|
442
|
+
src_is_landscape = src_w > src_h
|
|
443
|
+
tgt_is_landscape = tgt_w > tgt_h
|
|
444
|
+
|
|
445
|
+
# 如果方向不一致,交换目标分辨率的宽高
|
|
446
|
+
if src_is_landscape != tgt_is_landscape:
|
|
447
|
+
adjusted_tgt_size = (tgt_h, tgt_w)
|
|
448
|
+
|
|
449
|
+
# 校验调整后的分辨率是否兼容
|
|
450
|
+
if not self.__aspect_ratio_compatible(source, adjusted_tgt_size):
|
|
451
|
+
raise UnscalableResolutionError(target, source)
|
|
452
|
+
|
|
453
|
+
return adjusted_tgt_size
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class AndroidDevice(Device):
|
|
457
|
+
def __init__(self, adb_connection: 'AdbUtilsDevice | None' = None) -> None:
|
|
458
|
+
super().__init__('android')
|
|
459
|
+
self._adb: 'AdbUtilsDevice | None' = adb_connection
|
|
460
|
+
self.commands: AndroidCommandable
|
|
461
|
+
|
|
462
|
+
def current_package(self) -> str | None:
|
|
463
|
+
"""
|
|
464
|
+
获取前台 APP 的包名。
|
|
465
|
+
|
|
466
|
+
:return: 前台 APP 的包名。如果获取失败,则返回 None。
|
|
467
|
+
:exception: 如果设备不支持此功能,则抛出 NotImplementedError。
|
|
468
|
+
"""
|
|
469
|
+
ret = self.commands.current_package()
|
|
470
|
+
logger.debug("current_package: %s", ret)
|
|
471
|
+
return ret
|
|
472
|
+
|
|
473
|
+
def launch_app(self, package_name: str) -> None:
|
|
474
|
+
"""
|
|
475
|
+
根据包名启动 app
|
|
476
|
+
"""
|
|
477
|
+
self.commands.launch_app(package_name)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class WindowsDevice(Device):
|
|
481
|
+
def __init__(self) -> None:
|
|
482
|
+
super().__init__('windows')
|
|
483
|
+
self.commands: WindowsCommandable
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
if __name__ == "__main__":
|
|
487
|
+
from kotonebot.client.implements.adb import AdbImpl
|
|
488
|
+
from kotonebot.client.implements.adb_raw import AdbRawImpl
|
|
489
|
+
from .implements.uiautomator2 import UiAutomator2Impl
|
|
490
|
+
print("server version:", adb.server_version())
|
|
491
|
+
adb.connect("127.0.0.1:5555")
|
|
492
|
+
print("devices:", adb.device_list())
|
|
493
|
+
d = adb.device_list()[-1]
|
|
494
|
+
d.shell("dumpsys activity top | grep ACTIVITY | tail -n 1")
|
|
495
|
+
dd = AndroidDevice(d)
|
|
496
|
+
adb_imp = AdbRawImpl(d)
|
|
497
|
+
dd._touch = adb_imp
|
|
498
|
+
dd._screenshot = adb_imp
|
|
499
|
+
dd.commands = adb_imp
|
|
500
|
+
# dd._screenshot = MinicapScreenshotImpl(dd)
|
|
501
|
+
# dd._screenshot = UiAutomator2Impl(dd)
|
|
502
|
+
|
|
503
|
+
# 实时展示画面
|
|
504
|
+
import cv2
|
|
505
|
+
import numpy as np
|
|
506
|
+
import time
|
|
507
|
+
last_time = time.time()
|
|
508
|
+
while True:
|
|
509
|
+
start_time = time.time()
|
|
510
|
+
img = dd.screenshot()
|
|
511
|
+
# 50% 缩放
|
|
512
|
+
img = cv2.resize(img, (img.shape[1] // 2, img.shape[0] // 2))
|
|
513
|
+
|
|
514
|
+
# 计算帧间隔
|
|
515
|
+
interval = start_time - last_time
|
|
516
|
+
fps = 1 / interval if interval > 0 else 0
|
|
517
|
+
last_time = start_time
|
|
518
|
+
|
|
519
|
+
# 获取当前时间和帧率信息
|
|
520
|
+
current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
521
|
+
fps_text = f"FPS: {fps:.1f} {interval*1000:.1f}ms"
|
|
522
|
+
|
|
523
|
+
# 在图像上绘制信息
|
|
524
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
525
|
+
cv2.putText(img, current_time, (10, 30), font, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
|
526
|
+
cv2.putText(img, fps_text, (10, 60), font, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
|
|
527
|
+
|
|
528
|
+
cv2.imshow("screen", img)
|
|
503
529
|
cv2.waitKey(1)
|