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,213 +1,214 @@
1
- import time
2
- import socket
3
- from abc import ABC, abstractmethod
4
- from typing import Callable, TypeVar, Protocol, Any, Generic
5
- from dataclasses import dataclass
6
-
7
- from adbutils import adb, AdbTimeout, AdbError
8
- from adbutils._device import AdbDevice
9
-
10
- from kotonebot import logging
11
- from kotonebot.client import Device, DeviceImpl
12
-
13
- from kotonebot.util import Countdown, Interval
14
-
15
- logger = logging.getLogger(__name__)
16
- # https://github.com/python/typing/issues/769#issuecomment-903760354
17
- _T = TypeVar("_T")
18
- def copy_type(_: _T) -> Callable[[Any], _T]:
19
- return lambda x: x
20
-
21
- # --- 定义专用的 HostConfig 数据类 ---
22
- @dataclass
23
- class AdbHostConfig:
24
- """由外部为基于 ADB 的主机提供的配置。"""
25
- timeout: float = 180
26
-
27
- @dataclass
28
- class WindowsHostConfig:
29
- """由外部为 Windows 实现提供配置。"""
30
- window_title: str
31
- ahk_exe_path: str
32
-
33
- @dataclass
34
- class RemoteWindowsHostConfig:
35
- """由外部为远程 Windows 实现提供配置。"""
36
- windows_host_config: WindowsHostConfig
37
- host: str
38
- port: int
39
-
40
- # --- 使用泛型改造 Instance 协议 ---
41
- T_HostConfig = TypeVar("T_HostConfig")
42
-
43
- def tcp_ping(host: str, port: int, timeout: float = 1.0) -> bool:
44
- """
45
- 通过 TCP ping 检查主机和端口是否可达。
46
-
47
- :param host: 主机名或 IP 地址
48
- :param port: 端口号
49
- :param timeout: 超时时间(秒)
50
- :return: 如果主机和端口可达,则返回 True,否则返回 False
51
- """
52
- logger.debug('TCP ping %s:%d...', host, port)
53
- try:
54
- with socket.create_connection((host, port), timeout):
55
- logger.debug('TCP ping %s:%d success.', host, port)
56
- return True
57
- except (socket.timeout, ConnectionRefusedError, OSError):
58
- logger.debug('TCP ping %s:%d failed.', host, port)
59
- return False
60
-
61
-
62
- class Instance(Generic[T_HostConfig], ABC):
63
- """
64
- 代表一个可运行环境的实例(如一个模拟器)。
65
- 使用泛型来约束 create_device 方法的配置参数类型。
66
- """
67
- def __init__(self,
68
- id: str,
69
- name: str,
70
- adb_port: int | None = None,
71
- adb_ip: str = '127.0.0.1',
72
- adb_name: str | None = None
73
- ):
74
- self.id: str = id
75
- self.name: str = name
76
- self.adb_port: int | None = adb_port
77
- self.adb_ip: str = adb_ip
78
- self.adb_name: str | None = adb_name
79
-
80
- def require_adb_port(self) -> int:
81
- if self.adb_port is None:
82
- raise ValueError("ADB port is not set and is required.")
83
- return self.adb_port
84
-
85
- @abstractmethod
86
- def refresh(self):
87
- """
88
- 刷新实例信息,如 ADB 端口号等。
89
- """
90
- raise NotImplementedError()
91
-
92
- @abstractmethod
93
- def start(self):
94
- """
95
- 启动模拟器实例。
96
- """
97
- raise NotImplementedError()
98
-
99
- @abstractmethod
100
- def stop(self):
101
- """
102
- 停止模拟器实例。
103
- """
104
- raise NotImplementedError()
105
-
106
- @abstractmethod
107
- def running(self) -> bool:
108
- raise NotImplementedError()
109
-
110
- @abstractmethod
111
- def create_device(self, impl: DeviceImpl, host_config: T_HostConfig) -> Device:
112
- """
113
- 根据实现名称和类型化的主机配置创建设备。
114
-
115
- :param impl: 设备实现的名称。
116
- :param host_config: 一个类型化的数据对象,包含创建所需的所有外部配置。
117
- :return: 配置好的 Device 实例。
118
- """
119
- raise NotImplementedError()
120
-
121
- def wait_available(self, timeout: float = 180):
122
- logger.info('Starting to wait for emulator %s(127.0.0.1:%d) to be available...', self.name, self.adb_port)
123
- state = 0
124
- port = self.require_adb_port()
125
- emulator_name = self.adb_name
126
- cd = Countdown(timeout)
127
- it = Interval(1)
128
- d: AdbDevice | None = None
129
- while True:
130
- if cd.expired():
131
- raise TimeoutError(f'Emulator "{self.name}" is not available.')
132
- it.wait()
133
- try:
134
- match state:
135
- case 0:
136
- logger.debug('Ping emulator %s(127.0.0.1:%d)...', self.name, port)
137
- if tcp_ping('127.0.0.1', port):
138
- logger.debug('Ping emulator %s(127.0.0.1:%d) success.', self.name, port)
139
- state = 1
140
- case 1:
141
- logger.debug('Connecting to emulator %s(127.0.0.1:%d)...', self.name, port)
142
- if adb.connect(f'127.0.0.1:{port}', timeout=0.5):
143
- logger.debug('Connect to emulator %s(127.0.0.1:%d) success.', self.name, port)
144
- state = 2
145
- case 2:
146
- logger.debug('Getting device list...')
147
- if devices := adb.device_list():
148
- logger.debug('Get device list success. devices=%s', devices)
149
- # emulator_name 用于适配雷电模拟器
150
- # 雷电模拟器启动后,在上方的列表中并不会出现 127.0.0.1:5555,而是 emulator-5554
151
- d = next(
152
- (d for d in devices if d.serial == f'127.0.0.1:{port}' or d.serial == emulator_name),
153
- None
154
- )
155
- if d:
156
- logger.debug('Get target device success. d=%s', d)
157
- state = 3
158
- case 3:
159
- if not d:
160
- logger.warning('Device is None.')
161
- state = 0
162
- continue
163
- logger.debug('Waiting for device state...')
164
- if d.get_state() == 'device':
165
- logger.debug('Device state ready. state=%s', d.get_state())
166
- state = 4
167
- case 4:
168
- logger.debug('Waiting for device boot completed...')
169
- if not d:
170
- logger.warning('Device is None.')
171
- state = 0
172
- continue
173
- ret = d.shell('getprop sys.boot_completed')
174
- if isinstance(ret, str) and ret.strip() == '1':
175
- logger.debug('Device boot completed. ret=%s', ret)
176
- state = 5
177
- case 5:
178
- if not d:
179
- logger.warning('Device is None.')
180
- state = 0
181
- continue
182
- app = d.app_current()
183
- logger.debug('Waiting for launcher... (current=%s)', app)
184
- if app and 'launcher' in app.package:
185
- logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
186
- state = 6
187
- case 6:
188
- break
189
- except (AdbError, AdbTimeout):
190
- state = 1
191
- continue
192
- time.sleep(1)
193
- logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
194
-
195
- def __repr__(self) -> str:
196
- return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
197
-
198
- Recipe = TypeVar('Recipe', bound=str)
199
- class HostProtocol(Generic[Recipe], Protocol):
200
- @staticmethod
201
- def installed() -> bool: ...
202
-
203
- @staticmethod
204
- def list() -> list[Instance]: ...
205
-
206
- @staticmethod
207
- def query(*, id: str) -> Instance | None: ...
208
-
209
- @staticmethod
210
- def recipes() -> 'list[Recipe]': ...
211
-
212
- if __name__ == '__main__':
213
- pass
1
+ import time
2
+ import socket
3
+ from abc import ABC, abstractmethod
4
+ from typing import Callable, TypeVar, Protocol, Any, Generic
5
+ from dataclasses import dataclass
6
+
7
+ from kotonebot import logging
8
+ from kotonebot.client import Device, DeviceImpl
9
+
10
+ from kotonebot.util import Countdown, Interval
11
+
12
+ logger = logging.getLogger(__name__)
13
+ # https://github.com/python/typing/issues/769#issuecomment-903760354
14
+ _T = TypeVar("_T")
15
+ def copy_type(_: _T) -> Callable[[Any], _T]:
16
+ return lambda x: x
17
+
18
+ # --- 定义专用的 HostConfig 数据类 ---
19
+ @dataclass
20
+ class AdbHostConfig:
21
+ """由外部为基于 ADB 的主机提供的配置。"""
22
+ timeout: float = 180
23
+
24
+ @dataclass
25
+ class WindowsHostConfig:
26
+ """由外部为 Windows 实现提供配置。"""
27
+ window_title: str
28
+ ahk_exe_path: str
29
+
30
+ @dataclass
31
+ class RemoteWindowsHostConfig:
32
+ """由外部为远程 Windows 实现提供配置。"""
33
+ windows_host_config: WindowsHostConfig
34
+ host: str
35
+ port: int
36
+
37
+ # --- 使用泛型改造 Instance 协议 ---
38
+ T_HostConfig = TypeVar("T_HostConfig")
39
+
40
+ def tcp_ping(host: str, port: int, timeout: float = 1.0) -> bool:
41
+ """
42
+ 通过 TCP ping 检查主机和端口是否可达。
43
+
44
+ :param host: 主机名或 IP 地址
45
+ :param port: 端口号
46
+ :param timeout: 超时时间(秒)
47
+ :return: 如果主机和端口可达,则返回 True,否则返回 False
48
+ """
49
+ logger.debug('TCP ping %s:%d...', host, port)
50
+ try:
51
+ with socket.create_connection((host, port), timeout):
52
+ logger.debug('TCP ping %s:%d success.', host, port)
53
+ return True
54
+ except (socket.timeout, ConnectionRefusedError, OSError):
55
+ logger.debug('TCP ping %s:%d failed.', host, port)
56
+ return False
57
+
58
+
59
+ class Instance(Generic[T_HostConfig], ABC):
60
+ """
61
+ 代表一个可运行环境的实例(如一个模拟器)。
62
+ 使用泛型来约束 create_device 方法的配置参数类型。
63
+ """
64
+ def __init__(self,
65
+ id: str,
66
+ name: str,
67
+ adb_port: int | None = None,
68
+ adb_ip: str = '127.0.0.1',
69
+ adb_name: str | None = None
70
+ ):
71
+ self.id: str = id
72
+ self.name: str = name
73
+ self.adb_port: int | None = adb_port
74
+ self.adb_ip: str = adb_ip
75
+ self.adb_name: str | None = adb_name
76
+
77
+ def require_adb_port(self) -> int:
78
+ if self.adb_port is None:
79
+ raise ValueError("ADB port is not set and is required.")
80
+ return self.adb_port
81
+
82
+ @abstractmethod
83
+ def refresh(self):
84
+ """
85
+ 刷新实例信息,如 ADB 端口号等。
86
+ """
87
+ raise NotImplementedError()
88
+
89
+ @abstractmethod
90
+ def start(self):
91
+ """
92
+ 启动模拟器实例。
93
+ """
94
+ raise NotImplementedError()
95
+
96
+ @abstractmethod
97
+ def stop(self):
98
+ """
99
+ 停止模拟器实例。
100
+ """
101
+ raise NotImplementedError()
102
+
103
+ @abstractmethod
104
+ def running(self) -> bool:
105
+ raise NotImplementedError()
106
+
107
+ @abstractmethod
108
+ def create_device(self, impl: DeviceImpl, host_config: T_HostConfig) -> Device:
109
+ """
110
+ 根据实现名称和类型化的主机配置创建设备。
111
+
112
+ :param impl: 设备实现的名称。
113
+ :param host_config: 一个类型化的数据对象,包含创建所需的所有外部配置。
114
+ :return: 配置好的 Device 实例。
115
+ """
116
+ raise NotImplementedError()
117
+
118
+ # TODO: [refactor] 这个方法不应该挂在 Instance,而是 AndroidEmulatorInstance 上
119
+ def wait_available(self, timeout: float = 180):
120
+ from adbutils import adb, AdbTimeout, AdbError
121
+ from adbutils._device import AdbDevice
122
+
123
+ logger.info('Starting to wait for emulator %s(127.0.0.1:%d) to be available...', self.name, self.adb_port)
124
+ state = 0
125
+ port = self.require_adb_port()
126
+ emulator_name = self.adb_name
127
+ cd = Countdown(timeout)
128
+ it = Interval(1)
129
+ d: AdbDevice | None = None
130
+ while True:
131
+ if cd.expired():
132
+ raise TimeoutError(f'Emulator "{self.name}" is not available.')
133
+ it.wait()
134
+ try:
135
+ match state:
136
+ case 0:
137
+ logger.debug('Ping emulator %s(127.0.0.1:%d)...', self.name, port)
138
+ if tcp_ping('127.0.0.1', port):
139
+ logger.debug('Ping emulator %s(127.0.0.1:%d) success.', self.name, port)
140
+ state = 1
141
+ case 1:
142
+ logger.debug('Connecting to emulator %s(127.0.0.1:%d)...', self.name, port)
143
+ if adb.connect(f'127.0.0.1:{port}', timeout=0.5):
144
+ logger.debug('Connect to emulator %s(127.0.0.1:%d) success.', self.name, port)
145
+ state = 2
146
+ case 2:
147
+ logger.debug('Getting device list...')
148
+ if devices := adb.device_list():
149
+ logger.debug('Get device list success. devices=%s', devices)
150
+ # emulator_name 用于适配雷电模拟器
151
+ # 雷电模拟器启动后,在上方的列表中并不会出现 127.0.0.1:5555,而是 emulator-5554
152
+ d = next(
153
+ (d for d in devices if d.serial == f'127.0.0.1:{port}' or d.serial == emulator_name),
154
+ None
155
+ )
156
+ if d:
157
+ logger.debug('Get target device success. d=%s', d)
158
+ state = 3
159
+ case 3:
160
+ if not d:
161
+ logger.warning('Device is None.')
162
+ state = 0
163
+ continue
164
+ logger.debug('Waiting for device state...')
165
+ if d.get_state() == 'device':
166
+ logger.debug('Device state ready. state=%s', d.get_state())
167
+ state = 4
168
+ case 4:
169
+ logger.debug('Waiting for device boot completed...')
170
+ if not d:
171
+ logger.warning('Device is None.')
172
+ state = 0
173
+ continue
174
+ ret = d.shell('getprop sys.boot_completed')
175
+ if isinstance(ret, str) and ret.strip() == '1':
176
+ logger.debug('Device boot completed. ret=%s', ret)
177
+ state = 5
178
+ case 5:
179
+ if not d:
180
+ logger.warning('Device is None.')
181
+ state = 0
182
+ continue
183
+ app = d.app_current()
184
+ logger.debug('Waiting for launcher... (current=%s)', app)
185
+ if app and 'launcher' in app.package:
186
+ logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
187
+ state = 6
188
+ case 6:
189
+ break
190
+ except (AdbError, AdbTimeout):
191
+ state = 1
192
+ continue
193
+ time.sleep(1)
194
+ logger.info('Emulator %s(127.0.0.1:%d) now is available.', self.name, self.adb_port)
195
+
196
+ def __repr__(self) -> str:
197
+ return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
198
+
199
+ Recipe = TypeVar('Recipe', bound=str)
200
+ class HostProtocol(Generic[Recipe], Protocol):
201
+ @staticmethod
202
+ def installed() -> bool: ...
203
+
204
+ @staticmethod
205
+ def list() -> list[Instance]: ...
206
+
207
+ @staticmethod
208
+ def query(*, id: str) -> Instance | None: ...
209
+
210
+ @staticmethod
211
+ def recipes() -> 'list[Recipe]': ...
212
+
213
+ if __name__ == '__main__':
214
+ pass
@@ -1,55 +1,58 @@
1
- from abc import ABC
2
- from typing import Literal
3
- from typing_extensions import assert_never
4
-
5
- from kotonebot import logging
6
- from kotonebot.client.device import WindowsDevice
7
- from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
8
-
9
- logger = logging.getLogger(__name__)
10
- WindowsRecipes = Literal['windows', 'remote_windows']
11
-
12
- # Windows 相关的配置类型联合
13
- WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
14
-
15
- class CommonWindowsCreateDeviceMixin(ABC):
16
- """
17
- 通用 Windows 创建设备的 Mixin。
18
- Mixin 定义了创建 Windows 设备的通用接口。
19
- """
20
- def __init__(self, *args, **kwargs) -> None:
21
- super().__init__(*args, **kwargs)
22
-
23
- def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
24
- """
25
- 创建 Windows 设备。
26
- """
27
- match recipe:
28
- case 'windows':
29
- if not isinstance(config, WindowsHostConfig):
30
- raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
31
- from kotonebot.client.implements.windows import WindowsImpl
32
- d = WindowsDevice()
33
- impl = WindowsImpl(
34
- device=d,
35
- window_title=config.window_title,
36
- ahk_exe_path=config.ahk_exe_path
37
- )
38
- d._screenshot = impl
39
- d._touch = impl
40
- return d
41
- case 'remote_windows':
42
- if not isinstance(config, RemoteWindowsHostConfig):
43
- raise ValueError(f"Expected RemoteWindowsHostConfig for 'remote_windows' recipe, got {type(config)}")
44
- from kotonebot.client.implements.remote_windows import RemoteWindowsImpl
45
- d = WindowsDevice()
46
- impl = RemoteWindowsImpl(
47
- device=d,
48
- host=config.host,
49
- port=config.port
50
- )
51
- d._screenshot = impl
52
- d._touch = impl
53
- return d
54
- case _:
55
- assert_never(f'Unsupported Windows recipe: {recipe}')
1
+ from abc import ABC
2
+ from typing import Literal
3
+ from typing_extensions import assert_never
4
+
5
+ from kotonebot import logging
6
+ from kotonebot.client.device import WindowsDevice
7
+ from kotonebot.util import require_windows
8
+ from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
9
+
10
+ logger = logging.getLogger(__name__)
11
+ WindowsRecipes = Literal['windows', 'remote_windows']
12
+
13
+ # Windows 相关的配置类型联合
14
+ WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
15
+
16
+ class CommonWindowsCreateDeviceMixin(ABC):
17
+ """
18
+ 通用 Windows 创建设备的 Mixin。
19
+ 该 Mixin 定义了创建 Windows 设备的通用接口。
20
+ """
21
+ def __init__(self, *args, **kwargs) -> None:
22
+ super().__init__(*args, **kwargs)
23
+ require_windows('CommonWindowsCreateDeviceMixin', self.__class__)
24
+
25
+ def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
26
+ """
27
+ 创建 Windows 设备。
28
+ """
29
+ require_windows('CommonWindowsCreateDeviceMixin.create_device', self.__class__)
30
+ match recipe:
31
+ case 'windows':
32
+ if not isinstance(config, WindowsHostConfig):
33
+ raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
34
+ from kotonebot.client.implements.windows import WindowsImpl
35
+ d = WindowsDevice()
36
+ impl = WindowsImpl(
37
+ device=d,
38
+ window_title=config.window_title,
39
+ ahk_exe_path=config.ahk_exe_path
40
+ )
41
+ d._screenshot = impl
42
+ d._touch = impl
43
+ return d
44
+ case 'remote_windows':
45
+ if not isinstance(config, RemoteWindowsHostConfig):
46
+ raise ValueError(f"Expected RemoteWindowsHostConfig for 'remote_windows' recipe, got {type(config)}")
47
+ from kotonebot.client.implements.remote_windows import RemoteWindowsImpl
48
+ d = WindowsDevice()
49
+ impl = RemoteWindowsImpl(
50
+ device=d,
51
+ host=config.host,
52
+ port=config.port
53
+ )
54
+ d._screenshot = impl
55
+ d._touch = impl
56
+ return d
57
+ case _:
58
+ assert_never(f'Unsupported Windows recipe: {recipe}')
@@ -1,7 +1,71 @@
1
- # 导入所有内置实现,以触发它们的 @register_impl 装饰器
2
- from . import adb # noqa: F401
3
- from . import adb_raw # noqa: F401
4
- from . import remote_windows # noqa: F401
5
- from . import uiautomator2 # noqa: F401
6
- from . import windows # noqa: F401
7
- from . import nemu_ipc # noqa: F401
1
+ from typing import TYPE_CHECKING
2
+ from kotonebot.util import is_windows, require_windows
3
+
4
+ if TYPE_CHECKING:
5
+ from .adb import AdbImpl, AdbImplConfig
6
+ from .adb_raw import AdbRawImpl
7
+ from .uiautomator2 import UiAutomator2Impl
8
+ from .windows import WindowsImpl, WindowsImplConfig
9
+ from .remote_windows import RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
10
+ from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
11
+
12
+
13
+ def _require_windows():
14
+ global WindowsImpl, WindowsImplConfig
15
+ global RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
16
+ global NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
17
+
18
+ if not is_windows():
19
+ require_windows('"windows", "remote_windows" and "nemu_ipc" implementations')
20
+ from .windows import WindowsImpl, WindowsImplConfig
21
+ from .remote_windows import RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
22
+ from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
23
+
24
+ def _require_adb():
25
+ global AdbImpl, AdbImplConfig
26
+ global AdbRawImpl
27
+
28
+ from .adb import AdbImpl, AdbImplConfig
29
+ from .adb_raw import AdbRawImpl
30
+
31
+ def _require_uiautomator2():
32
+ global UiAutomator2Impl
33
+
34
+ from .uiautomator2 import UiAutomator2Impl
35
+
36
+ _IMPORT_NAMES = [
37
+ (_require_windows, [
38
+ 'WindowsImpl', 'WindowsImplConfig',
39
+ 'RemoteWindowsImpl', 'RemoteWindowsImplConfig', 'RemoteWindowsServer',
40
+ 'NemuIpcImpl', 'NemuIpcImplConfig', 'ExternalRendererIpc'
41
+ ]),
42
+ (_require_adb, [
43
+ 'AdbImpl', 'AdbImplConfig',
44
+ 'AdbRawImpl'
45
+ ]),
46
+ (_require_uiautomator2, [
47
+ 'UiAutomator2Impl'
48
+ ]),
49
+ ]
50
+
51
+
52
+ def __getattr__(name: str):
53
+ for item in _IMPORT_NAMES:
54
+ if name in item[1]:
55
+ item[0]()
56
+ break
57
+ try:
58
+ return globals()[name]
59
+ except KeyError:
60
+ raise AttributeError(name=name)
61
+
62
+ __all__ = [
63
+ # windows
64
+ 'WindowsImpl', 'WindowsImplConfig',
65
+ 'RemoteWindowsImpl', 'RemoteWindowsImplConfig', 'RemoteWindowsServer',
66
+ 'NemuIpcImpl', 'NemuIpcImplConfig', 'ExternalRendererIpc',
67
+ # android
68
+ 'AdbImpl', 'AdbImplConfig',
69
+ 'AdbRawImpl',
70
+ 'UiAutomator2Impl'
71
+ ]