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