kotonebot 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -302
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +49 -56
  6. kotonebot/backend/context/task_action.py +183 -175
  7. kotonebot/backend/core.py +129 -126
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/loop.py +12 -88
  15. kotonebot/backend/ocr.py +535 -529
  16. kotonebot/backend/preprocessor.py +103 -103
  17. kotonebot/client/__init__.py +9 -9
  18. kotonebot/client/device.py +528 -502
  19. kotonebot/client/fast_screenshot.py +377 -377
  20. kotonebot/client/host/__init__.py +43 -12
  21. kotonebot/client/host/adb_common.py +107 -94
  22. kotonebot/client/host/custom.py +118 -114
  23. kotonebot/client/host/leidian_host.py +196 -201
  24. kotonebot/client/host/mumu12_host.py +353 -358
  25. kotonebot/client/host/protocol.py +214 -213
  26. kotonebot/client/host/windows_common.py +58 -55
  27. kotonebot/client/implements/__init__.py +71 -7
  28. kotonebot/client/implements/adb.py +89 -85
  29. kotonebot/client/implements/adb_raw.py +162 -158
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -192
  34. kotonebot/client/implements/uiautomator2.py +85 -81
  35. kotonebot/client/implements/windows.py +176 -168
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/config/base_config.py +96 -96
  39. kotonebot/config/manager.py +36 -36
  40. kotonebot/errors.py +76 -71
  41. kotonebot/interop/win/__init__.py +10 -0
  42. kotonebot/interop/win/_mouse.py +311 -0
  43. kotonebot/interop/win/message_box.py +313 -313
  44. kotonebot/interop/win/reg.py +37 -37
  45. kotonebot/interop/win/shortcut.py +43 -43
  46. kotonebot/interop/win/task_dialog.py +513 -469
  47. kotonebot/logging/__init__.py +2 -2
  48. kotonebot/logging/log.py +17 -17
  49. kotonebot/primitives/__init__.py +17 -17
  50. kotonebot/primitives/geometry.py +862 -290
  51. kotonebot/primitives/visual.py +63 -63
  52. kotonebot/tools/mirror.py +354 -354
  53. kotonebot/ui/file_host/sensio.py +36 -36
  54. kotonebot/ui/file_host/tmp_send.py +54 -54
  55. kotonebot/ui/pushkit/__init__.py +3 -3
  56. kotonebot/ui/pushkit/image_host.py +88 -87
  57. kotonebot/ui/pushkit/protocol.py +13 -13
  58. kotonebot/ui/pushkit/wxpusher.py +54 -53
  59. kotonebot/ui/user.py +148 -143
  60. kotonebot/util.py +436 -409
  61. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
  62. kotonebot-0.5.0.dist-info/RECORD +71 -0
  63. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  64. kotonebot-0.3.1.dist-info/RECORD +0 -70
  65. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  66. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,43 @@
