kotonebot 0.1.0__py3-none-any.whl → 0.2.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/backend/context/context.py +1008 -1000
- kotonebot/backend/debug/vars.py +6 -1
- kotonebot/backend/image.py +778 -748
- kotonebot/backend/loop.py +283 -276
- kotonebot/client/device.py +6 -3
- kotonebot-0.2.0.dist-info/METADATA +76 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.2.0.dist-info}/RECORD +10 -10
- kotonebot-0.1.0.dist-info/METADATA +0 -204
- {kotonebot-0.1.0.dist-info → kotonebot-0.2.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,1001 +1,1009 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import time
|
|
4
|
-
import logging
|
|
5
|
-
import warnings
|
|
6
|
-
import threading
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from threading import Event
|
|
9
|
-
from typing import (
|
|
10
|
-
Callable,
|
|
11
|
-
Optional,
|
|
12
|
-
cast,
|
|
13
|
-
overload,
|
|
14
|
-
Any,
|
|
15
|
-
TypeVar,
|
|
16
|
-
Literal,
|
|
17
|
-
ParamSpec,
|
|
18
|
-
Concatenate,
|
|
19
|
-
Generic,
|
|
20
|
-
Type,
|
|
21
|
-
Sequence,
|
|
22
|
-
)
|
|
23
|
-
from typing_extensions import deprecated
|
|
24
|
-
|
|
25
|
-
import cv2
|
|
26
|
-
from cv2.typing import MatLike
|
|
27
|
-
|
|
28
|
-
from kotonebot.client.device import Device, AndroidDevice, WindowsDevice
|
|
29
|
-
from kotonebot.backend.flow_controller import FlowController
|
|
30
|
-
from kotonebot.util import Interval
|
|
31
|
-
import kotonebot.backend.image as raw_image
|
|
32
|
-
from kotonebot.backend.image import (
|
|
33
|
-
TemplateMatchResult,
|
|
34
|
-
MultipleTemplateMatchResult,
|
|
35
|
-
find_all_crop,
|
|
36
|
-
expect,
|
|
37
|
-
find as image_find,
|
|
38
|
-
find_multi as image_find_multi,
|
|
39
|
-
find_all as image_find_all,
|
|
40
|
-
find_all_multi as image_find_all_multi,
|
|
41
|
-
count as image_count
|
|
42
|
-
)
|
|
43
|
-
import kotonebot.backend.color as raw_color
|
|
44
|
-
from kotonebot.backend.color import (
|
|
45
|
-
find as color_find, find_all as color_find_all
|
|
46
|
-
)
|
|
47
|
-
from kotonebot.backend.ocr import (
|
|
48
|
-
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
|
49
|
-
)
|
|
50
|
-
from kotonebot.config.manager import load_config, save_config
|
|
51
|
-
from kotonebot.config.base_config import UserConfig
|
|
52
|
-
from kotonebot.backend.core import Image, HintBox
|
|
53
|
-
from kotonebot.errors import ContextNotInitializedError, KotonebotWarning
|
|
54
|
-
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
|
55
|
-
from kotonebot.primitives import Rect
|
|
56
|
-
|
|
57
|
-
OcrLanguage = Literal['jp', 'en']
|
|
58
|
-
ScreenshotMode = Literal['auto', 'manual', 'manual-inherit']
|
|
59
|
-
DEFAULT_TIMEOUT = 120
|
|
60
|
-
DEFAULT_INTERVAL = 0.4
|
|
61
|
-
logger = logging.getLogger(__name__)
|
|
62
|
-
|
|
63
|
-
# https://stackoverflow.com/questions/74714300/paramspec-for-a-pre-defined-function-without-using-generic-callablep
|
|
64
|
-
T = TypeVar('T')
|
|
65
|
-
P = ParamSpec('P')
|
|
66
|
-
ContextClass = TypeVar("ContextClass")
|
|
67
|
-
|
|
68
|
-
def context(
|
|
69
|
-
_: Callable[Concatenate[MatLike, P], T] # 输入函数
|
|
70
|
-
) -> Callable[
|
|
71
|
-
[Callable[Concatenate[ContextClass, P], T]], # 被装饰函数
|
|
72
|
-
Callable[Concatenate[ContextClass, P], T] # 结果函数
|
|
73
|
-
]:
|
|
74
|
-
"""
|
|
75
|
-
用于标记 Context 类方法的装饰器。
|
|
76
|
-
此装饰器仅用于辅助类型标注,运行时无实际功能。
|
|
77
|
-
|
|
78
|
-
装饰器输入的函数类型为 `(img: MatLike, a, b, c, ...) -> T`,
|
|
79
|
-
被装饰的函数类型为 `(self: ContextClass, *args, **kwargs) -> T`,
|
|
80
|
-
结果类型为 `(self: ContextClass, a, b, c, ...) -> T`。
|
|
81
|
-
|
|
82
|
-
也就是说,`@context` 会把输入函数的第一个参数 `img: MatLike` 删除,
|
|
83
|
-
然后再添加 `self` 作为第一个参数。
|
|
84
|
-
|
|
85
|
-
【例】
|
|
86
|
-
```python
|
|
87
|
-
def find_image(
|
|
88
|
-
img: MatLike,
|
|
89
|
-
mask: MatLike,
|
|
90
|
-
threshold: float = 0.9
|
|
91
|
-
) -> TemplateMatchResult | None:
|
|
92
|
-
...
|
|
93
|
-
```
|
|
94
|
-
```python
|
|
95
|
-
class ContextImage:
|
|
96
|
-
@context(find_image)
|
|
97
|
-
def find_image(self, *args, **kwargs):
|
|
98
|
-
return find_image(
|
|
99
|
-
self.context.device.screenshot(),
|
|
100
|
-
*args,
|
|
101
|
-
**kwargs
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
```python
|
|
106
|
-
|
|
107
|
-
c = ContextImage()
|
|
108
|
-
c.find_image()
|
|
109
|
-
# 此函数类型推断为 (
|
|
110
|
-
# self: ContextImage,
|
|
111
|
-
# img: MatLike,
|
|
112
|
-
# mask: MatLike,
|
|
113
|
-
# threshold: float = 0.9
|
|
114
|
-
# ) -> TemplateMatchResult | None
|
|
115
|
-
```
|
|
116
|
-
"""
|
|
117
|
-
def _decorator(func):
|
|
118
|
-
return func
|
|
119
|
-
return _decorator
|
|
120
|
-
|
|
121
|
-
def interruptible(func: Callable[P, T]) -> Callable[P, T]:
|
|
122
|
-
"""
|
|
123
|
-
将函数包装为可中断函数。
|
|
124
|
-
|
|
125
|
-
在调用函数前,自动检查用户是否请求中断。
|
|
126
|
-
如果用户请求中断,则抛出 `KeyboardInterrupt` 异常。
|
|
127
|
-
"""
|
|
128
|
-
def _decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
129
|
-
global vars
|
|
130
|
-
vars.flow.check()
|
|
131
|
-
return func(*args, **kwargs)
|
|
132
|
-
return _decorator
|
|
133
|
-
|
|
134
|
-
def interruptible_class(cls: Type[T]) -> Type[T]:
|
|
135
|
-
"""
|
|
136
|
-
将类中的所有方法包装为可中断方法。
|
|
137
|
-
|
|
138
|
-
在调用方法前,自动检查用户是否请求中断。
|
|
139
|
-
如果用户请求中断,则抛出 `KeyboardInterrupt` 异常。
|
|
140
|
-
"""
|
|
141
|
-
for name, func in cls.__dict__.items():
|
|
142
|
-
if callable(func) and not name.startswith('__'):
|
|
143
|
-
setattr(cls, name, interruptible(func))
|
|
144
|
-
return cls
|
|
145
|
-
|
|
146
|
-
def sleep(seconds: float, /):
|
|
147
|
-
"""
|
|
148
|
-
可中断和可暂停的 sleep 函数。
|
|
149
|
-
|
|
150
|
-
建议使用本函数代替 `time.sleep()`,
|
|
151
|
-
这样能以最快速度响应用户请求中断和暂停。
|
|
152
|
-
"""
|
|
153
|
-
global vars
|
|
154
|
-
vars.flow.sleep(seconds)
|
|
155
|
-
|
|
156
|
-
def warn_manual_screenshot_mode(name: str, alternative: str):
|
|
157
|
-
"""
|
|
158
|
-
警告在手动截图模式下使用的方法。
|
|
159
|
-
"""
|
|
160
|
-
warnings.warn(
|
|
161
|
-
f"You are calling `{name}` function in manual screenshot mode. "
|
|
162
|
-
f"This is meaningless. Write you own while loop and call `{alternative}` in the loop.",
|
|
163
|
-
KotonebotWarning
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
def is_manual_screenshot_mode() -> bool:
|
|
167
|
-
"""
|
|
168
|
-
检查当前是否处于手动截图模式。
|
|
169
|
-
"""
|
|
170
|
-
mode = ContextStackVars.ensure_current().screenshot_mode
|
|
171
|
-
return mode == 'manual' or mode == 'manual-inherit'
|
|
172
|
-
|
|
173
|
-
class ContextGlobalVars:
|
|
174
|
-
def __init__(self):
|
|
175
|
-
self.__vars = dict[str, Any]()
|
|
176
|
-
self.flow: FlowController = FlowController()
|
|
177
|
-
"""流程控制器,负责停止、暂停、恢复等操作"""
|
|
178
|
-
|
|
179
|
-
def __getitem__(self, key: str) -> Any:
|
|
180
|
-
return self.__vars[key]
|
|
181
|
-
|
|
182
|
-
def __setitem__(self, key: str, value: Any) -> None:
|
|
183
|
-
self.__vars[key] = value
|
|
184
|
-
|
|
185
|
-
def __delitem__(self, key: str) -> None:
|
|
186
|
-
del self.__vars[key]
|
|
187
|
-
|
|
188
|
-
def __contains__(self, key: str) -> bool:
|
|
189
|
-
return key in self.__vars
|
|
190
|
-
|
|
191
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
192
|
-
return self.__vars.get(key, default)
|
|
193
|
-
|
|
194
|
-
def set(self, key: str, value: Any) -> None:
|
|
195
|
-
self.__vars[key] = value
|
|
196
|
-
|
|
197
|
-
def clear(self):
|
|
198
|
-
self.__vars.clear()
|
|
199
|
-
self.flow.reset() # 重置流程控制器
|
|
200
|
-
|
|
201
|
-
def check_flow_control():
|
|
202
|
-
"""
|
|
203
|
-
统一的流程控制检查函数。
|
|
204
|
-
|
|
205
|
-
检查用户是否请求中断或暂停,如果是则相应处理:
|
|
206
|
-
- 如果请求中断,抛出 KeyboardInterrupt 异常
|
|
207
|
-
- 如果请求暂停,等待直到恢复
|
|
208
|
-
"""
|
|
209
|
-
vars.flow.check()
|
|
210
|
-
|
|
211
|
-
class ContextStackVars:
|
|
212
|
-
stack: list['ContextStackVars'] = []
|
|
213
|
-
|
|
214
|
-
def __init__(self):
|
|
215
|
-
self.screenshot_mode: ScreenshotMode = 'auto'
|
|
216
|
-
"""
|
|
217
|
-
截图模式。
|
|
218
|
-
|
|
219
|
-
* `auto`
|
|
220
|
-
自动截图。即调用 `color`、`image`、`ocr` 上的方法时,会自动更新截图。
|
|
221
|
-
* `manual`
|
|
222
|
-
完全手动截图,不自动截图。如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
223
|
-
* `manual-inherit`:
|
|
224
|
-
第一张截图继承自调用者,此后同手动截图。
|
|
225
|
-
如果调用者没有截图数据,则继承的截图数据为空。
|
|
226
|
-
如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
227
|
-
"""
|
|
228
|
-
self._screenshot: MatLike | None = None
|
|
229
|
-
"""截图数据"""
|
|
230
|
-
self._inherit_screenshot: MatLike | None = None
|
|
231
|
-
"""继承的截图数据"""
|
|
232
|
-
|
|
233
|
-
@property
|
|
234
|
-
def screenshot(self) -> MatLike:
|
|
235
|
-
match self.screenshot_mode:
|
|
236
|
-
case 'manual':
|
|
237
|
-
if self._screenshot is None:
|
|
238
|
-
raise ValueError("No screenshot data found.")
|
|
239
|
-
return self._screenshot
|
|
240
|
-
case 'manual-inherit':
|
|
241
|
-
# TODO: 这一部分要考虑和 device.screenshot() 合并
|
|
242
|
-
if self._inherit_screenshot is not None:
|
|
243
|
-
self._screenshot = self._inherit_screenshot
|
|
244
|
-
self._inherit_screenshot = None
|
|
245
|
-
if self._screenshot is None:
|
|
246
|
-
raise ValueError("No screenshot data found.")
|
|
247
|
-
return self._screenshot
|
|
248
|
-
case 'auto':
|
|
249
|
-
self._screenshot = device.screenshot()
|
|
250
|
-
return self._screenshot
|
|
251
|
-
case _:
|
|
252
|
-
raise ValueError(f"Invalid screenshot mode: {self.screenshot_mode}")
|
|
253
|
-
|
|
254
|
-
@staticmethod
|
|
255
|
-
def push(*, screenshot_mode: ScreenshotMode | None = None) -> 'ContextStackVars':
|
|
256
|
-
vars = ContextStackVars()
|
|
257
|
-
if screenshot_mode is not None:
|
|
258
|
-
vars.screenshot_mode = screenshot_mode
|
|
259
|
-
current = ContextStackVars.current()
|
|
260
|
-
if current and vars.screenshot_mode == 'manual-inherit':
|
|
261
|
-
vars._inherit_screenshot = current._screenshot
|
|
262
|
-
ContextStackVars.stack.append(vars)
|
|
263
|
-
return vars
|
|
264
|
-
|
|
265
|
-
@staticmethod
|
|
266
|
-
def pop() -> 'ContextStackVars':
|
|
267
|
-
last = ContextStackVars.stack.pop()
|
|
268
|
-
return last
|
|
269
|
-
|
|
270
|
-
@staticmethod
|
|
271
|
-
def current() -> 'ContextStackVars | None':
|
|
272
|
-
if len(ContextStackVars.stack) == 0:
|
|
273
|
-
return None
|
|
274
|
-
return ContextStackVars.stack[-1]
|
|
275
|
-
|
|
276
|
-
@staticmethod
|
|
277
|
-
def ensure_current() -> 'ContextStackVars':
|
|
278
|
-
if len(ContextStackVars.stack) == 0:
|
|
279
|
-
raise ValueError("No context stack found.")
|
|
280
|
-
return ContextStackVars.stack[-1]
|
|
281
|
-
|
|
282
|
-
@interruptible_class
|
|
283
|
-
class ContextOcr:
|
|
284
|
-
def __init__(self, context: 'Context'):
|
|
285
|
-
self.context = context
|
|
286
|
-
self.__engine = jp()
|
|
287
|
-
|
|
288
|
-
def _get_engine(self, lang: OcrLanguage | None = None) -> Ocr:
|
|
289
|
-
"""获取指定语言的OCR引擎,如果lang为None则使用默认引擎。"""
|
|
290
|
-
return self.__engine if lang is None else self.raw(lang)
|
|
291
|
-
|
|
292
|
-
def raw(self, lang: OcrLanguage | None = None) -> Ocr:
|
|
293
|
-
"""
|
|
294
|
-
返回 `kotonebot.backend.ocr` 中的 Ocr 对象。\n
|
|
295
|
-
Ocr 对象与此对象(ContextOcr)的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。
|
|
296
|
-
"""
|
|
297
|
-
if lang is None:
|
|
298
|
-
lang = 'jp'
|
|
299
|
-
match lang:
|
|
300
|
-
case 'jp':
|
|
301
|
-
return jp()
|
|
302
|
-
case 'en':
|
|
303
|
-
return en()
|
|
304
|
-
case _:
|
|
305
|
-
raise ValueError(f"Invalid language: {lang}")
|
|
306
|
-
|
|
307
|
-
def ocr(
|
|
308
|
-
self,
|
|
309
|
-
rect: Rect | None = None,
|
|
310
|
-
lang: OcrLanguage | None = None,
|
|
311
|
-
) -> OcrResultList:
|
|
312
|
-
"""OCR 当前设备画面或指定图像。"""
|
|
313
|
-
engine = self._get_engine(lang)
|
|
314
|
-
return engine.ocr(ContextStackVars.ensure_current().screenshot, rect=rect)
|
|
315
|
-
|
|
316
|
-
def find(
|
|
317
|
-
self,
|
|
318
|
-
pattern: str | re.Pattern | StringMatchFunction,
|
|
319
|
-
*,
|
|
320
|
-
hint: HintBox | None = None,
|
|
321
|
-
rect: Rect | None = None,
|
|
322
|
-
lang: OcrLanguage | None = None,
|
|
323
|
-
) -> OcrResult | None:
|
|
324
|
-
"""检查当前设备画面是否包含指定文本。"""
|
|
325
|
-
engine = self._get_engine(lang)
|
|
326
|
-
ret = engine.find(
|
|
327
|
-
ContextStackVars.ensure_current().screenshot,
|
|
328
|
-
pattern,
|
|
329
|
-
hint=hint,
|
|
330
|
-
rect=rect,
|
|
331
|
-
)
|
|
332
|
-
self.context.device.last_find = ret.original_rect if ret else None
|
|
333
|
-
return ret
|
|
334
|
-
|
|
335
|
-
def find_all(
|
|
336
|
-
self,
|
|
337
|
-
patterns: Sequence[str | re.Pattern | StringMatchFunction],
|
|
338
|
-
*,
|
|
339
|
-
hint: HintBox | None = None,
|
|
340
|
-
rect: Rect | None = None,
|
|
341
|
-
lang: OcrLanguage | None = None,
|
|
342
|
-
) -> list[OcrResult | None]:
|
|
343
|
-
engine = self._get_engine(lang)
|
|
344
|
-
return engine.find_all(
|
|
345
|
-
ContextStackVars.ensure_current().screenshot,
|
|
346
|
-
list(patterns),
|
|
347
|
-
hint=hint,
|
|
348
|
-
rect=rect,
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
def expect(
|
|
352
|
-
self,
|
|
353
|
-
pattern: str | re.Pattern | StringMatchFunction,
|
|
354
|
-
*,
|
|
355
|
-
rect: Rect | None = None,
|
|
356
|
-
hint: HintBox | None = None,
|
|
357
|
-
lang: OcrLanguage | None = None,
|
|
358
|
-
) -> OcrResult:
|
|
359
|
-
|
|
360
|
-
"""
|
|
361
|
-
检查当前设备画面是否包含指定文本。
|
|
362
|
-
|
|
363
|
-
与 `find()` 的区别在于,`expect()` 未找到时会抛出异常。
|
|
364
|
-
"""
|
|
365
|
-
engine = self._get_engine(lang)
|
|
366
|
-
ret = engine.expect(ContextStackVars.ensure_current().screenshot, pattern, rect=rect, hint=hint)
|
|
367
|
-
self.context.device.last_find = ret.original_rect if ret else None
|
|
368
|
-
return ret
|
|
369
|
-
|
|
370
|
-
def expect_wait(
|
|
371
|
-
self,
|
|
372
|
-
pattern: str | re.Pattern | StringMatchFunction,
|
|
373
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
374
|
-
*,
|
|
375
|
-
interval: float = DEFAULT_INTERVAL,
|
|
376
|
-
rect: Rect | None = None,
|
|
377
|
-
hint: HintBox | None = None,
|
|
378
|
-
) -> OcrResult:
|
|
379
|
-
"""
|
|
380
|
-
等待指定文本出现。
|
|
381
|
-
"""
|
|
382
|
-
is_manual = is_manual_screenshot_mode()
|
|
383
|
-
|
|
384
|
-
start_time = time.time()
|
|
385
|
-
while True:
|
|
386
|
-
if is_manual:
|
|
387
|
-
device.screenshot()
|
|
388
|
-
result = self.find(pattern, rect=rect, hint=hint)
|
|
389
|
-
|
|
390
|
-
if result is not None:
|
|
391
|
-
self.context.device.last_find = result.original_rect if result else None
|
|
392
|
-
return result
|
|
393
|
-
if time.time() - start_time > timeout:
|
|
394
|
-
raise TimeoutError(f"Timeout waiting for {pattern}")
|
|
395
|
-
sleep(interval)
|
|
396
|
-
|
|
397
|
-
def wait_for(
|
|
398
|
-
self,
|
|
399
|
-
pattern: str | re.Pattern | StringMatchFunction,
|
|
400
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
401
|
-
*,
|
|
402
|
-
interval: float = DEFAULT_INTERVAL,
|
|
403
|
-
rect: Rect | None = None,
|
|
404
|
-
hint: HintBox | None = None,
|
|
405
|
-
) -> OcrResult | None:
|
|
406
|
-
"""
|
|
407
|
-
等待指定文本出现。
|
|
408
|
-
"""
|
|
409
|
-
is_manual = is_manual_screenshot_mode()
|
|
410
|
-
|
|
411
|
-
start_time = time.time()
|
|
412
|
-
while True:
|
|
413
|
-
if is_manual:
|
|
414
|
-
device.screenshot()
|
|
415
|
-
result = self.find(pattern, rect=rect, hint=hint)
|
|
416
|
-
if result is not None:
|
|
417
|
-
self.context.device.last_find = result.original_rect if result else None
|
|
418
|
-
return result
|
|
419
|
-
if time.time() - start_time > timeout:
|
|
420
|
-
return None
|
|
421
|
-
sleep(interval)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
@interruptible_class
|
|
425
|
-
class ContextImage:
|
|
426
|
-
def __init__(self, context: 'Context', crop_rect: Rect | None = None):
|
|
427
|
-
self.context = context
|
|
428
|
-
self.crop_rect = crop_rect
|
|
429
|
-
|
|
430
|
-
def raw(self):
|
|
431
|
-
return raw_image
|
|
432
|
-
|
|
433
|
-
def wait_for(
|
|
434
|
-
self,
|
|
435
|
-
template: MatLike | str | Image,
|
|
436
|
-
mask: MatLike | str | None = None,
|
|
437
|
-
threshold: float = 0.8,
|
|
438
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
439
|
-
colored: bool = False,
|
|
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
|
-
return
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
ret
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return
|
|
614
|
-
|
|
615
|
-
@context(
|
|
616
|
-
def
|
|
617
|
-
return
|
|
618
|
-
|
|
619
|
-
@
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
def
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
def
|
|
633
|
-
return
|
|
634
|
-
|
|
635
|
-
@
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
class
|
|
645
|
-
def __init__(self, context: 'Context'
|
|
646
|
-
self.
|
|
647
|
-
self.
|
|
648
|
-
self.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
self.root.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return self.
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
def
|
|
690
|
-
"""
|
|
691
|
-
self.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
"""
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
return
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
self
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
self
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if
|
|
848
|
-
self.
|
|
849
|
-
if
|
|
850
|
-
self.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
@property
|
|
861
|
-
def
|
|
862
|
-
return self.
|
|
863
|
-
|
|
864
|
-
@property
|
|
865
|
-
def
|
|
866
|
-
return self.
|
|
867
|
-
|
|
868
|
-
@property
|
|
869
|
-
def
|
|
870
|
-
return self.
|
|
871
|
-
|
|
872
|
-
@property
|
|
873
|
-
def
|
|
874
|
-
return self.
|
|
875
|
-
|
|
876
|
-
@property
|
|
877
|
-
def
|
|
878
|
-
return self.
|
|
879
|
-
|
|
880
|
-
@
|
|
881
|
-
def
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
"""
|
|
918
|
-
|
|
919
|
-
"""
|
|
920
|
-
|
|
921
|
-
"""
|
|
922
|
-
|
|
923
|
-
"""
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
:
|
|
939
|
-
:
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
import warnings
|
|
6
|
+
import threading
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from threading import Event
|
|
9
|
+
from typing import (
|
|
10
|
+
Callable,
|
|
11
|
+
Optional,
|
|
12
|
+
cast,
|
|
13
|
+
overload,
|
|
14
|
+
Any,
|
|
15
|
+
TypeVar,
|
|
16
|
+
Literal,
|
|
17
|
+
ParamSpec,
|
|
18
|
+
Concatenate,
|
|
19
|
+
Generic,
|
|
20
|
+
Type,
|
|
21
|
+
Sequence,
|
|
22
|
+
)
|
|
23
|
+
from typing_extensions import deprecated
|
|
24
|
+
|
|
25
|
+
import cv2
|
|
26
|
+
from cv2.typing import MatLike
|
|
27
|
+
|
|
28
|
+
from kotonebot.client.device import Device, AndroidDevice, WindowsDevice
|
|
29
|
+
from kotonebot.backend.flow_controller import FlowController
|
|
30
|
+
from kotonebot.util import Interval
|
|
31
|
+
import kotonebot.backend.image as raw_image
|
|
32
|
+
from kotonebot.backend.image import (
|
|
33
|
+
TemplateMatchResult,
|
|
34
|
+
MultipleTemplateMatchResult,
|
|
35
|
+
find_all_crop,
|
|
36
|
+
expect,
|
|
37
|
+
find as image_find,
|
|
38
|
+
find_multi as image_find_multi,
|
|
39
|
+
find_all as image_find_all,
|
|
40
|
+
find_all_multi as image_find_all_multi,
|
|
41
|
+
count as image_count
|
|
42
|
+
)
|
|
43
|
+
import kotonebot.backend.color as raw_color
|
|
44
|
+
from kotonebot.backend.color import (
|
|
45
|
+
find as color_find, find_all as color_find_all
|
|
46
|
+
)
|
|
47
|
+
from kotonebot.backend.ocr import (
|
|
48
|
+
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
|
49
|
+
)
|
|
50
|
+
from kotonebot.config.manager import load_config, save_config
|
|
51
|
+
from kotonebot.config.base_config import UserConfig
|
|
52
|
+
from kotonebot.backend.core import Image, HintBox
|
|
53
|
+
from kotonebot.errors import ContextNotInitializedError, KotonebotWarning
|
|
54
|
+
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
|
55
|
+
from kotonebot.primitives import Rect
|
|
56
|
+
|
|
57
|
+
OcrLanguage = Literal['jp', 'en']
|
|
58
|
+
ScreenshotMode = Literal['auto', 'manual', 'manual-inherit']
|
|
59
|
+
DEFAULT_TIMEOUT = 120
|
|
60
|
+
DEFAULT_INTERVAL = 0.4
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
# https://stackoverflow.com/questions/74714300/paramspec-for-a-pre-defined-function-without-using-generic-callablep
|
|
64
|
+
T = TypeVar('T')
|
|
65
|
+
P = ParamSpec('P')
|
|
66
|
+
ContextClass = TypeVar("ContextClass")
|
|
67
|
+
|
|
68
|
+
def context(
|
|
69
|
+
_: Callable[Concatenate[MatLike, P], T] # 输入函数
|
|
70
|
+
) -> Callable[
|
|
71
|
+
[Callable[Concatenate[ContextClass, P], T]], # 被装饰函数
|
|
72
|
+
Callable[Concatenate[ContextClass, P], T] # 结果函数
|
|
73
|
+
]:
|
|
74
|
+
"""
|
|
75
|
+
用于标记 Context 类方法的装饰器。
|
|
76
|
+
此装饰器仅用于辅助类型标注,运行时无实际功能。
|
|
77
|
+
|
|
78
|
+
装饰器输入的函数类型为 `(img: MatLike, a, b, c, ...) -> T`,
|
|
79
|
+
被装饰的函数类型为 `(self: ContextClass, *args, **kwargs) -> T`,
|
|
80
|
+
结果类型为 `(self: ContextClass, a, b, c, ...) -> T`。
|
|
81
|
+
|
|
82
|
+
也就是说,`@context` 会把输入函数的第一个参数 `img: MatLike` 删除,
|
|
83
|
+
然后再添加 `self` 作为第一个参数。
|
|
84
|
+
|
|
85
|
+
【例】
|
|
86
|
+
```python
|
|
87
|
+
def find_image(
|
|
88
|
+
img: MatLike,
|
|
89
|
+
mask: MatLike,
|
|
90
|
+
threshold: float = 0.9
|
|
91
|
+
) -> TemplateMatchResult | None:
|
|
92
|
+
...
|
|
93
|
+
```
|
|
94
|
+
```python
|
|
95
|
+
class ContextImage:
|
|
96
|
+
@context(find_image)
|
|
97
|
+
def find_image(self, *args, **kwargs):
|
|
98
|
+
return find_image(
|
|
99
|
+
self.context.device.screenshot(),
|
|
100
|
+
*args,
|
|
101
|
+
**kwargs
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
```python
|
|
106
|
+
|
|
107
|
+
c = ContextImage()
|
|
108
|
+
c.find_image()
|
|
109
|
+
# 此函数类型推断为 (
|
|
110
|
+
# self: ContextImage,
|
|
111
|
+
# img: MatLike,
|
|
112
|
+
# mask: MatLike,
|
|
113
|
+
# threshold: float = 0.9
|
|
114
|
+
# ) -> TemplateMatchResult | None
|
|
115
|
+
```
|
|
116
|
+
"""
|
|
117
|
+
def _decorator(func):
|
|
118
|
+
return func
|
|
119
|
+
return _decorator
|
|
120
|
+
|
|
121
|
+
def interruptible(func: Callable[P, T]) -> Callable[P, T]:
|
|
122
|
+
"""
|
|
123
|
+
将函数包装为可中断函数。
|
|
124
|
+
|
|
125
|
+
在调用函数前,自动检查用户是否请求中断。
|
|
126
|
+
如果用户请求中断,则抛出 `KeyboardInterrupt` 异常。
|
|
127
|
+
"""
|
|
128
|
+
def _decorator(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
129
|
+
global vars
|
|
130
|
+
vars.flow.check()
|
|
131
|
+
return func(*args, **kwargs)
|
|
132
|
+
return _decorator
|
|
133
|
+
|
|
134
|
+
def interruptible_class(cls: Type[T]) -> Type[T]:
|
|
135
|
+
"""
|
|
136
|
+
将类中的所有方法包装为可中断方法。
|
|
137
|
+
|
|
138
|
+
在调用方法前,自动检查用户是否请求中断。
|
|
139
|
+
如果用户请求中断,则抛出 `KeyboardInterrupt` 异常。
|
|
140
|
+
"""
|
|
141
|
+
for name, func in cls.__dict__.items():
|
|
142
|
+
if callable(func) and not name.startswith('__'):
|
|
143
|
+
setattr(cls, name, interruptible(func))
|
|
144
|
+
return cls
|
|
145
|
+
|
|
146
|
+
def sleep(seconds: float, /):
|
|
147
|
+
"""
|
|
148
|
+
可中断和可暂停的 sleep 函数。
|
|
149
|
+
|
|
150
|
+
建议使用本函数代替 `time.sleep()`,
|
|
151
|
+
这样能以最快速度响应用户请求中断和暂停。
|
|
152
|
+
"""
|
|
153
|
+
global vars
|
|
154
|
+
vars.flow.sleep(seconds)
|
|
155
|
+
|
|
156
|
+
def warn_manual_screenshot_mode(name: str, alternative: str):
|
|
157
|
+
"""
|
|
158
|
+
警告在手动截图模式下使用的方法。
|
|
159
|
+
"""
|
|
160
|
+
warnings.warn(
|
|
161
|
+
f"You are calling `{name}` function in manual screenshot mode. "
|
|
162
|
+
f"This is meaningless. Write you own while loop and call `{alternative}` in the loop.",
|
|
163
|
+
KotonebotWarning
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def is_manual_screenshot_mode() -> bool:
|
|
167
|
+
"""
|
|
168
|
+
检查当前是否处于手动截图模式。
|
|
169
|
+
"""
|
|
170
|
+
mode = ContextStackVars.ensure_current().screenshot_mode
|
|
171
|
+
return mode == 'manual' or mode == 'manual-inherit'
|
|
172
|
+
|
|
173
|
+
class ContextGlobalVars:
|
|
174
|
+
def __init__(self):
|
|
175
|
+
self.__vars = dict[str, Any]()
|
|
176
|
+
self.flow: FlowController = FlowController()
|
|
177
|
+
"""流程控制器,负责停止、暂停、恢复等操作"""
|
|
178
|
+
|
|
179
|
+
def __getitem__(self, key: str) -> Any:
|
|
180
|
+
return self.__vars[key]
|
|
181
|
+
|
|
182
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
183
|
+
self.__vars[key] = value
|
|
184
|
+
|
|
185
|
+
def __delitem__(self, key: str) -> None:
|
|
186
|
+
del self.__vars[key]
|
|
187
|
+
|
|
188
|
+
def __contains__(self, key: str) -> bool:
|
|
189
|
+
return key in self.__vars
|
|
190
|
+
|
|
191
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
192
|
+
return self.__vars.get(key, default)
|
|
193
|
+
|
|
194
|
+
def set(self, key: str, value: Any) -> None:
|
|
195
|
+
self.__vars[key] = value
|
|
196
|
+
|
|
197
|
+
def clear(self):
|
|
198
|
+
self.__vars.clear()
|
|
199
|
+
self.flow.reset() # 重置流程控制器
|
|
200
|
+
|
|
201
|
+
def check_flow_control():
|
|
202
|
+
"""
|
|
203
|
+
统一的流程控制检查函数。
|
|
204
|
+
|
|
205
|
+
检查用户是否请求中断或暂停,如果是则相应处理:
|
|
206
|
+
- 如果请求中断,抛出 KeyboardInterrupt 异常
|
|
207
|
+
- 如果请求暂停,等待直到恢复
|
|
208
|
+
"""
|
|
209
|
+
vars.flow.check()
|
|
210
|
+
|
|
211
|
+
class ContextStackVars:
|
|
212
|
+
stack: list['ContextStackVars'] = []
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self.screenshot_mode: ScreenshotMode = 'auto'
|
|
216
|
+
"""
|
|
217
|
+
截图模式。
|
|
218
|
+
|
|
219
|
+
* `auto`
|
|
220
|
+
自动截图。即调用 `color`、`image`、`ocr` 上的方法时,会自动更新截图。
|
|
221
|
+
* `manual`
|
|
222
|
+
完全手动截图,不自动截图。如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
223
|
+
* `manual-inherit`:
|
|
224
|
+
第一张截图继承自调用者,此后同手动截图。
|
|
225
|
+
如果调用者没有截图数据,则继承的截图数据为空。
|
|
226
|
+
如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
227
|
+
"""
|
|
228
|
+
self._screenshot: MatLike | None = None
|
|
229
|
+
"""截图数据"""
|
|
230
|
+
self._inherit_screenshot: MatLike | None = None
|
|
231
|
+
"""继承的截图数据"""
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def screenshot(self) -> MatLike:
|
|
235
|
+
match self.screenshot_mode:
|
|
236
|
+
case 'manual':
|
|
237
|
+
if self._screenshot is None:
|
|
238
|
+
raise ValueError("No screenshot data found.")
|
|
239
|
+
return self._screenshot
|
|
240
|
+
case 'manual-inherit':
|
|
241
|
+
# TODO: 这一部分要考虑和 device.screenshot() 合并
|
|
242
|
+
if self._inherit_screenshot is not None:
|
|
243
|
+
self._screenshot = self._inherit_screenshot
|
|
244
|
+
self._inherit_screenshot = None
|
|
245
|
+
if self._screenshot is None:
|
|
246
|
+
raise ValueError("No screenshot data found.")
|
|
247
|
+
return self._screenshot
|
|
248
|
+
case 'auto':
|
|
249
|
+
self._screenshot = device.screenshot()
|
|
250
|
+
return self._screenshot
|
|
251
|
+
case _:
|
|
252
|
+
raise ValueError(f"Invalid screenshot mode: {self.screenshot_mode}")
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def push(*, screenshot_mode: ScreenshotMode | None = None) -> 'ContextStackVars':
|
|
256
|
+
vars = ContextStackVars()
|
|
257
|
+
if screenshot_mode is not None:
|
|
258
|
+
vars.screenshot_mode = screenshot_mode
|
|
259
|
+
current = ContextStackVars.current()
|
|
260
|
+
if current and vars.screenshot_mode == 'manual-inherit':
|
|
261
|
+
vars._inherit_screenshot = current._screenshot
|
|
262
|
+
ContextStackVars.stack.append(vars)
|
|
263
|
+
return vars
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def pop() -> 'ContextStackVars':
|
|
267
|
+
last = ContextStackVars.stack.pop()
|
|
268
|
+
return last
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def current() -> 'ContextStackVars | None':
|
|
272
|
+
if len(ContextStackVars.stack) == 0:
|
|
273
|
+
return None
|
|
274
|
+
return ContextStackVars.stack[-1]
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def ensure_current() -> 'ContextStackVars':
|
|
278
|
+
if len(ContextStackVars.stack) == 0:
|
|
279
|
+
raise ValueError("No context stack found.")
|
|
280
|
+
return ContextStackVars.stack[-1]
|
|
281
|
+
|
|
282
|
+
@interruptible_class
|
|
283
|
+
class ContextOcr:
|
|
284
|
+
def __init__(self, context: 'Context'):
|
|
285
|
+
self.context = context
|
|
286
|
+
self.__engine = jp()
|
|
287
|
+
|
|
288
|
+
def _get_engine(self, lang: OcrLanguage | None = None) -> Ocr:
|
|
289
|
+
"""获取指定语言的OCR引擎,如果lang为None则使用默认引擎。"""
|
|
290
|
+
return self.__engine if lang is None else self.raw(lang)
|
|
291
|
+
|
|
292
|
+
def raw(self, lang: OcrLanguage | None = None) -> Ocr:
|
|
293
|
+
"""
|
|
294
|
+
返回 `kotonebot.backend.ocr` 中的 Ocr 对象。\n
|
|
295
|
+
Ocr 对象与此对象(ContextOcr)的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。
|
|
296
|
+
"""
|
|
297
|
+
if lang is None:
|
|
298
|
+
lang = 'jp'
|
|
299
|
+
match lang:
|
|
300
|
+
case 'jp':
|
|
301
|
+
return jp()
|
|
302
|
+
case 'en':
|
|
303
|
+
return en()
|
|
304
|
+
case _:
|
|
305
|
+
raise ValueError(f"Invalid language: {lang}")
|
|
306
|
+
|
|
307
|
+
def ocr(
|
|
308
|
+
self,
|
|
309
|
+
rect: Rect | None = None,
|
|
310
|
+
lang: OcrLanguage | None = None,
|
|
311
|
+
) -> OcrResultList:
|
|
312
|
+
"""OCR 当前设备画面或指定图像。"""
|
|
313
|
+
engine = self._get_engine(lang)
|
|
314
|
+
return engine.ocr(ContextStackVars.ensure_current().screenshot, rect=rect)
|
|
315
|
+
|
|
316
|
+
def find(
|
|
317
|
+
self,
|
|
318
|
+
pattern: str | re.Pattern | StringMatchFunction,
|
|
319
|
+
*,
|
|
320
|
+
hint: HintBox | None = None,
|
|
321
|
+
rect: Rect | None = None,
|
|
322
|
+
lang: OcrLanguage | None = None,
|
|
323
|
+
) -> OcrResult | None:
|
|
324
|
+
"""检查当前设备画面是否包含指定文本。"""
|
|
325
|
+
engine = self._get_engine(lang)
|
|
326
|
+
ret = engine.find(
|
|
327
|
+
ContextStackVars.ensure_current().screenshot,
|
|
328
|
+
pattern,
|
|
329
|
+
hint=hint,
|
|
330
|
+
rect=rect,
|
|
331
|
+
)
|
|
332
|
+
self.context.device.last_find = ret.original_rect if ret else None
|
|
333
|
+
return ret
|
|
334
|
+
|
|
335
|
+
def find_all(
|
|
336
|
+
self,
|
|
337
|
+
patterns: Sequence[str | re.Pattern | StringMatchFunction],
|
|
338
|
+
*,
|
|
339
|
+
hint: HintBox | None = None,
|
|
340
|
+
rect: Rect | None = None,
|
|
341
|
+
lang: OcrLanguage | None = None,
|
|
342
|
+
) -> list[OcrResult | None]:
|
|
343
|
+
engine = self._get_engine(lang)
|
|
344
|
+
return engine.find_all(
|
|
345
|
+
ContextStackVars.ensure_current().screenshot,
|
|
346
|
+
list(patterns),
|
|
347
|
+
hint=hint,
|
|
348
|
+
rect=rect,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def expect(
|
|
352
|
+
self,
|
|
353
|
+
pattern: str | re.Pattern | StringMatchFunction,
|
|
354
|
+
*,
|
|
355
|
+
rect: Rect | None = None,
|
|
356
|
+
hint: HintBox | None = None,
|
|
357
|
+
lang: OcrLanguage | None = None,
|
|
358
|
+
) -> OcrResult:
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
检查当前设备画面是否包含指定文本。
|
|
362
|
+
|
|
363
|
+
与 `find()` 的区别在于,`expect()` 未找到时会抛出异常。
|
|
364
|
+
"""
|
|
365
|
+
engine = self._get_engine(lang)
|
|
366
|
+
ret = engine.expect(ContextStackVars.ensure_current().screenshot, pattern, rect=rect, hint=hint)
|
|
367
|
+
self.context.device.last_find = ret.original_rect if ret else None
|
|
368
|
+
return ret
|
|
369
|
+
|
|
370
|
+
def expect_wait(
|
|
371
|
+
self,
|
|
372
|
+
pattern: str | re.Pattern | StringMatchFunction,
|
|
373
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
374
|
+
*,
|
|
375
|
+
interval: float = DEFAULT_INTERVAL,
|
|
376
|
+
rect: Rect | None = None,
|
|
377
|
+
hint: HintBox | None = None,
|
|
378
|
+
) -> OcrResult:
|
|
379
|
+
"""
|
|
380
|
+
等待指定文本出现。
|
|
381
|
+
"""
|
|
382
|
+
is_manual = is_manual_screenshot_mode()
|
|
383
|
+
|
|
384
|
+
start_time = time.time()
|
|
385
|
+
while True:
|
|
386
|
+
if is_manual:
|
|
387
|
+
device.screenshot()
|
|
388
|
+
result = self.find(pattern, rect=rect, hint=hint)
|
|
389
|
+
|
|
390
|
+
if result is not None:
|
|
391
|
+
self.context.device.last_find = result.original_rect if result else None
|
|
392
|
+
return result
|
|
393
|
+
if time.time() - start_time > timeout:
|
|
394
|
+
raise TimeoutError(f"Timeout waiting for {pattern}")
|
|
395
|
+
sleep(interval)
|
|
396
|
+
|
|
397
|
+
def wait_for(
|
|
398
|
+
self,
|
|
399
|
+
pattern: str | re.Pattern | StringMatchFunction,
|
|
400
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
401
|
+
*,
|
|
402
|
+
interval: float = DEFAULT_INTERVAL,
|
|
403
|
+
rect: Rect | None = None,
|
|
404
|
+
hint: HintBox | None = None,
|
|
405
|
+
) -> OcrResult | None:
|
|
406
|
+
"""
|
|
407
|
+
等待指定文本出现。
|
|
408
|
+
"""
|
|
409
|
+
is_manual = is_manual_screenshot_mode()
|
|
410
|
+
|
|
411
|
+
start_time = time.time()
|
|
412
|
+
while True:
|
|
413
|
+
if is_manual:
|
|
414
|
+
device.screenshot()
|
|
415
|
+
result = self.find(pattern, rect=rect, hint=hint)
|
|
416
|
+
if result is not None:
|
|
417
|
+
self.context.device.last_find = result.original_rect if result else None
|
|
418
|
+
return result
|
|
419
|
+
if time.time() - start_time > timeout:
|
|
420
|
+
return None
|
|
421
|
+
sleep(interval)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@interruptible_class
|
|
425
|
+
class ContextImage:
|
|
426
|
+
def __init__(self, context: 'Context', crop_rect: Rect | None = None):
|
|
427
|
+
self.context = context
|
|
428
|
+
self.crop_rect = crop_rect
|
|
429
|
+
|
|
430
|
+
def raw(self):
|
|
431
|
+
return raw_image
|
|
432
|
+
|
|
433
|
+
def wait_for(
|
|
434
|
+
self,
|
|
435
|
+
template: MatLike | str | Image,
|
|
436
|
+
mask: MatLike | str | None = None,
|
|
437
|
+
threshold: float = 0.8,
|
|
438
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
439
|
+
colored: bool = False,
|
|
440
|
+
*,
|
|
441
|
+
rect: Rect | None = None,
|
|
442
|
+
transparent: bool = False,
|
|
443
|
+
interval: float = DEFAULT_INTERVAL,
|
|
444
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
445
|
+
) -> TemplateMatchResult | None:
|
|
446
|
+
"""
|
|
447
|
+
等待指定图像出现。
|
|
448
|
+
"""
|
|
449
|
+
is_manual = is_manual_screenshot_mode()
|
|
450
|
+
|
|
451
|
+
start_time = time.time()
|
|
452
|
+
while True:
|
|
453
|
+
if is_manual:
|
|
454
|
+
device.screenshot()
|
|
455
|
+
ret = self.find(
|
|
456
|
+
template,
|
|
457
|
+
mask,
|
|
458
|
+
rect=rect,
|
|
459
|
+
transparent=transparent,
|
|
460
|
+
threshold=threshold,
|
|
461
|
+
colored=colored,
|
|
462
|
+
preprocessors=preprocessors,
|
|
463
|
+
)
|
|
464
|
+
if ret is not None:
|
|
465
|
+
self.context.device.last_find = ret
|
|
466
|
+
return ret
|
|
467
|
+
if time.time() - start_time > timeout:
|
|
468
|
+
return None
|
|
469
|
+
sleep(interval)
|
|
470
|
+
|
|
471
|
+
def wait_for_any(
|
|
472
|
+
self,
|
|
473
|
+
templates: list[str | Image],
|
|
474
|
+
masks: list[str | None] | None = None,
|
|
475
|
+
threshold: float = 0.8,
|
|
476
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
477
|
+
colored: bool = False,
|
|
478
|
+
*,
|
|
479
|
+
rect: Rect | None = None,
|
|
480
|
+
transparent: bool = False,
|
|
481
|
+
interval: float = DEFAULT_INTERVAL,
|
|
482
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
483
|
+
):
|
|
484
|
+
"""
|
|
485
|
+
等待指定图像中的任意一个出现。
|
|
486
|
+
"""
|
|
487
|
+
is_manual = is_manual_screenshot_mode()
|
|
488
|
+
|
|
489
|
+
if masks is None:
|
|
490
|
+
_masks = [None] * len(templates)
|
|
491
|
+
else:
|
|
492
|
+
_masks = masks
|
|
493
|
+
start_time = time.time()
|
|
494
|
+
while True:
|
|
495
|
+
if is_manual:
|
|
496
|
+
device.screenshot()
|
|
497
|
+
for template, mask in zip(templates, _masks):
|
|
498
|
+
if self.find(
|
|
499
|
+
template,
|
|
500
|
+
mask,
|
|
501
|
+
rect=rect,
|
|
502
|
+
transparent=transparent,
|
|
503
|
+
threshold=threshold,
|
|
504
|
+
colored=colored,
|
|
505
|
+
preprocessors=preprocessors,
|
|
506
|
+
):
|
|
507
|
+
return True
|
|
508
|
+
if time.time() - start_time > timeout:
|
|
509
|
+
return False
|
|
510
|
+
sleep(interval)
|
|
511
|
+
|
|
512
|
+
def expect_wait(
|
|
513
|
+
self,
|
|
514
|
+
template: str | Image,
|
|
515
|
+
mask: str | None = None,
|
|
516
|
+
threshold: float = 0.8,
|
|
517
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
518
|
+
colored: bool = False,
|
|
519
|
+
*,
|
|
520
|
+
rect: Rect | None = None,
|
|
521
|
+
transparent: bool = False,
|
|
522
|
+
interval: float = DEFAULT_INTERVAL,
|
|
523
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
524
|
+
) -> TemplateMatchResult:
|
|
525
|
+
"""
|
|
526
|
+
等待指定图像出现。
|
|
527
|
+
"""
|
|
528
|
+
is_manual = is_manual_screenshot_mode()
|
|
529
|
+
|
|
530
|
+
start_time = time.time()
|
|
531
|
+
while True:
|
|
532
|
+
if is_manual:
|
|
533
|
+
device.screenshot()
|
|
534
|
+
ret = self.find(
|
|
535
|
+
template,
|
|
536
|
+
mask,
|
|
537
|
+
rect=rect,
|
|
538
|
+
transparent=transparent,
|
|
539
|
+
threshold=threshold,
|
|
540
|
+
colored=colored,
|
|
541
|
+
preprocessors=preprocessors,
|
|
542
|
+
)
|
|
543
|
+
if ret is not None:
|
|
544
|
+
self.context.device.last_find = ret
|
|
545
|
+
return ret
|
|
546
|
+
if time.time() - start_time > timeout:
|
|
547
|
+
raise TimeoutError(f"Timeout waiting for {template}")
|
|
548
|
+
sleep(interval)
|
|
549
|
+
|
|
550
|
+
def expect_wait_any(
|
|
551
|
+
self,
|
|
552
|
+
templates: list[str | Image],
|
|
553
|
+
masks: list[str | None] | None = None,
|
|
554
|
+
threshold: float = 0.8,
|
|
555
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
556
|
+
colored: bool = False,
|
|
557
|
+
*,
|
|
558
|
+
rect: Rect | None = None,
|
|
559
|
+
transparent: bool = False,
|
|
560
|
+
interval: float = DEFAULT_INTERVAL,
|
|
561
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
562
|
+
) -> TemplateMatchResult:
|
|
563
|
+
"""
|
|
564
|
+
等待指定图像中的任意一个出现。
|
|
565
|
+
"""
|
|
566
|
+
is_manual = is_manual_screenshot_mode()
|
|
567
|
+
|
|
568
|
+
if masks is None:
|
|
569
|
+
_masks = [None] * len(templates)
|
|
570
|
+
else:
|
|
571
|
+
_masks = masks
|
|
572
|
+
start_time = time.time()
|
|
573
|
+
while True:
|
|
574
|
+
if is_manual:
|
|
575
|
+
device.screenshot()
|
|
576
|
+
for template, mask in zip(templates, _masks):
|
|
577
|
+
ret = self.find(
|
|
578
|
+
template,
|
|
579
|
+
mask,
|
|
580
|
+
rect=rect,
|
|
581
|
+
transparent=transparent,
|
|
582
|
+
threshold=threshold,
|
|
583
|
+
colored=colored,
|
|
584
|
+
preprocessors=preprocessors,
|
|
585
|
+
)
|
|
586
|
+
if ret is not None:
|
|
587
|
+
self.context.device.last_find = ret
|
|
588
|
+
return ret
|
|
589
|
+
if time.time() - start_time > timeout:
|
|
590
|
+
raise TimeoutError(f"Timeout waiting for any of {templates}")
|
|
591
|
+
sleep(interval)
|
|
592
|
+
|
|
593
|
+
@context(expect)
|
|
594
|
+
def expect(self, *args, **kwargs):
|
|
595
|
+
ret = expect(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
596
|
+
self.context.device.last_find = ret
|
|
597
|
+
return ret
|
|
598
|
+
|
|
599
|
+
@context(image_find)
|
|
600
|
+
def find(self, *args, **kwargs):
|
|
601
|
+
ret = image_find(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
602
|
+
self.context.device.last_find = ret
|
|
603
|
+
return ret
|
|
604
|
+
|
|
605
|
+
@context(image_find_all)
|
|
606
|
+
def find_all(self, *args, **kwargs):
|
|
607
|
+
return image_find_all(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
608
|
+
|
|
609
|
+
@context(image_find_multi)
|
|
610
|
+
def find_multi(self, *args, **kwargs):
|
|
611
|
+
ret = image_find_multi(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
612
|
+
self.context.device.last_find = ret
|
|
613
|
+
return ret
|
|
614
|
+
|
|
615
|
+
@context(image_find_all_multi)
|
|
616
|
+
def find_all_multi(self, *args, **kwargs):
|
|
617
|
+
return image_find_all_multi(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
618
|
+
|
|
619
|
+
@context(find_all_crop)
|
|
620
|
+
def find_all_crop(self, *args, **kwargs):
|
|
621
|
+
return find_all_crop(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
622
|
+
|
|
623
|
+
@context(image_count)
|
|
624
|
+
def count(self, *args, **kwargs):
|
|
625
|
+
return image_count(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
626
|
+
|
|
627
|
+
@interruptible_class
|
|
628
|
+
class ContextColor:
|
|
629
|
+
def __init__(self, context: 'Context'):
|
|
630
|
+
self.context = context
|
|
631
|
+
|
|
632
|
+
def raw(self):
|
|
633
|
+
return raw_color
|
|
634
|
+
|
|
635
|
+
@context(color_find)
|
|
636
|
+
def find(self, *args, **kwargs):
|
|
637
|
+
return color_find(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
638
|
+
|
|
639
|
+
@context(color_find_all)
|
|
640
|
+
def find_all(self, *args, **kwargs):
|
|
641
|
+
return color_find_all(ContextStackVars.ensure_current().screenshot, *args, **kwargs)
|
|
642
|
+
|
|
643
|
+
@deprecated('使用 kotonebot.backend.debug 模块替代')
|
|
644
|
+
class ContextDebug:
|
|
645
|
+
def __init__(self, context: 'Context'):
|
|
646
|
+
self.__context = context
|
|
647
|
+
self.save_images: bool = False
|
|
648
|
+
self.save_images_dir: str = "debug_images"
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
V = TypeVar('V')
|
|
652
|
+
class ContextConfig(Generic[T]):
|
|
653
|
+
def __init__(self, context: 'Context', config_path: str = 'config.json', config_type: Type[T] = dict[str, Any]):
|
|
654
|
+
self.context = context
|
|
655
|
+
self.config_path: str = config_path
|
|
656
|
+
self.current_key: int | str = 0
|
|
657
|
+
self.config_type: Type = config_type
|
|
658
|
+
self.root = load_config(self.config_path, type=config_type)
|
|
659
|
+
|
|
660
|
+
def to(self, conf_type: Type[V]) -> 'ContextConfig[V]':
|
|
661
|
+
self.config_type = conf_type
|
|
662
|
+
return cast(ContextConfig[V], self)
|
|
663
|
+
|
|
664
|
+
def create(self, config: UserConfig[T]):
|
|
665
|
+
"""创建新用户配置"""
|
|
666
|
+
self.root.user_configs.append(config)
|
|
667
|
+
self.save()
|
|
668
|
+
|
|
669
|
+
def get(self, key: str | int | None = None) -> UserConfig[T] | None:
|
|
670
|
+
"""
|
|
671
|
+
获取指定或当前用户配置数据。
|
|
672
|
+
|
|
673
|
+
:param key: 用户配置 ID 或索引(从 0 开始),为 None 时获取当前用户配置
|
|
674
|
+
:return: 用户配置数据
|
|
675
|
+
"""
|
|
676
|
+
if isinstance(key, int):
|
|
677
|
+
if key < 0 or key >= len(self.root.user_configs):
|
|
678
|
+
return None
|
|
679
|
+
return self.root.user_configs[key]
|
|
680
|
+
elif isinstance(key, str):
|
|
681
|
+
for user in self.root.user_configs:
|
|
682
|
+
if user.id == key:
|
|
683
|
+
return user
|
|
684
|
+
else:
|
|
685
|
+
return None
|
|
686
|
+
else:
|
|
687
|
+
return self.get(self.current_key)
|
|
688
|
+
|
|
689
|
+
def save(self):
|
|
690
|
+
"""保存所有配置数据到本地"""
|
|
691
|
+
save_config(self.root, self.config_path)
|
|
692
|
+
|
|
693
|
+
def load(self):
|
|
694
|
+
"""从本地加载所有配置数据"""
|
|
695
|
+
self.root = load_config(self.config_path, type=self.config_type)
|
|
696
|
+
|
|
697
|
+
def switch(self, key: str | int):
|
|
698
|
+
"""切换到指定用户配置"""
|
|
699
|
+
self.current_key = key
|
|
700
|
+
|
|
701
|
+
@property
|
|
702
|
+
def current(self) -> UserConfig[T]:
|
|
703
|
+
"""
|
|
704
|
+
当前配置数据。
|
|
705
|
+
|
|
706
|
+
如果当前配置不存在,则使用默认值自动创建一个新配置。
|
|
707
|
+
(不推荐,建议在 UI 中启动前要求用户手动创建,或自行创建一个默认配置。)
|
|
708
|
+
"""
|
|
709
|
+
c = self.get(self.current_key)
|
|
710
|
+
if c is None:
|
|
711
|
+
if not self.config_type:
|
|
712
|
+
raise ValueError("No config type specified.")
|
|
713
|
+
logger.warning("No config found, creating a new one using default values. (NOT RECOMMENDED)")
|
|
714
|
+
c = self.config_type()
|
|
715
|
+
u = UserConfig(options=c)
|
|
716
|
+
self.create(u)
|
|
717
|
+
c = u
|
|
718
|
+
return c
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class Forwarded:
|
|
722
|
+
def __init__(self, getter: Callable[[], T] | None = None, name: str | None = None):
|
|
723
|
+
self._FORWARD_getter = getter
|
|
724
|
+
self._FORWARD_name = name
|
|
725
|
+
|
|
726
|
+
def __getattr__(self, name: str) -> Any:
|
|
727
|
+
if name.startswith('_FORWARD_'):
|
|
728
|
+
return object.__getattribute__(self, name)
|
|
729
|
+
if self._FORWARD_getter is None:
|
|
730
|
+
raise ContextNotInitializedError(f"Forwarded object {self._FORWARD_name} called before initialization.")
|
|
731
|
+
return getattr(self._FORWARD_getter(), name)
|
|
732
|
+
|
|
733
|
+
def __setattr__(self, name: str, value: Any):
|
|
734
|
+
if name.startswith('_FORWARD_'):
|
|
735
|
+
return object.__setattr__(self, name, value)
|
|
736
|
+
if self._FORWARD_getter is None:
|
|
737
|
+
raise ContextNotInitializedError(f"Forwarded object {self._FORWARD_name} called before initialization.")
|
|
738
|
+
setattr(self._FORWARD_getter(), name, value)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
T_Device = TypeVar('T_Device', bound=Device)
|
|
742
|
+
class ContextDevice(Generic[T_Device], Device):
|
|
743
|
+
def __init__(self, device: T_Device, target_screenshot_interval: float | None = None):
|
|
744
|
+
"""
|
|
745
|
+
:param device: 目标设备。
|
|
746
|
+
:param target_screenshot_interval: 见 `ContextDevice.target_screenshot_interval`。
|
|
747
|
+
"""
|
|
748
|
+
self._device = device
|
|
749
|
+
self.target_screenshot_interval: float | None = target_screenshot_interval
|
|
750
|
+
"""
|
|
751
|
+
目标截图间隔,可用于限制截图速度。若两次截图实际间隔小于该值,则会自动等待。
|
|
752
|
+
为 None 时不限制截图速度。
|
|
753
|
+
"""
|
|
754
|
+
self._screenshot_interval: Interval | None = None
|
|
755
|
+
if self.target_screenshot_interval is not None:
|
|
756
|
+
self._screenshot_interval = Interval(self.target_screenshot_interval)
|
|
757
|
+
|
|
758
|
+
def screenshot(self, *, force: bool = False):
|
|
759
|
+
"""
|
|
760
|
+
截图。返回截图数据,同时更新当前上下文的截图数据。
|
|
761
|
+
"""
|
|
762
|
+
check_flow_control()
|
|
763
|
+
global next_wait, last_screenshot_time, next_wait_time
|
|
764
|
+
current = ContextStackVars.ensure_current()
|
|
765
|
+
if force:
|
|
766
|
+
current._inherit_screenshot = None
|
|
767
|
+
if current._inherit_screenshot is not None:
|
|
768
|
+
img = current._inherit_screenshot
|
|
769
|
+
current._inherit_screenshot = None
|
|
770
|
+
else:
|
|
771
|
+
if self._screenshot_interval is not None:
|
|
772
|
+
self._screenshot_interval.wait()
|
|
773
|
+
|
|
774
|
+
if next_wait == 'screenshot':
|
|
775
|
+
delta = time.time() - last_screenshot_time
|
|
776
|
+
if delta < next_wait_time:
|
|
777
|
+
sleep(next_wait_time - delta)
|
|
778
|
+
last_screenshot_time = time.time()
|
|
779
|
+
next_wait_time = 0
|
|
780
|
+
next_wait = None
|
|
781
|
+
img = self._device.screenshot()
|
|
782
|
+
current._screenshot = img
|
|
783
|
+
return img
|
|
784
|
+
|
|
785
|
+
def __getattribute__(self, name: str):
|
|
786
|
+
if name in ['_device', 'screenshot', 'of_android', 'of_windows']:
|
|
787
|
+
return object.__getattribute__(self, name)
|
|
788
|
+
else:
|
|
789
|
+
return getattr(self._device, name)
|
|
790
|
+
|
|
791
|
+
def __setattr__(self, name: str, value: Any):
|
|
792
|
+
if name in ['_device', 'screenshot', 'of_android', 'of_windows']:
|
|
793
|
+
return object.__setattr__(self, name, value)
|
|
794
|
+
else:
|
|
795
|
+
return setattr(self._device, name, value)
|
|
796
|
+
|
|
797
|
+
def of_android(self) -> 'ContextDevice | AndroidDevice':
|
|
798
|
+
"""
|
|
799
|
+
确保此 ContextDevice 底层为 Android 平台。
|
|
800
|
+
同时通过返回的对象可以调用 Android 平台特有的方法。
|
|
801
|
+
"""
|
|
802
|
+
if not isinstance(self._device, AndroidDevice):
|
|
803
|
+
raise ValueError("Device is not AndroidDevice")
|
|
804
|
+
return self
|
|
805
|
+
|
|
806
|
+
def of_windows(self) -> 'ContextDevice | WindowsDevice':
|
|
807
|
+
"""
|
|
808
|
+
确保此 ContextDevice 底层为 Windows 平台。
|
|
809
|
+
同时通过返回的对象可以调用 Windows 平台特有的方法。
|
|
810
|
+
"""
|
|
811
|
+
if not isinstance(self._device, WindowsDevice):
|
|
812
|
+
raise ValueError("Device is not WindowsDevice")
|
|
813
|
+
return self
|
|
814
|
+
|
|
815
|
+
class Context(Generic[T]):
|
|
816
|
+
def __init__(
|
|
817
|
+
self,
|
|
818
|
+
config_path: str,
|
|
819
|
+
config_type: Type[T],
|
|
820
|
+
device: Device,
|
|
821
|
+
target_screenshot_interval: float | None = None
|
|
822
|
+
):
|
|
823
|
+
self.__ocr = ContextOcr(self)
|
|
824
|
+
self.__image = ContextImage(self)
|
|
825
|
+
self.__color = ContextColor(self)
|
|
826
|
+
self.__vars = ContextGlobalVars()
|
|
827
|
+
self.__debug = ContextDebug(self)
|
|
828
|
+
self.__config = ContextConfig[T](self, config_path, config_type)
|
|
829
|
+
self.__device = ContextDevice(device, target_screenshot_interval)
|
|
830
|
+
|
|
831
|
+
def inject(
|
|
832
|
+
self,
|
|
833
|
+
*,
|
|
834
|
+
device: Optional[ContextDevice | Device] = None,
|
|
835
|
+
ocr: Optional[ContextOcr] = None,
|
|
836
|
+
image: Optional[ContextImage] = None,
|
|
837
|
+
color: Optional[ContextColor] = None,
|
|
838
|
+
vars: Optional[ContextGlobalVars] = None,
|
|
839
|
+
debug: Optional[ContextDebug] = None,
|
|
840
|
+
config: Optional[ContextConfig] = None,
|
|
841
|
+
):
|
|
842
|
+
if device is not None:
|
|
843
|
+
if isinstance(device, Device):
|
|
844
|
+
self.__device = ContextDevice(device)
|
|
845
|
+
else:
|
|
846
|
+
self.__device = device
|
|
847
|
+
if ocr is not None:
|
|
848
|
+
self.__ocr = ocr
|
|
849
|
+
if image is not None:
|
|
850
|
+
self.__image = image
|
|
851
|
+
if color is not None:
|
|
852
|
+
self.__color = color
|
|
853
|
+
if vars is not None:
|
|
854
|
+
self.__vars = vars
|
|
855
|
+
if debug is not None:
|
|
856
|
+
self.__debug = debug
|
|
857
|
+
if config is not None:
|
|
858
|
+
self.__config = config
|
|
859
|
+
|
|
860
|
+
@property
|
|
861
|
+
def device(self) -> ContextDevice:
|
|
862
|
+
return self.__device
|
|
863
|
+
|
|
864
|
+
@property
|
|
865
|
+
def ocr(self) -> 'ContextOcr':
|
|
866
|
+
return self.__ocr
|
|
867
|
+
|
|
868
|
+
@property
|
|
869
|
+
def image(self) -> 'ContextImage':
|
|
870
|
+
return self.__image
|
|
871
|
+
|
|
872
|
+
@property
|
|
873
|
+
def color(self) -> 'ContextColor':
|
|
874
|
+
return self.__color
|
|
875
|
+
|
|
876
|
+
@property
|
|
877
|
+
def vars(self) -> 'ContextGlobalVars':
|
|
878
|
+
return self.__vars
|
|
879
|
+
|
|
880
|
+
@property
|
|
881
|
+
def debug(self) -> 'ContextDebug':
|
|
882
|
+
return self.__debug
|
|
883
|
+
|
|
884
|
+
@property
|
|
885
|
+
def config(self) -> 'ContextConfig[T]':
|
|
886
|
+
return self.__config
|
|
887
|
+
|
|
888
|
+
@deprecated('使用 Rect 类的实例方法代替')
|
|
889
|
+
def rect_expand(rect: Rect, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0) -> Rect:
|
|
890
|
+
"""
|
|
891
|
+
向四个方向扩展矩形区域。
|
|
892
|
+
"""
|
|
893
|
+
return Rect(rect.x1 - left, rect.y1 - top, rect.w + right + left, rect.h + bottom + top)
|
|
894
|
+
|
|
895
|
+
def use_screenshot(*args: MatLike | None) -> MatLike:
|
|
896
|
+
for img in args:
|
|
897
|
+
if img is not None:
|
|
898
|
+
ContextStackVars.ensure_current()._screenshot = img # HACK
|
|
899
|
+
return img
|
|
900
|
+
return device.screenshot()
|
|
901
|
+
|
|
902
|
+
WaitBeforeType = Literal['screenshot']
|
|
903
|
+
@deprecated('使用普通 sleep 代替')
|
|
904
|
+
def wait(at_least: float = 0.3, *, before: WaitBeforeType) -> None:
|
|
905
|
+
global next_wait, next_wait_time
|
|
906
|
+
if before == 'screenshot':
|
|
907
|
+
if time.time() - last_screenshot_time < at_least:
|
|
908
|
+
next_wait = 'screenshot'
|
|
909
|
+
next_wait_time = at_least
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
# 这里 Context 类还没有初始化,但是 tasks 中的脚本可能已经引用了这里的变量
|
|
913
|
+
# 为了能够动态更新这里变量的值,这里使用 Forwarded 类再封装一层,
|
|
914
|
+
# 将调用转发到实际的稍后初始化的 Context 类上
|
|
915
|
+
_c: Context | None = None
|
|
916
|
+
device: ContextDevice = cast(ContextDevice, Forwarded(name="device"))
|
|
917
|
+
"""当前正在执行任务的设备。"""
|
|
918
|
+
ocr: ContextOcr = cast(ContextOcr, Forwarded(name="ocr"))
|
|
919
|
+
"""OCR 引擎。"""
|
|
920
|
+
image: ContextImage = cast(ContextImage, Forwarded(name="image"))
|
|
921
|
+
"""图像识别。"""
|
|
922
|
+
color: ContextColor = cast(ContextColor, Forwarded(name="color"))
|
|
923
|
+
"""颜色识别。"""
|
|
924
|
+
vars: ContextGlobalVars = cast(ContextGlobalVars, Forwarded(name="vars"))
|
|
925
|
+
"""全局变量。"""
|
|
926
|
+
debug: ContextDebug = cast(ContextDebug, Forwarded(name="debug"))
|
|
927
|
+
"""调试工具。"""
|
|
928
|
+
config: ContextConfig = cast(ContextConfig, Forwarded(name="config"))
|
|
929
|
+
"""配置数据。"""
|
|
930
|
+
last_screenshot_time: float = -1
|
|
931
|
+
"""上一次截图的时间。"""
|
|
932
|
+
next_wait: WaitBeforeType | None = None
|
|
933
|
+
next_wait_time: float = 0
|
|
934
|
+
|
|
935
|
+
def init_context(
|
|
936
|
+
*,
|
|
937
|
+
config_path: str = 'config.json',
|
|
938
|
+
config_type: Type[T] = dict[str, Any],
|
|
939
|
+
force: bool = False,
|
|
940
|
+
target_device: Device,
|
|
941
|
+
target_screenshot_interval: float | None = None,
|
|
942
|
+
):
|
|
943
|
+
"""
|
|
944
|
+
初始化 Context 模块。
|
|
945
|
+
|
|
946
|
+
:param config_path: 配置文件路径。
|
|
947
|
+
:param config_type: 配置数据类类型。
|
|
948
|
+
配置数据类必须继承自 pydantic 的 `BaseModel`。
|
|
949
|
+
默认为 `dict[str, Any]`,即普通的 JSON 数据,不包含任何类型信息。
|
|
950
|
+
:param force: 是否强制重新初始化。
|
|
951
|
+
若为 `True`,则忽略已存在的 Context 实例,并重新创建一个新的实例。
|
|
952
|
+
:param target_device: 目标设备
|
|
953
|
+
:param target_screenshot_interval: 见 `ContextDevice.target_screenshot_interval`。
|
|
954
|
+
"""
|
|
955
|
+
global _c, device, ocr, image, color, vars, debug, config
|
|
956
|
+
if _c is not None and not force:
|
|
957
|
+
return
|
|
958
|
+
_c = Context(
|
|
959
|
+
config_path=config_path,
|
|
960
|
+
config_type=config_type,
|
|
961
|
+
device=target_device,
|
|
962
|
+
target_screenshot_interval=target_screenshot_interval,
|
|
963
|
+
)
|
|
964
|
+
device._FORWARD_getter = lambda: _c.device # type: ignore
|
|
965
|
+
ocr._FORWARD_getter = lambda: _c.ocr # type: ignore
|
|
966
|
+
image._FORWARD_getter = lambda: _c.image # type: ignore
|
|
967
|
+
color._FORWARD_getter = lambda: _c.color # type: ignore
|
|
968
|
+
vars._FORWARD_getter = lambda: _c.vars # type: ignore
|
|
969
|
+
debug._FORWARD_getter = lambda: _c.debug # type: ignore
|
|
970
|
+
config._FORWARD_getter = lambda: _c.config # type: ignore
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def inject_context(
|
|
974
|
+
*,
|
|
975
|
+
device: Optional[ContextDevice | Device] = None,
|
|
976
|
+
ocr: Optional[ContextOcr] = None,
|
|
977
|
+
image: Optional[ContextImage] = None,
|
|
978
|
+
color: Optional[ContextColor] = None,
|
|
979
|
+
vars: Optional[ContextGlobalVars] = None,
|
|
980
|
+
debug: Optional[ContextDebug] = None,
|
|
981
|
+
config: Optional[ContextConfig] = None,
|
|
982
|
+
):
|
|
983
|
+
global _c
|
|
984
|
+
if _c is None:
|
|
985
|
+
raise ContextNotInitializedError('Context not initialized')
|
|
986
|
+
_c.inject(device=device, ocr=ocr, image=image, color=color, vars=vars, debug=debug, config=config)
|
|
987
|
+
|
|
988
|
+
class ManualContextManager:
|
|
989
|
+
def __init__(self, screenshot_mode: ScreenshotMode = 'auto'):
|
|
990
|
+
self.screenshot_mode: ScreenshotMode = screenshot_mode
|
|
991
|
+
|
|
992
|
+
def __enter__(self):
|
|
993
|
+
ContextStackVars.push(screenshot_mode=self.screenshot_mode)
|
|
994
|
+
|
|
995
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
996
|
+
ContextStackVars.pop()
|
|
997
|
+
|
|
998
|
+
def begin(self):
|
|
999
|
+
self.__enter__()
|
|
1000
|
+
|
|
1001
|
+
def end(self):
|
|
1002
|
+
self.__exit__(None, None, None)
|
|
1003
|
+
|
|
1004
|
+
def manual_context(screenshot_mode: ScreenshotMode = 'auto') -> ManualContextManager:
|
|
1005
|
+
"""
|
|
1006
|
+
默认情况下,Context* 类仅允许在 @task/@action 函数中使用。
|
|
1007
|
+
如果想要在其他地方使用,使用此函数手动创建一个上下文。
|
|
1008
|
+
"""
|
|
1001
1009
|
return ManualContextManager(screenshot_mode)
|