kotonebot 0.3.1__py3-none-any.whl → 0.4.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/bot.py +22 -12
- kotonebot/backend/context/context.py +49 -56
- kotonebot/backend/context/task_action.py +16 -8
- kotonebot/backend/core.py +5 -2
- kotonebot/backend/loop.py +12 -88
- kotonebot/client/device.py +2 -1
- kotonebot/client/host/adb_common.py +12 -3
- kotonebot/client/host/leidian_host.py +1 -1
- kotonebot/client/host/windows_common.py +3 -0
- kotonebot/client/implements/__init__.py +12 -4
- kotonebot/client/implements/remote_windows.py +3 -7
- kotonebot/client/implements/windows.py +4 -0
- kotonebot/interop/win/__init__.py +3 -0
- kotonebot/interop/win/task_dialog.py +44 -0
- kotonebot/ui/user.py +6 -1
- kotonebot/util.py +27 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.4.0.dist-info}/METADATA +16 -11
- {kotonebot-0.3.1.dist-info → kotonebot-0.4.0.dist-info}/RECORD +21 -21
- {kotonebot-0.3.1.dist-info → kotonebot-0.4.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.4.0.dist-info}/top_level.txt +0 -0
kotonebot/backend/bot.py
CHANGED
|
@@ -13,7 +13,16 @@ from kotonebot.client.host.protocol import Instance
|
|
|
13
13
|
from kotonebot.backend.context import init_context, vars
|
|
14
14
|
from kotonebot.backend.context import task_registry, action_registry, Task, Action
|
|
15
15
|
from kotonebot.errors import StopCurrentTask, UserFriendlyError
|
|
16
|
-
from kotonebot.
|
|
16
|
+
from kotonebot.util import is_windows
|
|
17
|
+
|
|
18
|
+
# 条件导入 TaskDialog(仅在 Windows 上)
|
|
19
|
+
if is_windows():
|
|
20
|
+
try:
|
|
21
|
+
from kotonebot.interop.win.task_dialog import TaskDialog
|
|
22
|
+
except ImportError:
|
|
23
|
+
TaskDialog = None
|
|
24
|
+
else:
|
|
25
|
+
TaskDialog = None
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
@dataclass
|
|
@@ -226,19 +235,20 @@ class KotoneBot:
|
|
|
226
235
|
# 用户可以自行处理的错误
|
|
227
236
|
except UserFriendlyError as e:
|
|
228
237
|
logger.error(f'Task failed: {task.name}')
|
|
229
|
-
logger.exception(
|
|
238
|
+
logger.exception('Error: ')
|
|
230
239
|
has_error = True
|
|
231
240
|
exception = e
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
if TaskDialog:
|
|
242
|
+
dialog = TaskDialog(
|
|
243
|
+
title='琴音小助手',
|
|
244
|
+
common_buttons=0,
|
|
245
|
+
main_instruction='任务执行失败',
|
|
246
|
+
content=e.message,
|
|
247
|
+
custom_buttons=e.action_buttons,
|
|
248
|
+
main_icon='error'
|
|
249
|
+
)
|
|
250
|
+
result_custom, _, _ = dialog.show()
|
|
251
|
+
e.invoke(result_custom)
|
|
242
252
|
# 其他错误
|
|
243
253
|
except Exception as e:
|
|
244
254
|
logger.error(f'Task failed: {task.name}')
|
|
@@ -175,6 +175,8 @@ class ContextGlobalVars:
|
|
|
175
175
|
self.__vars = dict[str, Any]()
|
|
176
176
|
self.flow: FlowController = FlowController()
|
|
177
177
|
"""流程控制器,负责停止、暂停、恢复等操作"""
|
|
178
|
+
self.screenshot_data: MatLike | None = None
|
|
179
|
+
"""截图数据"""
|
|
178
180
|
|
|
179
181
|
def __getitem__(self, key: str) -> Any:
|
|
180
182
|
return self.__vars[key]
|
|
@@ -197,6 +199,7 @@ class ContextGlobalVars:
|
|
|
197
199
|
def clear(self):
|
|
198
200
|
self.__vars.clear()
|
|
199
201
|
self.flow.reset() # 重置流程控制器
|
|
202
|
+
self.screenshot_data = None
|
|
200
203
|
|
|
201
204
|
def check_flow_control():
|
|
202
205
|
"""
|
|
@@ -220,45 +223,40 @@ class ContextStackVars:
|
|
|
220
223
|
自动截图。即调用 `color`、`image`、`ocr` 上的方法时,会自动更新截图。
|
|
221
224
|
* `manual`
|
|
222
225
|
完全手动截图,不自动截图。如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
223
|
-
*
|
|
224
|
-
|
|
225
|
-
如果调用者没有截图数据,则继承的截图数据为空。
|
|
226
|
-
如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
|
|
226
|
+
* ~~`manual-inherit`~~:
|
|
227
|
+
已废弃。
|
|
227
228
|
"""
|
|
228
|
-
self._screenshot: MatLike | None = None
|
|
229
|
-
"""截图数据"""
|
|
230
|
-
self._inherit_screenshot: MatLike | None = None
|
|
231
|
-
"""继承的截图数据"""
|
|
232
229
|
|
|
233
230
|
@property
|
|
234
231
|
def screenshot(self) -> MatLike:
|
|
235
232
|
match self.screenshot_mode:
|
|
236
|
-
case 'manual':
|
|
237
|
-
if
|
|
238
|
-
raise ValueError("No screenshot data found.")
|
|
239
|
-
return
|
|
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
|
|
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
|
|
248
237
|
case 'auto':
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
251
242
|
case _:
|
|
252
243
|
raise ValueError(f"Invalid screenshot mode: {self.screenshot_mode}")
|
|
253
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
|
+
|
|
254
255
|
@staticmethod
|
|
255
256
|
def push(*, screenshot_mode: ScreenshotMode | None = None) -> 'ContextStackVars':
|
|
256
257
|
vars = ContextStackVars()
|
|
257
258
|
if screenshot_mode is not None:
|
|
258
259
|
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
260
|
ContextStackVars.stack.append(vars)
|
|
263
261
|
return vars
|
|
264
262
|
|
|
@@ -266,7 +264,7 @@ class ContextStackVars:
|
|
|
266
264
|
def pop() -> 'ContextStackVars':
|
|
267
265
|
last = ContextStackVars.stack.pop()
|
|
268
266
|
return last
|
|
269
|
-
|
|
267
|
+
|
|
270
268
|
@staticmethod
|
|
271
269
|
def current() -> 'ContextStackVars | None':
|
|
272
270
|
if len(ContextStackVars.stack) == 0:
|
|
@@ -331,7 +329,7 @@ class ContextOcr:
|
|
|
331
329
|
)
|
|
332
330
|
self.context.device.last_find = ret.original_rect if ret else None
|
|
333
331
|
return ret
|
|
334
|
-
|
|
332
|
+
|
|
335
333
|
def find_all(
|
|
336
334
|
self,
|
|
337
335
|
patterns: Sequence[str | re.Pattern | StringMatchFunction],
|
|
@@ -366,7 +364,7 @@ class ContextOcr:
|
|
|
366
364
|
ret = engine.expect(ContextStackVars.ensure_current().screenshot, pattern, rect=rect, hint=hint)
|
|
367
365
|
self.context.device.last_find = ret.original_rect if ret else None
|
|
368
366
|
return ret
|
|
369
|
-
|
|
367
|
+
|
|
370
368
|
def expect_wait(
|
|
371
369
|
self,
|
|
372
370
|
pattern: str | re.Pattern | StringMatchFunction,
|
|
@@ -447,7 +445,7 @@ class ContextImage:
|
|
|
447
445
|
等待指定图像出现。
|
|
448
446
|
"""
|
|
449
447
|
is_manual = is_manual_screenshot_mode()
|
|
450
|
-
|
|
448
|
+
|
|
451
449
|
start_time = time.time()
|
|
452
450
|
while True:
|
|
453
451
|
if is_manual:
|
|
@@ -702,7 +700,7 @@ class ContextConfig(Generic[T]):
|
|
|
702
700
|
def current(self) -> UserConfig[T]:
|
|
703
701
|
"""
|
|
704
702
|
当前配置数据。
|
|
705
|
-
|
|
703
|
+
|
|
706
704
|
如果当前配置不存在,则使用默认值自动创建一个新配置。
|
|
707
705
|
(不推荐,建议在 UI 中启动前要求用户手动创建,或自行创建一个默认配置。)
|
|
708
706
|
"""
|
|
@@ -729,7 +727,7 @@ class Forwarded:
|
|
|
729
727
|
if self._FORWARD_getter is None:
|
|
730
728
|
raise ContextNotInitializedError(f"Forwarded object {self._FORWARD_name} called before initialization.")
|
|
731
729
|
return getattr(self._FORWARD_getter(), name)
|
|
732
|
-
|
|
730
|
+
|
|
733
731
|
def __setattr__(self, name: str, value: Any):
|
|
734
732
|
if name.startswith('_FORWARD_'):
|
|
735
733
|
return object.__setattr__(self, name, value)
|
|
@@ -761,25 +759,20 @@ class ContextDevice(Generic[T_Device], Device):
|
|
|
761
759
|
"""
|
|
762
760
|
check_flow_control()
|
|
763
761
|
global next_wait, last_screenshot_time, next_wait_time
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
last_screenshot_time = time.time()
|
|
779
|
-
next_wait_time = 0
|
|
780
|
-
next_wait = None
|
|
781
|
-
img = self._device.screenshot()
|
|
782
|
-
current._screenshot = img
|
|
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
|
|
783
776
|
return img
|
|
784
777
|
|
|
785
778
|
def __getattribute__(self, name: str):
|
|
@@ -787,7 +780,7 @@ class ContextDevice(Generic[T_Device], Device):
|
|
|
787
780
|
return object.__getattribute__(self, name)
|
|
788
781
|
else:
|
|
789
782
|
return getattr(self._device, name)
|
|
790
|
-
|
|
783
|
+
|
|
791
784
|
def __setattr__(self, name: str, value: Any):
|
|
792
785
|
if name in ['_device', 'screenshot', 'of_android', 'of_windows']:
|
|
793
786
|
return object.__setattr__(self, name, value)
|
|
@@ -853,7 +846,7 @@ class Context(Generic[T]):
|
|
|
853
846
|
if vars is not None:
|
|
854
847
|
self.__vars = vars
|
|
855
848
|
if debug is not None:
|
|
856
|
-
self.__debug = debug
|
|
849
|
+
self.__debug = debug
|
|
857
850
|
if config is not None:
|
|
858
851
|
self.__config = config
|
|
859
852
|
|
|
@@ -864,7 +857,7 @@ class Context(Generic[T]):
|
|
|
864
857
|
@property
|
|
865
858
|
def ocr(self) -> 'ContextOcr':
|
|
866
859
|
return self.__ocr
|
|
867
|
-
|
|
860
|
+
|
|
868
861
|
@property
|
|
869
862
|
def image(self) -> 'ContextImage':
|
|
870
863
|
return self.__image
|
|
@@ -876,7 +869,7 @@ class Context(Generic[T]):
|
|
|
876
869
|
@property
|
|
877
870
|
def vars(self) -> 'ContextGlobalVars':
|
|
878
871
|
return self.__vars
|
|
879
|
-
|
|
872
|
+
|
|
880
873
|
@property
|
|
881
874
|
def debug(self) -> 'ContextDebug':
|
|
882
875
|
return self.__debug
|
|
@@ -895,7 +888,7 @@ def rect_expand(rect: Rect, left: int = 0, top: int = 0, right: int = 0, bottom:
|
|
|
895
888
|
def use_screenshot(*args: MatLike | None) -> MatLike:
|
|
896
889
|
for img in args:
|
|
897
890
|
if img is not None:
|
|
898
|
-
|
|
891
|
+
vars.screenshot_data = img
|
|
899
892
|
return img
|
|
900
893
|
return device.screenshot()
|
|
901
894
|
|
|
@@ -1006,4 +999,4 @@ def manual_context(screenshot_mode: ScreenshotMode = 'auto') -> ManualContextMan
|
|
|
1006
999
|
默认情况下,Context* 类仅允许在 @task/@action 函数中使用。
|
|
1007
1000
|
如果想要在其他地方使用,使用此函数手动创建一个上下文。
|
|
1008
1001
|
"""
|
|
1009
|
-
return ManualContextManager(screenshot_mode)
|
|
1002
|
+
return ManualContextManager(screenshot_mode)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
2
|
+
import warnings
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing_extensions import deprecated
|
|
5
|
+
from typing import Callable, ParamSpec, TypeVar, overload, Literal
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
from .context import ContextStackVars, ScreenshotMode
|
|
@@ -93,6 +95,17 @@ def task(
|
|
|
93
95
|
@overload
|
|
94
96
|
def action(func: Callable[P, R]) -> Callable[P, R]: ...
|
|
95
97
|
|
|
98
|
+
@deprecated('Use `action` with screenshot_mode=`manual` instead.')
|
|
99
|
+
@overload
|
|
100
|
+
def action(
|
|
101
|
+
name: str,
|
|
102
|
+
*,
|
|
103
|
+
description: str|None = None,
|
|
104
|
+
pass_through: bool = False,
|
|
105
|
+
priority: int = 0,
|
|
106
|
+
screenshot_mode: Literal['manual-inherit'],
|
|
107
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
108
|
+
|
|
96
109
|
@overload
|
|
97
110
|
def action(
|
|
98
111
|
name: str,
|
|
@@ -115,9 +128,6 @@ def action(
|
|
|
115
128
|
"""
|
|
116
129
|
...
|
|
117
130
|
|
|
118
|
-
# TODO: 需要找个地方统一管理这些属性名
|
|
119
|
-
ATTR_ORIGINAL_FUNC = '_kb_inner'
|
|
120
|
-
ATTR_ACTION_MARK = '__kb_action_mark'
|
|
121
131
|
def action(*args, **kwargs):
|
|
122
132
|
def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
|
|
123
133
|
description = description or func.__doc__ or ''
|
|
@@ -136,8 +146,6 @@ def action(*args, **kwargs):
|
|
|
136
146
|
ContextStackVars.pop()
|
|
137
147
|
current_callstack.pop()
|
|
138
148
|
return ret
|
|
139
|
-
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
|
|
140
|
-
setattr(_wrapper, ATTR_ACTION_MARK, True)
|
|
141
149
|
action.func = _wrapper
|
|
142
150
|
return _wrapper
|
|
143
151
|
else:
|
|
@@ -146,6 +154,8 @@ def action(*args, **kwargs):
|
|
|
146
154
|
pass_through = kwargs.get('pass_through', False)
|
|
147
155
|
priority = kwargs.get('priority', 0)
|
|
148
156
|
screenshot_mode = kwargs.get('screenshot_mode', None)
|
|
157
|
+
if screenshot_mode == 'manual-inherit':
|
|
158
|
+
warnings.warn('`screenshot_mode=manual-inherit` is deprecated. Use `screenshot_mode=manual` instead.')
|
|
149
159
|
def _action_decorator(func: Callable):
|
|
150
160
|
nonlocal pass_through
|
|
151
161
|
action = _register(_placeholder, name, description)
|
|
@@ -160,8 +170,6 @@ def action(*args, **kwargs):
|
|
|
160
170
|
ContextStackVars.pop()
|
|
161
171
|
current_callstack.pop()
|
|
162
172
|
return ret
|
|
163
|
-
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
|
|
164
|
-
setattr(_wrapper, ATTR_ACTION_MARK, True)
|
|
165
173
|
action.func = _wrapper
|
|
166
174
|
return _wrapper
|
|
167
175
|
return _action_decorator
|
kotonebot/backend/core.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from functools import cache
|
|
3
3
|
from typing import Callable
|
|
4
|
+
from typing_extensions import deprecated
|
|
4
5
|
|
|
5
6
|
import cv2
|
|
6
7
|
from cv2.typing import MatLike
|
|
@@ -9,6 +10,7 @@ from kotonebot.util import cv2_imread
|
|
|
9
10
|
from kotonebot.primitives import RectTuple, Rect, Point
|
|
10
11
|
from kotonebot.errors import ResourceFileMissingError
|
|
11
12
|
|
|
13
|
+
@deprecated('unused')
|
|
12
14
|
class Ocr:
|
|
13
15
|
def __init__(
|
|
14
16
|
self,
|
|
@@ -19,7 +21,8 @@ class Ocr:
|
|
|
19
21
|
self.text = text
|
|
20
22
|
self.language = language
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
# TODO: 这个类和 kotonebot.primitives.Image 重复了
|
|
25
|
+
@deprecated('Use kotonebot.primitives.Image instead.')
|
|
23
26
|
class Image:
|
|
24
27
|
def __init__(
|
|
25
28
|
self,
|
|
@@ -65,7 +68,7 @@ class Image:
|
|
|
65
68
|
else:
|
|
66
69
|
return f'<Image: "{self.name}" at {self.path}>'
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
# TODO: 这里的其他类应该移动到 primitives 模块下面
|
|
69
72
|
class HintBox(Rect):
|
|
70
73
|
def __init__(
|
|
71
74
|
self,
|
kotonebot/backend/loop.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from functools import lru_cache, partial
|
|
3
3
|
from typing import Callable, Any, overload, Literal, Generic, TypeVar, cast, get_args, get_origin
|
|
4
|
+
from typing_extensions import deprecated
|
|
4
5
|
|
|
5
6
|
from cv2.typing import MatLike
|
|
6
7
|
|
|
@@ -9,8 +10,9 @@ from kotonebot import device, image, ocr
|
|
|
9
10
|
from kotonebot.backend.core import Image
|
|
10
11
|
from kotonebot.backend.ocr import TextComparator
|
|
11
12
|
from kotonebot.client.protocol import ClickableObjectProtocol
|
|
13
|
+
from .context import vars
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
@deprecated('No longer used.')
|
|
14
16
|
class LoopAction:
|
|
15
17
|
def __init__(self, loop: 'Loop', func: Callable[[], ClickableObjectProtocol | None]):
|
|
16
18
|
self.loop = loop
|
|
@@ -81,15 +83,16 @@ class Loop:
|
|
|
81
83
|
是否在每次循环开始时(Loop.tick() 被调用时)截图。
|
|
82
84
|
"""
|
|
83
85
|
self.__last_loop: float = -1
|
|
84
|
-
self.
|
|
86
|
+
self.interval = interval
|
|
87
|
+
"""每次循环后等待的时间。"""
|
|
85
88
|
self.screenshot: MatLike | None = None
|
|
86
89
|
"""上次截图时的图像数据。"""
|
|
87
90
|
self.__skip_first_wait = skip_first_wait
|
|
88
91
|
self.__is_first_tick = True
|
|
89
92
|
|
|
90
93
|
def __iter__(self):
|
|
91
|
-
self.__interval.reset()
|
|
92
94
|
self.__is_first_tick = True
|
|
95
|
+
vars.flow.check()
|
|
93
96
|
return self
|
|
94
97
|
|
|
95
98
|
def __next__(self):
|
|
@@ -101,7 +104,7 @@ class Loop:
|
|
|
101
104
|
|
|
102
105
|
def tick(self):
|
|
103
106
|
if not (self.__is_first_tick and self.__skip_first_wait):
|
|
104
|
-
self.
|
|
107
|
+
time.sleep(self.interval)
|
|
105
108
|
self.__is_first_tick = False
|
|
106
109
|
|
|
107
110
|
if self.auto_screenshot:
|
|
@@ -117,13 +120,16 @@ class Loop:
|
|
|
117
120
|
self.running = False
|
|
118
121
|
|
|
119
122
|
@overload
|
|
123
|
+
@deprecated('Use plain if statement instead.')
|
|
120
124
|
def when(self, condition: Image) -> LoopAction:
|
|
121
125
|
...
|
|
122
126
|
|
|
123
127
|
@overload
|
|
128
|
+
@deprecated('Use plain if statement instead.')
|
|
124
129
|
def when(self, condition: TextComparator) -> LoopAction:
|
|
125
130
|
...
|
|
126
131
|
|
|
132
|
+
@deprecated('Use plain if statement instead.')
|
|
127
133
|
def when(self, condition: Any):
|
|
128
134
|
"""
|
|
129
135
|
判断某个条件是否成立。
|
|
@@ -142,6 +148,7 @@ class Loop:
|
|
|
142
148
|
la.do()
|
|
143
149
|
return la
|
|
144
150
|
|
|
151
|
+
@deprecated('Use plain if statement instead.')
|
|
145
152
|
def until(self, condition: Any):
|
|
146
153
|
"""
|
|
147
154
|
当满足指定条件时,结束循环。
|
|
@@ -150,6 +157,7 @@ class Loop:
|
|
|
150
157
|
"""
|
|
151
158
|
return self.when(condition).call(lambda _: self.exit())
|
|
152
159
|
|
|
160
|
+
@deprecated('Use image.find() and device.click() instead.')
|
|
153
161
|
def click_if(self, condition: Any, *, at: tuple[int, int] | None = None):
|
|
154
162
|
"""
|
|
155
163
|
检测指定对象是否出现,若出现,点击该对象或指定位置。
|
|
@@ -198,87 +206,3 @@ class StatedLoop(Loop, Generic[StateType]):
|
|
|
198
206
|
self.states = cast(tuple[StateType, ...], state_values)
|
|
199
207
|
self.state = self.__tmp_initial_state or self.states[0]
|
|
200
208
|
return state_values
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def StatedLoop2(states: StateType) -> StatedLoop[StateType]:
|
|
204
|
-
state_values = get_args(states)
|
|
205
|
-
return cast(StatedLoop[StateType], Loop())
|
|
206
|
-
|
|
207
|
-
if __name__ == '__main__':
|
|
208
|
-
from kotonebot.kaa.tasks import R
|
|
209
|
-
from kotonebot.backend.ocr import contains
|
|
210
|
-
from kotonebot.backend.context import manual_context, init_context
|
|
211
|
-
|
|
212
|
-
# T = TypeVar('T')
|
|
213
|
-
# class Foo(Generic[T]):
|
|
214
|
-
# def get_literal_params(self) -> list | None:
|
|
215
|
-
# """
|
|
216
|
-
# 尝试获取泛型参数 T (如果它是 Literal 类型) 的参数列表。
|
|
217
|
-
# """
|
|
218
|
-
# # self.__orig_class__ 会是 Foo 的具体参数化类型,
|
|
219
|
-
# # 例如 Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]
|
|
220
|
-
# if not hasattr(self, '__orig_class__'):
|
|
221
|
-
# # 如果 Foo 不是以参数化泛型的方式实例化的,可能没有 __orig_class__
|
|
222
|
-
# return None
|
|
223
|
-
#
|
|
224
|
-
# # generic_type_args 是传递给 Foo 的类型参数元组
|
|
225
|
-
# # 例如 (Literal['p0', 'p1', 'p2', 'p3', 'ap'],)
|
|
226
|
-
# generic_type_args = get_args(self.__orig_class__)
|
|
227
|
-
#
|
|
228
|
-
# if not generic_type_args:
|
|
229
|
-
# # Foo 没有类型参数
|
|
230
|
-
# return None
|
|
231
|
-
#
|
|
232
|
-
# # T_type 是 Foo 的第一个类型参数
|
|
233
|
-
# # 例如 Literal['p0', 'p1', 'p2', 'p3', 'ap']
|
|
234
|
-
# t_type = generic_type_args[0]
|
|
235
|
-
#
|
|
236
|
-
# # 检查 T_type 是否是 Literal 类型
|
|
237
|
-
# if get_origin(t_type) is Literal:
|
|
238
|
-
# # literal_args 是 Literal 类型的参数元组
|
|
239
|
-
# # 例如 ('p0', 'p1', 'p2', 'p3', 'ap')
|
|
240
|
-
# literal_args = get_args(t_type)
|
|
241
|
-
# return list(literal_args)
|
|
242
|
-
# else:
|
|
243
|
-
# # T 不是 Literal 类型
|
|
244
|
-
# return None
|
|
245
|
-
# f = Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
|
|
246
|
-
# values = f.get_literal_params()
|
|
247
|
-
# 1
|
|
248
|
-
|
|
249
|
-
from typing_extensions import reveal_type
|
|
250
|
-
slp = StatedLoop[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
|
|
251
|
-
for l in slp:
|
|
252
|
-
reveal_type(l.states)
|
|
253
|
-
|
|
254
|
-
# init_context()
|
|
255
|
-
# manual_context().begin()
|
|
256
|
-
# for l in Loop():
|
|
257
|
-
# l.when(R.Produce.ButtonUse).click()
|
|
258
|
-
# l.when(R.Produce.ButtonRefillAP).click()
|
|
259
|
-
# l.when(contains("123")).click()
|
|
260
|
-
# l.click_if(contains("!23"), at=(1, 2))
|
|
261
|
-
|
|
262
|
-
# State = Literal['p0', 'p1', 'p2', 'p3', 'ap']
|
|
263
|
-
# for sl in StatedLoop[State]():
|
|
264
|
-
# match sl.state:
|
|
265
|
-
# case 'p0':
|
|
266
|
-
# sl.click_if(R.Produce.ButtonProduce)
|
|
267
|
-
# sl.click_if(contains('master'))
|
|
268
|
-
# sl.when(R.Produce.ButtonPIdolOverview).goto('p1')
|
|
269
|
-
# # AP 不足
|
|
270
|
-
# sl.when(R.Produce.TextAPInsufficient).goto('ap')
|
|
271
|
-
# case 'ap':
|
|
272
|
-
# pass
|
|
273
|
-
# # p1: 选择偶像
|
|
274
|
-
# case 'p1':
|
|
275
|
-
# sl.call(lambda _: select_idol(idol_skin_id), once=True)
|
|
276
|
-
# sl.when(R.Produce.TextAnotherIdolAvailableDialog).call(dialog.no)
|
|
277
|
-
# sl.click_if(R.Common.ButtonNextNoIcon)
|
|
278
|
-
# sl.until(R.Produce.TextStepIndicator2).goto('p2')
|
|
279
|
-
# case 'p2':
|
|
280
|
-
# sl.when(contains("123")).click()
|
|
281
|
-
# case 'p3':
|
|
282
|
-
# sl.click_if(contains("!23"), at=(1, 2))
|
|
283
|
-
# case _:
|
|
284
|
-
# assert_never(sl.state)
|
kotonebot/client/device.py
CHANGED
|
@@ -239,7 +239,8 @@ class Device:
|
|
|
239
239
|
调用前确保 `orientation` 属性与设备方向一致,
|
|
240
240
|
否则点击位置会不正确。
|
|
241
241
|
"""
|
|
242
|
-
|
|
242
|
+
size = self.target_resolution or self.screen_size
|
|
243
|
+
x, y = size[0] // 2, size[1] // 2
|
|
243
244
|
self.click(x, y)
|
|
244
245
|
|
|
245
246
|
@overload
|
|
@@ -28,11 +28,15 @@ def connect_adb(
|
|
|
28
28
|
if disconnect:
|
|
29
29
|
logger.debug('adb disconnect %s:%d', ip, port)
|
|
30
30
|
adb.disconnect(f'{ip}:{port}')
|
|
31
|
+
else:
|
|
32
|
+
logger.debug('Skip adb disconnect.')
|
|
31
33
|
if connect:
|
|
32
34
|
logger.debug('adb connect %s:%d', ip, port)
|
|
33
35
|
result = adb.connect(f'{ip}:{port}')
|
|
34
36
|
if 'cannot connect to' in result:
|
|
35
37
|
raise ValueError(result)
|
|
38
|
+
else:
|
|
39
|
+
logger.debug('Skip adb connect.')
|
|
36
40
|
serial = device_serial or f'{ip}:{port}'
|
|
37
41
|
logger.debug('adb wait for %s', serial)
|
|
38
42
|
adb.wait_for(serial, timeout=timeout)
|
|
@@ -56,15 +60,20 @@ class CommonAdbCreateDeviceMixin(ABC):
|
|
|
56
60
|
self.adb_port: int
|
|
57
61
|
self.adb_name: str
|
|
58
62
|
|
|
59
|
-
def create_device(self, recipe: AdbRecipes, config: AdbHostConfig) -> Device:
|
|
63
|
+
def create_device(self, recipe: AdbRecipes, config: AdbHostConfig, *, connect: bool = True, disconnect: bool = True) -> Device:
|
|
60
64
|
"""
|
|
61
65
|
创建 ADB 设备。
|
|
66
|
+
|
|
67
|
+
:param recipe: 连接方式配方名称。
|
|
68
|
+
:param config: ADB 配置。
|
|
69
|
+
:param connect: 创建设备实例前,是否连接 ADB(执行 adb connect)。
|
|
70
|
+
:param disconnect: 创建设备实例前,是否先断开已有 ADB 连接(执行 adb disconnect)。
|
|
62
71
|
"""
|
|
63
72
|
connection = connect_adb(
|
|
64
73
|
self.adb_ip,
|
|
65
74
|
self.adb_port,
|
|
66
|
-
connect=
|
|
67
|
-
disconnect=
|
|
75
|
+
connect=connect,
|
|
76
|
+
disconnect=disconnect,
|
|
68
77
|
timeout=config.timeout,
|
|
69
78
|
device_serial=self.adb_name
|
|
70
79
|
)
|
|
@@ -77,7 +77,7 @@ class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
|
77
77
|
if self.adb_port is None:
|
|
78
78
|
raise ValueError("ADB port is not set and is required.")
|
|
79
79
|
|
|
80
|
-
return super().create_device(impl, host_config)
|
|
80
|
+
return super().create_device(impl, host_config, connect=False, disconnect=False)
|
|
81
81
|
|
|
82
82
|
class LeidianHost(HostProtocol[LeidianRecipes]):
|
|
83
83
|
@staticmethod
|
|
@@ -4,6 +4,7 @@ from typing_extensions import assert_never
|
|
|
4
4
|
|
|
5
5
|
from kotonebot import logging
|
|
6
6
|
from kotonebot.client.device import WindowsDevice
|
|
7
|
+
from kotonebot.util import require_windows
|
|
7
8
|
from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
@@ -19,11 +20,13 @@ class CommonWindowsCreateDeviceMixin(ABC):
|
|
|
19
20
|
"""
|
|
20
21
|
def __init__(self, *args, **kwargs) -> None:
|
|
21
22
|
super().__init__(*args, **kwargs)
|
|
23
|
+
require_windows('CommonWindowsCreateDeviceMixin', self.__class__)
|
|
22
24
|
|
|
23
25
|
def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
|
|
24
26
|
"""
|
|
25
27
|
创建 Windows 设备。
|
|
26
28
|
"""
|
|
29
|
+
require_windows('CommonWindowsCreateDeviceMixin.create_device', self.__class__)
|
|
27
30
|
match recipe:
|
|
28
31
|
case 'windows':
|
|
29
32
|
if not isinstance(config, WindowsHostConfig):
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
from kotonebot.util import is_windows, require_windows
|
|
2
|
+
|
|
3
|
+
# 基础实现
|
|
2
4
|
from . import adb # noqa: F401
|
|
3
5
|
from . import adb_raw # noqa: F401
|
|
4
|
-
from . import remote_windows # noqa: F401
|
|
5
6
|
from . import uiautomator2 # noqa: F401
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
# Windows 实现(默认仅在 Windows 上导入)
|
|
9
|
+
if is_windows():
|
|
10
|
+
try:
|
|
11
|
+
from . import nemu_ipc # noqa: F401
|
|
12
|
+
from . import windows # noqa: F401
|
|
13
|
+
from . import remote_windows # noqa: F401
|
|
14
|
+
except ImportError:
|
|
15
|
+
require_windows('"windows" and "remote_windows" implementations')
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
This module provides:
|
|
5
|
-
1. RemoteWindowsImpl - Client implementation that connects to a remote Windows machine
|
|
6
|
-
2. RemoteWindowsServer - Server implementation that exposes a WindowsImpl instance via XML-RPC
|
|
7
|
-
"""
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
from kotonebot.util import require_windows
|
|
3
|
+
require_windows('"RemoteWindowsImpl" implementation')
|
|
8
4
|
|
|
9
5
|
import io
|
|
10
6
|
import base64
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows Task Dialog interop module.
|
|
3
|
+
|
|
4
|
+
This module provides Windows TaskDialog functionality and is only available on Windows systems.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
from kotonebot.util import is_windows
|
|
11
|
+
|
|
12
|
+
# 检查是否在 Windows 平台上
|
|
13
|
+
if not is_windows():
|
|
14
|
+
_WINDOWS_ONLY_MSG = (
|
|
15
|
+
f"TaskDialog is only available on Windows systems. "
|
|
16
|
+
f"Current system: non-Windows\n"
|
|
17
|
+
"To use Windows TaskDialog features, please run this code on a Windows system."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 提供虚拟类以避免导入错误
|
|
22
|
+
class TaskDialog:
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
25
|
+
|
|
26
|
+
# 导出所有常量作为 None
|
|
27
|
+
__all__ = [
|
|
28
|
+
"TaskDialog",
|
|
29
|
+
"TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
|
|
30
|
+
"TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
|
|
31
|
+
"IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
|
|
32
|
+
"TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# 设置所有常量为 None 或保留为模块级变量
|
|
36
|
+
TDCBF_OK_BUTTON = TDCBF_YES_BUTTON = TDCBF_NO_BUTTON = TDCBF_CANCEL_BUTTON = None
|
|
37
|
+
TDCBF_RETRY_BUTTON = TDCBF_CLOSE_BUTTON = None
|
|
38
|
+
IDOK = IDCANCEL = IDABORT = IDRETRY = IDIGNORE = IDYES = IDNO = IDCLOSE = None
|
|
39
|
+
TD_WARNING_ICON = TD_ERROR_ICON = TD_INFORMATION_ICON = TD_SHIELD_ICON = None
|
|
40
|
+
|
|
41
|
+
# 阻止模块加载
|
|
42
|
+
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
43
|
+
|
|
44
|
+
# 如果是 Windows,继续正常加载
|
|
1
45
|
import ctypes
|
|
2
46
|
from ctypes import wintypes
|
|
3
47
|
import time
|
kotonebot/ui/user.py
CHANGED
|
@@ -4,7 +4,12 @@ import time
|
|
|
4
4
|
|
|
5
5
|
import cv2
|
|
6
6
|
from cv2.typing import MatLike
|
|
7
|
-
from
|
|
7
|
+
from kotonebot.util import is_windows
|
|
8
|
+
if is_windows():
|
|
9
|
+
from win11toast import toast
|
|
10
|
+
else:
|
|
11
|
+
def toast(title: str, message: str | None = None, buttons: list[str] | None = None):
|
|
12
|
+
raise ImportError('toast notification is only available on Windows')
|
|
8
13
|
|
|
9
14
|
from .pushkit import Wxpusher
|
|
10
15
|
from .. import logging
|
kotonebot/util.py
CHANGED
|
@@ -4,6 +4,7 @@ import pstats
|
|
|
4
4
|
import typing
|
|
5
5
|
import logging
|
|
6
6
|
import cProfile
|
|
7
|
+
import platform
|
|
7
8
|
from importlib import resources
|
|
8
9
|
from functools import lru_cache
|
|
9
10
|
from typing import Literal, Callable, TYPE_CHECKING, TypeGuard
|
|
@@ -17,6 +18,32 @@ if TYPE_CHECKING:
|
|
|
17
18
|
from kotonebot.client.protocol import Device
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
21
|
+
_WINDOWS_ONLY_MSG = (
|
|
22
|
+
"This feature is only available on Windows. "
|
|
23
|
+
f"You are using {platform.system()}.\n"
|
|
24
|
+
"The requested feature is: {feature_name}\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def is_windows() -> bool:
|
|
28
|
+
"""检查当前是否为 Windows 系统"""
|
|
29
|
+
return platform.system() == 'Windows'
|
|
30
|
+
|
|
31
|
+
def is_linux() -> bool:
|
|
32
|
+
"""检查当前是否为 Linux 系统"""
|
|
33
|
+
return platform.system() == 'Linux'
|
|
34
|
+
|
|
35
|
+
def is_macos() -> bool:
|
|
36
|
+
"""检查当前是否为 macOS 系统"""
|
|
37
|
+
return platform.system() == 'Darwin'
|
|
38
|
+
|
|
39
|
+
def require_windows(feature_name: str | None = None, class_: type | None = None) -> None:
|
|
40
|
+
"""要求必须在 Windows 系统上运行,否则抛出 ImportError"""
|
|
41
|
+
if not is_windows():
|
|
42
|
+
feature_name = feature_name or 'not specified'
|
|
43
|
+
if class_:
|
|
44
|
+
full_name = '.'.join([class_.__module__, class_.__name__])
|
|
45
|
+
feature_name += f' ({full_name})'
|
|
46
|
+
raise ImportError(_WINDOWS_ONLY_MSG.format(feature_name=feature_name))
|
|
20
47
|
|
|
21
48
|
|
|
22
49
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kotonebot
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Kotonebot is game
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Kotonebot is game automation library based on computer vision technology, works for Windows and Android.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: opencv-python~=4.10
|
|
9
9
|
Requires-Dist: rapidocr_onnxruntime~=1.4
|
|
10
|
-
Requires-Dist: av~=14.0
|
|
11
10
|
Requires-Dist: scikit-image~=0.25
|
|
12
11
|
Requires-Dist: thefuzz~=0.22
|
|
13
12
|
Requires-Dist: pydantic~=2.10
|
|
@@ -17,24 +16,26 @@ Requires-Dist: python-dotenv~=1.0
|
|
|
17
16
|
Requires-Dist: onnxruntime~=1.14
|
|
18
17
|
Requires-Dist: numpy
|
|
19
18
|
Provides-Extra: android
|
|
20
|
-
Requires-Dist: adbutils
|
|
21
|
-
Requires-Dist: uiautomator2
|
|
19
|
+
Requires-Dist: adbutils>=2.8; extra == "android"
|
|
20
|
+
Requires-Dist: uiautomator2>=3.2; extra == "android"
|
|
22
21
|
Provides-Extra: windows
|
|
23
22
|
Requires-Dist: pywin32; extra == "windows"
|
|
24
|
-
Requires-Dist: ahk
|
|
25
|
-
Requires-Dist: win11toast
|
|
26
|
-
Requires-Dist: psutil
|
|
23
|
+
Requires-Dist: ahk>=1.8; extra == "windows"
|
|
24
|
+
Requires-Dist: win11toast>=0.35; extra == "windows"
|
|
25
|
+
Requires-Dist: psutil>=6.1; extra == "windows"
|
|
27
26
|
Provides-Extra: dev
|
|
28
27
|
Requires-Dist: fastapi~=0.115; extra == "dev"
|
|
29
28
|
Requires-Dist: uvicorn~=0.34; extra == "dev"
|
|
30
29
|
Requires-Dist: python-multipart~=0.0; extra == "dev"
|
|
31
30
|
Requires-Dist: websockets~=14.1; extra == "dev"
|
|
32
31
|
Requires-Dist: psutil~=6.1; extra == "dev"
|
|
33
|
-
Requires-Dist:
|
|
34
|
-
Requires-Dist: snakeviz; extra == "dev"
|
|
32
|
+
Requires-Dist: twine~=6.1; extra == "dev"
|
|
35
33
|
Requires-Dist: build; extra == "dev"
|
|
34
|
+
Requires-Dist: snakeviz; extra == "dev"
|
|
35
|
+
Requires-Dist: jinja2~=3.1; extra == "dev"
|
|
36
|
+
Requires-Dist: tqdm~=4.67; extra == "dev"
|
|
36
37
|
Provides-Extra: all
|
|
37
|
-
Requires-Dist: kotonebot[android,windows]; extra == "all"
|
|
38
|
+
Requires-Dist: kotonebot[android,dev,windows]; extra == "all"
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# kotonebot
|
|
@@ -67,6 +68,10 @@ pip install kotonebot[dev]
|
|
|
67
68
|
## 快速开始
|
|
68
69
|
WIP
|
|
69
70
|
|
|
71
|
+
### 协同开发
|
|
72
|
+
有时候你可能想以源码方式安装 kotonebot,以便与自己的项目一起调试修改。此时,如果你以 `pip install -e /path/to/kotonebot` 的方式安装,Pylance 可能无法正常静态分析。
|
|
73
|
+
解决方案是在 VSCode 里搜索 `python.analysis.extraPaths` 并将其设置为你本地 kotonebot 的根目录。
|
|
74
|
+
|
|
70
75
|
## 文档
|
|
71
76
|
WIP
|
|
72
77
|
|
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
kotonebot/__init__.py,sha256=DWS8zZAWH-MQZxk6bDm9uNAfY9UVmtd7Q928suujpiQ,636
|
|
2
2
|
kotonebot/errors.py,sha256=1mZLFhNNxtRkm66y80rPzOeFjCgR0TgxgCDGo9ByZek,2294
|
|
3
|
-
kotonebot/util.py,sha256=
|
|
3
|
+
kotonebot/util.py,sha256=ftWhlnZ3qJ80x2wVCHYr2wW299JQkYDOREP1mA-n1vg,14370
|
|
4
4
|
kotonebot/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
kotonebot/backend/bot.py,sha256=
|
|
5
|
+
kotonebot/backend/bot.py,sha256=Ph0pfAubr_TV4MmqkyYL0P0hLbXdFEkzlQ0V2B1niZI,11870
|
|
6
6
|
kotonebot/backend/color.py,sha256=KqFISc6puMNbsyB5diu5PcqNjdkLwUeryVsMmeAJG4Q,20125
|
|
7
|
-
kotonebot/backend/core.py,sha256=
|
|
7
|
+
kotonebot/backend/core.py,sha256=A0f2i1KO9PXn4ndRDOBbADtB_beVxD1P0g4FYioxUY0,4026
|
|
8
8
|
kotonebot/backend/dispatch.py,sha256=t3eqkOVNNrdaeXSMLxDbReYhVRoAlSRNxb7oO2clRsQ,7423
|
|
9
9
|
kotonebot/backend/flow_controller.py,sha256=sbr6JiuYXErevY_BPrzw7hUCfxLGVrz0W_KNbXIxe9Q,6228
|
|
10
10
|
kotonebot/backend/image.py,sha256=jXYGVXa9K1zBCJBG1Btsf2AG5XI9A0DMy5uYy6KOusk,28111
|
|
11
|
-
kotonebot/backend/loop.py,sha256=
|
|
11
|
+
kotonebot/backend/loop.py,sha256=e4STYQ6sbUp53xcYJXyhz4vOyEzXds1MKR0xebqU228,6638
|
|
12
12
|
kotonebot/backend/ocr.py,sha256=IE_LUCdD4i00a02xgLkSRPcYYo3R2MBfZKKQHjOFfgM,18058
|
|
13
13
|
kotonebot/backend/preprocessor.py,sha256=YmAbLa-XXES2AchMJtsBpPZwIIGHuYapwpXpw0YSpbA,3423
|
|
14
14
|
kotonebot/backend/context/__init__.py,sha256=0X9jzM0ftGQUgjoXCk98xf1inoCqHqaUwT0wcHn9P5s,168
|
|
15
|
-
kotonebot/backend/context/context.py,sha256=
|
|
16
|
-
kotonebot/backend/context/task_action.py,sha256=
|
|
15
|
+
kotonebot/backend/context/context.py,sha256=R91knq7Ry5MSWaFhOj3HIF13u8bomOOsGk59yPOG4aw,33780
|
|
16
|
+
kotonebot/backend/context/task_action.py,sha256=LdxyeW-1ie71ludnAN36ltlcAw4AX3Lb9ua7I5xK4yY,6444
|
|
17
17
|
kotonebot/backend/debug/__init__.py,sha256=pcSpwzU2YwGrogOoHmsI035nkA_-kDLfm-lfBxHuQ-c,43
|
|
18
18
|
kotonebot/backend/debug/entry.py,sha256=_sIGi9_LegbaM2DCcDTvGJhrkyUqF6W2Al4nYmkb9rM,2833
|
|
19
19
|
kotonebot/backend/debug/mock.py,sha256=0fTiJeqVTanQv6L3TPbldgbJRBPmunZOjlzKtVicJ_M,2118
|
|
20
20
|
kotonebot/backend/debug/server.py,sha256=9PEpczIrwCnD4c_FAtiojwk27aKkkzTtRySCsdho4Rs,7517
|
|
21
21
|
kotonebot/backend/debug/vars.py,sha256=3wtbkH2WFNXyT2ZNu-8en3S0pAb40h-SefYyScRMLnM,10912
|
|
22
22
|
kotonebot/client/__init__.py,sha256=1eXyGopBFpYoucNTNkTo-7nIeDyasyETfHJ8DWuaNTo,192
|
|
23
|
-
kotonebot/client/device.py,sha256=
|
|
23
|
+
kotonebot/client/device.py,sha256=bb49Gbo-zcF1SaeBbRljxkuKGMp5e9umPiguAbV1kok,19405
|
|
24
24
|
kotonebot/client/fast_screenshot.py,sha256=q69AX15VXRuB0U2qFJKfoTOBgG4nVBCUcaN1CX0VsUc,13647
|
|
25
25
|
kotonebot/client/protocol.py,sha256=x05llULFI3MddbvBX_c0sYWlSzCyWa3hped24ktq2Ko,2300
|
|
26
26
|
kotonebot/client/registration.py,sha256=XK424QEWbJKfNdkiDoIJUh_JIy8ryk4I4I3hrWUXCX8,848
|
|
27
27
|
kotonebot/client/host/__init__.py,sha256=WsDNAugbr4k1fMCHBdaYZmrifrq6WmSQByp1qOFbV_I,579
|
|
28
|
-
kotonebot/client/host/adb_common.py,sha256=
|
|
28
|
+
kotonebot/client/host/adb_common.py,sha256=_S2LKpsNnRUREUpzO71OfMIB8Dt0pmxKdSbcmpqss-4,3750
|
|
29
29
|
kotonebot/client/host/custom.py,sha256=_r2GAtJ20CoEx8JO7wVa4jr6KT8YgO5wzrFHLFtOgZE,4079
|
|
30
|
-
kotonebot/client/host/leidian_host.py,sha256=
|
|
30
|
+
kotonebot/client/host/leidian_host.py,sha256=H1yNhKRBdjzyY8_UjqohuJwjj-gVvN1s3dnQfKgFdW8,7729
|
|
31
31
|
kotonebot/client/host/mumu12_host.py,sha256=cWlbZiV0HdN2S42L1Nd5Jcv3bw-b1oxjX4_3y5JE-tI,14444
|
|
32
32
|
kotonebot/client/host/protocol.py,sha256=x2TbnDELDQpqxSKWUHLT3Pez8Qx6IzL4wyUfgf9mFyk,8051
|
|
33
|
-
kotonebot/client/host/windows_common.py,sha256=
|
|
34
|
-
kotonebot/client/implements/__init__.py,sha256=
|
|
33
|
+
kotonebot/client/host/windows_common.py,sha256=_Hf4CvHKNgiMIVY3ZPr3wF71r6nMErNi84Y_fswUjBc,2431
|
|
34
|
+
kotonebot/client/implements/__init__.py,sha256=_qZNM-2wEr4Q-_2kWisyuc4AUoqpRB_gAC_hr8_RkSY,511
|
|
35
35
|
kotonebot/client/implements/adb.py,sha256=OcATiRGj84KMM9mEuQLzLoNyDBaZRJugR-BFrlWjB-c,3145
|
|
36
36
|
kotonebot/client/implements/adb_raw.py,sha256=zUTI9QscmdN031tbBbInUN8G-LrgQv-cTc_Yl0zaZQU,6307
|
|
37
|
-
kotonebot/client/implements/remote_windows.py,sha256=
|
|
37
|
+
kotonebot/client/implements/remote_windows.py,sha256=SRq97cuXdUVvFIhsazZt4djMeGSLfYS88IUyzmlpaww,6756
|
|
38
38
|
kotonebot/client/implements/uiautomator2.py,sha256=ER4cNLI_cCpIGKWIXeuaUPmVtz50JuFO5Kx-ZwCGI1s,2575
|
|
39
|
-
kotonebot/client/implements/windows.py,sha256=-
|
|
39
|
+
kotonebot/client/implements/windows.py,sha256=-P5nFiUbCxz9qMM-WZPtw-Q2xYc3Tmym5MNK3gBwoB4,6738
|
|
40
40
|
kotonebot/client/implements/nemu_ipc/__init__.py,sha256=vSZzv75bn38Wch86PYs5UDOCLwxxoDGm2v1jrwff_S8,200
|
|
41
41
|
kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py,sha256=YsfKf0-qorfAf2YvNuxpLb9af-HJFsu97bnXABshhbA,10643
|
|
42
42
|
kotonebot/client/implements/nemu_ipc/nemu_ipc.py,sha256=LhUUyfB28MDnRg8z2FyGah1hTeOMFiX7w8LZLAAjLF8,12082
|
|
43
43
|
kotonebot/config/__init__.py,sha256=-jATUOdrpUrBRT9RiTRQho2-2zeet50qQggsVMVpqNE,35
|
|
44
44
|
kotonebot/config/base_config.py,sha256=NpJuUmzgjUHelzm55bbVvk9x9MdwuKwfxwpaJFp-9iw,3438
|
|
45
45
|
kotonebot/config/manager.py,sha256=XBtriAU9eo-wv2iKOwyDqu8tzhbKqFCuy0jsAM9T9uU,1061
|
|
46
|
-
kotonebot/interop/win/__init__.py,sha256=
|
|
46
|
+
kotonebot/interop/win/__init__.py,sha256=jH8E0iqT1lOs4C_i5yl4bJ7z7iZh2ieGOF1lu8raJ30,111
|
|
47
47
|
kotonebot/interop/win/message_box.py,sha256=R06GSu936Bx_Wg7ddn6LOvazD9_Gt3mhz4_oUuIoYO0,8635
|
|
48
48
|
kotonebot/interop/win/reg.py,sha256=xw35d1xl8ucITT4bOMFgHmMkAUhak7x3lzegR3g3S48,1347
|
|
49
49
|
kotonebot/interop/win/shortcut.py,sha256=f1u6IWvpw6Kxt014wnHz5Z94rVK1qf4kLtRsI9bJYnk,1764
|
|
50
|
-
kotonebot/interop/win/task_dialog.py,sha256=
|
|
50
|
+
kotonebot/interop/win/task_dialog.py,sha256=Ezi1CsjFSbnKccvYfIRaJoCGHUsJnnIpHb0gtKR603E,20191
|
|
51
51
|
kotonebot/logging/__init__.py,sha256=r0q4z59yYy_bQnHTwJYsiPGwOGIgEOrcXH1mNs1h_N0,142
|
|
52
52
|
kotonebot/logging/log.py,sha256=PLb6r_hlW1mvqU_kx6_89zaQ1IpamgHWG3sQ0ZnlrCI,555
|
|
53
53
|
kotonebot/primitives/__init__.py,sha256=R11AoTQQU6ql86oaVjZJkH2-kkxf5kr9xY8gnqPjLgM,355
|
|
@@ -56,15 +56,15 @@ kotonebot/primitives/visual.py,sha256=fZbNgaM03wCaR1Rb0QgeAQIfNsBBXPoUp2-E2IYFsL
|
|
|
56
56
|
kotonebot/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
57
|
kotonebot/tools/mirror.py,sha256=jlGpeX_WkFpcZxojxDzT5QqVeIpd_UIDzSMR2K31VJ0,13820
|
|
58
58
|
kotonebot/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
kotonebot/ui/user.py,sha256=
|
|
59
|
+
kotonebot/ui/user.py,sha256=aP6Va3iHaVA0-AHViNa04a3HDpLLiovZjV5_AMGY4cI,4485
|
|
60
60
|
kotonebot/ui/file_host/sensio.py,sha256=QyH8DpyO3sf2Hz4NuOW9oDt1V3G06XMO7yKfgLwq1oE,914
|
|
61
61
|
kotonebot/ui/file_host/tmp_send.py,sha256=SwzTUGIjZXaP1_H86qahuLwUd8VeHGWW86XKaC__WKI,1802
|
|
62
62
|
kotonebot/ui/pushkit/__init__.py,sha256=xDUctRUL3euvge-yl8IhFYMlxIxQXsjxcyGN5tUwPtE,73
|
|
63
63
|
kotonebot/ui/pushkit/image_host.py,sha256=nB6BCOA5ZgSGi-ntgqQp49H1UZDk8qC41O_PTLPzZ-E,2581
|
|
64
64
|
kotonebot/ui/pushkit/protocol.py,sha256=KVZ-xr0sMdiuri7AiYqugpZRRtefBsosXm6zouScUR4,266
|
|
65
65
|
kotonebot/ui/pushkit/wxpusher.py,sha256=U7WKxyf9pVgGvppmBxwMRuBuFkQG3NC3tkdRh7_-IOw,1732
|
|
66
|
-
kotonebot-0.
|
|
67
|
-
kotonebot-0.
|
|
68
|
-
kotonebot-0.
|
|
69
|
-
kotonebot-0.
|
|
70
|
-
kotonebot-0.
|
|
66
|
+
kotonebot-0.4.0.dist-info/licenses/LICENSE,sha256=gcuuhKKc5-dwvyvHsXjlC9oM6N5gZ6umYbC8ewW1Yvg,35821
|
|
67
|
+
kotonebot-0.4.0.dist-info/METADATA,sha256=4P-5GBc-v3VEtDlQqx0F3pUJXC7D9p2b0cmPbVZ91NE,3207
|
|
68
|
+
kotonebot-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
69
|
+
kotonebot-0.4.0.dist-info/top_level.txt,sha256=QUWAZdbBndoojkrs6RcNytLAn7a0ns4YNF4tLx2Nc4s,10
|
|
70
|
+
kotonebot-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|