1
- from .protocol import HostProtocol, Instance, AdbHostConfig, WindowsHostConfig, RemoteWindowsHostConfig
2
- from .custom import CustomInstance, create as create_custom
3
- from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
4
- from .leidian_host import LeidianHost, LeidianInstance
5
-
6
- __all__ = [
7
- 'HostProtocol', 'Instance',
8
- 'AdbHostConfig', 'WindowsHostConfig', 'RemoteWindowsHostConfig',
9
- 'CustomInstance', 'create_custom',
10
- 'Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance',
11
- 'LeidianHost', 'LeidianInstance'
12
- ]
1
+ from typing import TYPE_CHECKING
2
+
3
+ from .protocol import HostProtocol, Instance, AdbHostConfig, WindowsHostConfig, RemoteWindowsHostConfig
4
+ if TYPE_CHECKING:
5
+ from .custom import CustomInstance, create as create_custom
6
+ from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
7
+ from .leidian_host import LeidianHost, LeidianInstance
8
+
9
+ def _require_custom():
10
+ global CustomInstance, create_custom
11
+ from .custom import CustomInstance, create as create_custom
12
+
13
+ def _require_mumu12():
14
+ global Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
15
+ from .mumu12_host import Mumu12Host, Mumu12Instance, Mumu12V5Host, Mumu12V5Instance
16
+
17
+ def _require_leidian():
18
+ global LeidianHost, LeidianInstance
19
+ from .leidian_host import LeidianHost, LeidianInstance
20
+
21
+ _IMPORT_NAMES = [
22
+ (_require_custom, ['CustomInstance', 'create_custom']),
23
+ (_require_mumu12, ['Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance']),
24
+ (_require_leidian, ['LeidianHost', 'LeidianInstance']),
25
+ ]
26
+
27
+ def __getattr__(name: str):
28
+ for item in _IMPORT_NAMES:
29
+ if name in item[1]:
30
+ item[0]()
31
+ break
32
+ try:
33
+ return globals()[name]
34
+ except KeyError:
35
+ raise AttributeError(name=name)
36
+
37
+ __all__ = [
38
+ 'HostProtocol', 'Instance',
39
+ 'AdbHostConfig', 'WindowsHostConfig', 'RemoteWindowsHostConfig',
40
+ 'CustomInstance', 'create_custom',
41
+ 'Mumu12Host', 'Mumu12Instance', 'Mumu12V5Host', 'Mumu12V5Instance',
42
+ 'LeidianHost', 'LeidianInstance'
43
+ ]
@@ -1,94 +1,107 @@
1
- from abc import ABC
2
- from typing import Any, Literal, TypeGuard, TypeVar, get_args
3
- from typing_extensions import assert_never
4
-
5
- from adbutils import adb
6
- from adbutils._device import AdbDevice
7
- from kotonebot import logging
8
- from kotonebot.client.device import AndroidDevice
9
- from .protocol import Instance, AdbHostConfig, Device
10
-
11
- logger = logging.getLogger(__name__)
12
- AdbRecipes = Literal['adb', 'adb_raw', 'uiautomator2']
13
-
14
- def is_adb_recipe(recipe: Any) -> TypeGuard[AdbRecipes]:
15
- return recipe in get_args(AdbRecipes)
16
-
17
- def connect_adb(
18
- ip: str,
19
- port: int,
20
- connect: bool = True,
21
- disconnect: bool = True,
22
- timeout: float = 180,
23
- device_serial: str | None = None
24
- ) -> AdbDevice:
25
- """
26
- 创建 ADB 连接。
27
- """
28
- if disconnect:
29
- logger.debug('adb disconnect %s:%d', ip, port)
30
- adb.disconnect(f'{ip}:{port}')
31
- if connect:
32
- logger.debug('adb connect %s:%d', ip, port)
33
- result = adb.connect(f'{ip}:{port}')
34
- if 'cannot connect to' in result:
35
- raise ValueError(result)
36
- serial = device_serial or f'{ip}:{port}'
37
- logger.debug('adb wait for %s', serial)
38
- adb.wait_for(serial, timeout=timeout)
39
- devices = adb.device_list()
40
- logger.debug('adb device_list: %s', devices)
41
- d = [d for d in devices if d.serial == serial]
42
- if len(d) == 0:
43
- raise ValueError(f"Device {serial} not found")
44
- d = d[0]
45
- return d
46
-
47
- class CommonAdbCreateDeviceMixin(ABC):
48
- """
49
- 通用 ADB 创建设备的 Mixin。
50
- Mixin 定义了创建 ADB 设备的通用接口。
51
- """
52
- def __init__(self, *args, **kwargs) -> None:
53
- super().__init__(*args, **kwargs)
54
- # 下面的属性只是为了让类型检查通过,无实际实现
55
- self.adb_ip: str
56
- self.adb_port: int
57
- self.adb_name: str
58
-
59
- def create_device(self, recipe: AdbRecipes, config: AdbHostConfig) -> Device:
60
- """
61
- 创建 ADB 设备。
62
- """
63
- connection = connect_adb(
64
- self.adb_ip,
65
- self.adb_port,
66
- connect=True,
67
- disconnect=True,
68
- timeout=config.timeout,
69
- device_serial=self.adb_name
70
- )
71
- d = AndroidDevice(connection)
72
- match recipe:
73
- case 'adb':
74
- from kotonebot.client.implements.adb import AdbImpl
75
- impl = AdbImpl(connection)
76
- d._screenshot = impl
77
- d._touch = impl
78
- d.commands = impl
79
- case 'adb_raw':
80
- from kotonebot.client.implements.adb_raw import AdbRawImpl
81
- impl = AdbRawImpl(connection)
82
- d._screenshot = impl
83
- d._touch = impl
84
- d.commands = impl
85
- case 'uiautomator2':
86
- from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
87
- from kotonebot.client.implements.adb import AdbImpl
88
- impl = UiAutomator2Impl(connection)
89
- d._screenshot = impl
90
- d._touch = impl
91
- d.commands = AdbImpl(connection)
92
- case _:
93
- assert_never(f'Unsupported ADB recipe: {recipe}')
94
- return d
1
+ from abc import ABC
2
+ from typing import Any, Literal, TypeGuard, TypeVar, get_args
3
+ from typing_extensions import assert_never
4
+
5
+ try:
6
+ from adbutils import adb
7
+ from adbutils._device import AdbDevice
8
+ except ImportError as _e:
9
+ from kotonebot.errors import MissingDependencyError
10
+ raise MissingDependencyError(_e, 'android')
11
+ from kotonebot import logging
12
+ from kotonebot.client.device import AndroidDevice
13
+ from .protocol import Instance, AdbHostConfig, Device
14
+
15
+ logger = logging.getLogger(__name__)
16
+ AdbRecipes = Literal['adb', 'adb_raw', 'uiautomator2']
17
+
18
+ def is_adb_recipe(recipe: Any) -> TypeGuard[AdbRecipes]:
19
+ return recipe in get_args(AdbRecipes)
20
+
21
+ def connect_adb(
22
+ ip: str,
23
+ port: int,
24
+ connect: bool = True,
25
+ disconnect: bool = True,
26
+ timeout: float = 180,
27
+ device_serial: str | None = None
28
+ ) -> AdbDevice:
29
+ """
30
+ 创建 ADB 连接。
31
+ """
32
+ if disconnect:
33
+ logger.debug('adb disconnect %s:%d', ip, port)
34
+ adb.disconnect(f'{ip}:{port}')
35
+ else:
36
+ logger.debug('Skip adb disconnect.')
37
+ if connect:
38
+ logger.debug('adb connect %s:%d', ip, port)
39
+ result = adb.connect(f'{ip}:{port}')
40
+ if 'cannot connect to' in result:
41
+ raise ValueError(result)
42
+ else:
43
+ logger.debug('Skip adb connect.')
44
+ serial = device_serial or f'{ip}:{port}'
45
+ logger.debug('adb wait for %s', serial)
46
+ adb.wait_for(serial, timeout=timeout)
47
+ devices = adb.device_list()
48
+ logger.debug('adb device_list: %s', devices)
49
+ d = [d for d in devices if d.serial == serial]
50
+ if len(d) == 0:
51
+ raise ValueError(f"Device {serial} not found")
52
+ d = d[0]
53
+ return d
54
+
55
+ class CommonAdbCreateDeviceMixin(ABC):
56
+ """
57
+ 通用 ADB 创建设备的 Mixin。
58
+ 该 Mixin 定义了创建 ADB 设备的通用接口。
59
+ """
60
+ def __init__(self, *args, **kwargs) -> None:
61
+ super().__init__(*args, **kwargs)
62
+ # 下面的属性只是为了让类型检查通过,无实际实现
63
+ self.adb_ip: str
64
+ self.adb_port: int
65
+ self.adb_name: str
66
+
67
+ def create_device(self, recipe: AdbRecipes, config: AdbHostConfig, *, connect: bool = True, disconnect: bool = True) -> Device:
68
+ """
69
+ 创建 ADB 设备。
70
+
71
+ :param recipe: 连接方式配方名称。
72
+ :param config: ADB 配置。
73
+ :param connect: 创建设备实例前,是否连接 ADB(执行 adb connect)。
74
+ :param disconnect: 创建设备实例前,是否先断开已有 ADB 连接(执行 adb disconnect)。
75
+ """
76
+ connection = connect_adb(
77
+ self.adb_ip,
78
+ self.adb_port,
79
+ connect=connect,
80
+ disconnect=disconnect,
81
+ timeout=config.timeout,
82
+ device_serial=self.adb_name
83
+ )
84
+ d = AndroidDevice(connection)
85
+ match recipe:
86
+ case 'adb':
87
+ from kotonebot.client.implements.adb import AdbImpl
88
+ impl = AdbImpl(connection)
89
+ d._screenshot = impl
90
+ d._touch = impl
91
+ d.commands = impl
92
+ case 'adb_raw':
93
+ from kotonebot.client.implements.adb_raw import AdbRawImpl
94
+ impl = AdbRawImpl(connection)
95
+ d._screenshot = impl
96
+ d._touch = impl
97
+ d.commands = impl
98
+ case 'uiautomator2':
99
+ from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
100
+ from kotonebot.client.implements.adb import AdbImpl
101
+ impl = UiAutomator2Impl(connection)
102
+ d._screenshot = impl
103
+ d._touch = impl
104
+ d.commands = AdbImpl(connection)
105
+ case _:
106
+ assert_never(f'Unsupported ADB recipe: {recipe}')
107
+ return d
@@ -1,114 +1,118 @@
1
- import os
2
- import subprocess
3
- from psutil import process_iter
4
- from .protocol import Instance, AdbHostConfig, HostProtocol
5
- from typing import ParamSpec, TypeVar
6
- from typing_extensions import override
7
-
8
- from kotonebot import logging
9
- from kotonebot.client import Device
10
- from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
11
-
12
- logger = logging.getLogger(__name__)
13
- CustomRecipes = AdbRecipes
14
-
15
- P = ParamSpec('P')
16
- T = TypeVar('T')
17
-
18
- class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
19
- def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
20
- super().__init__(*args, **kwargs)
21
- self.exe_path: str | None = exe_path
22
- self.exe_args: str = emulator_args
23
- self.process: subprocess.Popen | None = None
24
-
25
- @override
26
- def start(self):
27
- if self.process:
28
- logger.warning('Process is already running.')
29
- return
30
-
31
- if not self.exe_path:
32
- raise ValueError('Executable path is not set.')
33
- if self.exe_args:
34
- logger.info('Starting process "%s" with args "%s"...', self.exe_path, self.exe_args)
35
- cmd = f'"{self.exe_path}" {self.exe_args}'
36
- self.process = subprocess.Popen(cmd, shell=True)
37
- else:
38
- logger.info('Starting process "%s"...', self.exe_path)
39
- self.process = subprocess.Popen(self.exe_path)
40
-
41
- @override
42
- def stop(self):
43
- if not self.process:
44
- logger.warning('Process is not running.')
45
- return
46
- logger.info('Stopping process "%s"...', self.process.pid)
47
- self.process.terminate()
48
- self.process.wait()
49
- self.process = None
50
-
51
- @override
52
- def running(self) -> bool:
53
- if self.process is not None:
54
- return True
55
- else:
56
- if not self.exe_path:
57
- logger.warning('Executable path is not set, cannot check if process is running.')
58
- return False
59
- process_name = os.path.basename(self.exe_path)
60
- p = next((proc for proc in process_iter() if proc.name() == process_name), None)
61
- if p:
62
- return True
63
- else:
64
- return False
65
-
66
- @override
67
- def refresh(self):
68
- pass
69
-
70
- @override
71
- def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
72
- """为自定义实例创建 Device。"""
73
- if self.adb_port is None:
74
- raise ValueError("ADB port is not set and is required.")
75
-
76
- return super().create_device(impl, host_config)
77
-
78
- def __repr__(self) -> str:
79
- return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
80
-
81
- def _type_check(ins: Instance) -> CustomInstance:
82
- if not isinstance(ins, CustomInstance):
83
- raise ValueError(f'Instance {ins} is not a CustomInstance')
84
- return ins
85
-
86
- def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
87
- return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
88
-
89
- class CustomHost(HostProtocol[CustomRecipes]):
90
- @staticmethod
91
- def installed() -> bool:
92
- # Custom instances don't have a specific installation requirement
93
- return True
94
-
95
- @staticmethod
96
- def list() -> list[Instance]:
97
- # Custom instances are created manually, not discovered
98
- return []
99
-
100
- @staticmethod
101
- def query(*, id: str) -> Instance | None:
102
- # Custom instances are created manually, not discovered
103
- return None
104
-
105
- @staticmethod
106
- def recipes() -> 'list[CustomRecipes]':
107
- return ['adb', 'adb_raw', 'uiautomator2']
108
-
109
- if __name__ == '__main__':
110
- ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
111
- ins.start()
112
- ins.wait_available()
113
- input('Press Enter to stop...')
114
- ins.stop()
1
+ import os
2
+ import subprocess
3
+ try:
4
+ from psutil import process_iter
5
+ except ImportError as _e:
6
+ from kotonebot.errors import MissingDependencyError
7
+ raise MissingDependencyError(_e, 'windows')
8
+ from .protocol import Instance, AdbHostConfig, HostProtocol
9
+ from typing import ParamSpec, TypeVar
10
+ from typing_extensions import override
11
+
12
+ from kotonebot import logging
13
+ from kotonebot.client import Device
14
+ from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
15
+
16
+ logger = logging.getLogger(__name__)
17
+ CustomRecipes = AdbRecipes
18
+
19
+ P = ParamSpec('P')
20
+ T = TypeVar('T')
21
+
22
+ class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
23
+ def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
24
+ super().__init__(*args, **kwargs)
25
+ self.exe_path: str | None = exe_path
26
+ self.exe_args: str = emulator_args
27
+ self.process: subprocess.Popen | None = None
28
+
29
+ @override
30
+ def start(self):
31
+ if self.process:
32
+ logger.warning('Process is already running.')
33
+ return
34
+
35
+ if not self.exe_path:
36
+ raise ValueError('Executable path is not set.')
37
+ if self.exe_args:
38
+ logger.info('Starting process "%s" with args "%s"...', self.exe_path, self.exe_args)
39
+ cmd = f'"{self.exe_path}" {self.exe_args}'
40
+ self.process = subprocess.Popen(cmd, shell=True)
41
+ else:
42
+ logger.info('Starting process "%s"...', self.exe_path)
43
+ self.process = subprocess.Popen(self.exe_path)
44
+
45
+ @override
46
+ def stop(self):
47
+ if not self.process:
48
+ logger.warning('Process is not running.')
49
+ return
50
+ logger.info('Stopping process "%s"...', self.process.pid)
51
+ self.process.terminate()
52
+ self.process.wait()
53
+ self.process = None
54
+
55
+ @override
56
+ def running(self) -> bool:
57
+ if self.process is not None:
58
+ return True
59
+ else:
60
+ if not self.exe_path:
61
+ logger.warning('Executable path is not set, cannot check if process is running.')
62
+ return False
63
+ process_name = os.path.basename(self.exe_path)
64
+ p = next((proc for proc in process_iter() if proc.name() == process_name), None)
65
+ if p:
66
+ return True
67
+ else:
68
+ return False
69
+
70
+ @override
71
+ def refresh(self):
72
+ pass
73
+
74
+ @override
75
+ def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
76
+ """为自定义实例创建 Device。"""
77
+ if self.adb_port is None:
78
+ raise ValueError("ADB port is not set and is required.")
79
+
80
+ return super().create_device(impl, host_config)
81
+
82
+ def __repr__(self) -> str:
83
+ return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
84
+
85
+ def _type_check(ins: Instance) -> CustomInstance:
86
+ if not isinstance(ins, CustomInstance):
87
+ raise ValueError(f'Instance {ins} is not a CustomInstance')
88
+ return ins
89
+
90
+ def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
91
+ return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
92
+
93
+ class CustomHost(HostProtocol[CustomRecipes]):
94
+ @staticmethod
95
+ def installed() -> bool:
96
+ # Custom instances don't have a specific installation requirement
97
+ return True
98
+
99
+ @staticmethod
100
+ def list() -> list[Instance]:
101
+ # Custom instances are created manually, not discovered
102
+ return []
103
+
104
+ @staticmethod
105
+ def query(*, id: str) -> Instance | None:
106
+ # Custom instances are created manually, not discovered
107
+ return None
108
+
109
+ @staticmethod
110
+ def recipes() -> 'list[CustomRecipes]':
111
+ return ['adb', 'adb_raw', 'uiautomator2']
112
+
113
+ if __name__ == '__main__':
114
+ ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
115
+ ins.start()
116
+ ins.wait_available()
117
+ input('Press Enter to stop...')
118
+ ins.stop()