kotonebot 0.4.0__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 (64) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/task_action.py +183 -183
  6. kotonebot/backend/core.py +129 -129
  7. kotonebot/backend/debug/entry.py +89 -89
  8. kotonebot/backend/debug/mock.py +78 -78
  9. kotonebot/backend/debug/server.py +222 -222
  10. kotonebot/backend/debug/vars.py +351 -351
  11. kotonebot/backend/dispatch.py +227 -227
  12. kotonebot/backend/flow_controller.py +196 -196
  13. kotonebot/backend/ocr.py +535 -529
  14. kotonebot/backend/preprocessor.py +103 -103
  15. kotonebot/client/__init__.py +9 -9
  16. kotonebot/client/device.py +528 -503
  17. kotonebot/client/fast_screenshot.py +377 -377
  18. kotonebot/client/host/__init__.py +43 -12
  19. kotonebot/client/host/adb_common.py +107 -103
  20. kotonebot/client/host/custom.py +118 -114
  21. kotonebot/client/host/leidian_host.py +196 -201
  22. kotonebot/client/host/mumu12_host.py +353 -358
  23. kotonebot/client/host/protocol.py +214 -213
  24. kotonebot/client/host/windows_common.py +58 -58
  25. kotonebot/client/implements/__init__.py +71 -15
  26. kotonebot/client/implements/adb.py +89 -85
  27. kotonebot/client/implements/adb_raw.py +162 -158
  28. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  29. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  30. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  31. kotonebot/client/implements/remote_windows.py +188 -188
  32. kotonebot/client/implements/uiautomator2.py +85 -81
  33. kotonebot/client/implements/windows.py +176 -172
  34. kotonebot/client/protocol.py +69 -69
  35. kotonebot/client/registration.py +24 -24
  36. kotonebot/config/base_config.py +96 -96
  37. kotonebot/config/manager.py +36 -36
  38. kotonebot/errors.py +76 -71
  39. kotonebot/interop/win/__init__.py +10 -3
  40. kotonebot/interop/win/_mouse.py +311 -0
  41. kotonebot/interop/win/message_box.py +313 -313
  42. kotonebot/interop/win/reg.py +37 -37
  43. kotonebot/interop/win/shortcut.py +43 -43
  44. kotonebot/interop/win/task_dialog.py +513 -513
  45. kotonebot/logging/__init__.py +2 -2
  46. kotonebot/logging/log.py +17 -17
  47. kotonebot/primitives/__init__.py +17 -17
  48. kotonebot/primitives/geometry.py +862 -290
  49. kotonebot/primitives/visual.py +63 -63
  50. kotonebot/tools/mirror.py +354 -354
  51. kotonebot/ui/file_host/sensio.py +36 -36
  52. kotonebot/ui/file_host/tmp_send.py +54 -54
  53. kotonebot/ui/pushkit/__init__.py +3 -3
  54. kotonebot/ui/pushkit/image_host.py +88 -87
  55. kotonebot/ui/pushkit/protocol.py +13 -13
  56. kotonebot/ui/pushkit/wxpusher.py +54 -53
  57. kotonebot/ui/user.py +148 -148
  58. kotonebot/util.py +436 -436
  59. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -81
  60. kotonebot-0.5.0.dist-info/RECORD +71 -0
  61. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  62. kotonebot-0.4.0.dist-info/RECORD +0 -70
  63. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  64. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,202 +1,197 @@
1
- import os
2
- import subprocess
3
- from typing import Literal
4
- from functools import lru_cache
5
- from typing_extensions import override
6
-
7
- from kotonebot import logging
8
- from kotonebot.client import Device
9
- from kotonebot.util import Countdown, Interval
10
- from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
11
- from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
12
-
13
- logger = logging.getLogger(__name__)
14
- LeidianRecipes = AdbRecipes
15
-
16
- if os.name == 'nt':
17
- from ...interop.win.reg import read_reg
18
- else:
19
- def read_reg(key, subkey, name, *, default=None, **kwargs):
20
- """Stub for read_reg on non-Windows platforms."""
21
- return default
22
-
23
- class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
24
- @copy_type(Instance.__init__)
25
- def __init__(self, *args, **kwargs):
26
- super().__init__(*args, **kwargs)
27
- self._args = args
28
- self.index: int | None = None
29
- self.is_running: bool = False
30
-
31
- @override
32
- def refresh(self):
33
- ins = LeidianHost.query(id=self.id)
34
- assert isinstance(ins, LeidianInstance), f'Expected LeidianInstance, got {type(ins)}'
35
- if ins is not None:
36
- self.adb_port = ins.adb_port
37
- self.adb_ip = ins.adb_ip
38
- self.adb_name = ins.adb_name
39
- self.is_running = ins.is_running
40
- logger.debug('Refreshed Leidian instance: %s', repr(ins))
41
-
42
- @override
43
- def start(self):
44
- if self.running():
45
- logger.warning('Instance is already running.')
46
- return
47
- logger.info('Starting Leidian instance %s', self)
48
- LeidianHost._invoke_manager(['launch', '--index', str(self.index)])
49
- self.refresh()
50
-
51
- @override
52
- def stop(self):
53
- if not self.running():
54
- logger.warning('Instance is not running.')
55
- return
56
- logger.info('Stopping Leidian instance id=%s name=%s...', self.id, self.name)
57
- LeidianHost._invoke_manager(['quit', '--index', str(self.index)])
58
- self.refresh()
59
-
60
- @override
61
- def wait_available(self, timeout: float = 180):
62
- cd = Countdown(timeout)
63
- it = Interval(5)
64
- while not cd.expired() and not self.running():
65
- it.wait()
66
- self.refresh()
67
- if not self.running():
68
- raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
69
-
70
- @override
71
- def running(self) -> bool:
72
- return self.is_running
73
-
74
- @override
75
- def create_device(self, impl: LeidianRecipes, 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, connect=False, disconnect=False)
81
-
82
- class LeidianHost(HostProtocol[LeidianRecipes]):
83
- @staticmethod
84
- @lru_cache(maxsize=1)
85
- def _read_install_path() -> str | None:
86
- """
87
- 从注册表中读取雷电模拟器的安装路径。
88
-
89
- :return: 安装路径,如果未找到则返回 None。
90
- """
91
- if os.name != 'nt':
92
- return None
93
-
94
- try:
95
- icon_path = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'DisplayIcon', default=None)
96
- if icon_path and isinstance(icon_path, str):
97
- icon_path = icon_path.replace('"', '')
98
- path = os.path.dirname(icon_path)
99
- logger.debug('Leidian installation path (from DisplayIcon): %s', path)
100
- return path
101
- install_dir = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'InstallDir', default=None)
102
- if install_dir and isinstance(install_dir, str):
103
- install_dir = install_dir.replace('"', '')
104
- logger.debug('Leidian installation path (from InstallDir): %s', install_dir)
105
- return install_dir
106
- except Exception as e:
107
- logger.error(f'Failed to read Leidian installation path from registry: {e}')
108
-
109
- return None
110
-
111
- @staticmethod
112
- def _invoke_manager(args: list[str]) -> str:
113
- """
114
- 调用 ldconsole.exe。
115
-
116
- 参考文档:https://www.ldmnq.com/forum/30.html,以及命令行帮助。
117
- 另外还有个 ld.exe,封装了 adb.exe,可以直接执行 adb 命令。(https://www.ldmnq.com/forum/9178.html)
118
-
119
- :param args: 命令行参数列表。
120
- :return: 命令执行的输出。
121
- """
122
- install_path = LeidianHost._read_install_path()
123
- if install_path is None:
124
- raise RuntimeError('Leidian is not installed.')
125
- manager_path = os.path.join(install_path, 'ldconsole.exe')
126
- logger.debug('ldconsole execute: %s', repr(args))
127
- output = subprocess.run(
128
- [manager_path] + args,
129
- capture_output=True,
130
- text=True,
131
- # encoding='utf-8', # 居然不是 utf-8 编码
132
- # https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
133
- creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
134
- )
135
- if output.returncode != 0:
136
- raise RuntimeError(f'Failed to invoke ldconsole: {output.stderr}')
137
- return output.stdout
138
-
139
- @staticmethod
140
- def installed() -> bool:
141
- return LeidianHost._read_install_path() is not None
142
-
143
- @staticmethod
144
- def list() -> list[Instance]:
145
- output = LeidianHost._invoke_manager(['list2'])
146
- instances = []
147
-
148
- # 解析 list2 命令的输出
149
- # 格式: 索引,标题,顶层窗口句柄,绑定窗口句柄,是否进入android,进程PID,VBox进程PID
150
- for line in output.strip().split('\n'):
151
- if not line:
152
- continue
153
-
154
- parts = line.split(',')
155
- if len(parts) < 5:
156
- logger.warning(f'Invalid list2 output line: {line}')
157
- continue
158
-
159
- index = parts[0]
160
- name = parts[1]
161
- is_android_started = parts[4] == '1'
162
- # 端口号规则 https://help.ldmnq.com/docs/LD9adbserver#a67730c2e7e2e0400d40bcab37d0e0cf
163
- adb_port = 5554 + (int(index) * 2)
164
-
165
- instance = LeidianInstance(
166
- id=index,
167
- name=name,
168
- adb_port=adb_port,
169
- adb_ip='127.0.0.1',
170
- adb_name=f'emulator-{adb_port}'
171
- )
172
- instance.index = int(index)
173
- instance.is_running = is_android_started
174
- logger.debug('Leidian instance: %s', repr(instance))
175
- instances.append(instance)
176
-
177
- return instances
178
-
179
- @staticmethod
180
- def query(*, id: str) -> Instance | None:
181
- instances = LeidianHost.list()
182
- for instance in instances:
183
- if instance.id == id:
184
- return instance
185
- return None
186
-
187
- @staticmethod
188
- def recipes() -> 'list[LeidianRecipes]':
189
- return ['adb', 'adb_raw', 'uiautomator2']
190
-
191
- if __name__ == '__main__':
192
- logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
193
- print(LeidianHost._read_install_path())
194
- print(LeidianHost.installed())
195
- print(LeidianHost.list())
196
- print(ins:=LeidianHost.query(id='0'))
197
- assert isinstance(ins, LeidianInstance)
198
- ins.start()
199
- ins.wait_available()
200
- print('status', ins.running(), ins.adb_port, ins.adb_ip)
201
- # ins.stop()
1
+ import os
2
+ import subprocess
3
+ from typing import Literal
4
+ from functools import lru_cache
5
+ from typing_extensions import override
6
+
7
+ from kotonebot import logging
8
+ from kotonebot.client import Device
9
+ from kotonebot.util import Countdown, Interval
10
+ from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
11
+ from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
12
+ from ...interop.win.reg import read_reg
13
+
14
+ logger = logging.getLogger(__name__)
15
+ LeidianRecipes = AdbRecipes
16
+
17
+
18
+ class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
19
+ @copy_type(Instance.__init__)
20
+ def __init__(self, *args, **kwargs):
21
+ super().__init__(*args, **kwargs)
22
+ self._args = args
23
+ self.index: int | None = None
24
+ self.is_running: bool = False
25
+
26
+ @override
27
+ def refresh(self):
28
+ ins = LeidianHost.query(id=self.id)
29
+ assert isinstance(ins, LeidianInstance), f'Expected LeidianInstance, got {type(ins)}'
30
+ if ins is not None:
31
+ self.adb_port = ins.adb_port
32
+ self.adb_ip = ins.adb_ip
33
+ self.adb_name = ins.adb_name
34
+ self.is_running = ins.is_running
35
+ logger.debug('Refreshed Leidian instance: %s', repr(ins))
36
+
37
+ @override
38
+ def start(self):
39
+ if self.running():
40
+ logger.warning('Instance is already running.')
41
+ return
42
+ logger.info('Starting Leidian instance %s', self)
43
+ LeidianHost._invoke_manager(['launch', '--index', str(self.index)])
44
+ self.refresh()
45
+
46
+ @override
47
+ def stop(self):
48
+ if not self.running():
49
+ logger.warning('Instance is not running.')
50
+ return
51
+ logger.info('Stopping Leidian instance id=%s name=%s...', self.id, self.name)
52
+ LeidianHost._invoke_manager(['quit', '--index', str(self.index)])
53
+ self.refresh()
54
+
55
+ @override
56
+ def wait_available(self, timeout: float = 180):
57
+ cd = Countdown(timeout)
58
+ it = Interval(5)
59
+ while not cd.expired() and not self.running():
60
+ it.wait()
61
+ self.refresh()
62
+ if not self.running():
63
+ raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
64
+
65
+ @override
66
+ def running(self) -> bool:
67
+ return self.is_running
68
+
69
+ @override
70
+ def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
71
+ """为雷电模拟器实例创建 Device。"""
72
+ if self.adb_port is None:
73
+ raise ValueError("ADB port is not set and is required.")
74
+
75
+ return super().create_device(impl, host_config, connect=False, disconnect=False)
76
+
77
+ class LeidianHost(HostProtocol[LeidianRecipes]):
78
+ @staticmethod
79
+ @lru_cache(maxsize=1)
80
+ def _read_install_path() -> str | None:
81
+ """
82
+ 从注册表中读取雷电模拟器的安装路径。
83
+
84
+ :return: 安装路径,如果未找到则返回 None。
85
+ """
86
+ if os.name != 'nt':
87
+ return None
88
+
89
+ try:
90
+ icon_path = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'DisplayIcon', default=None)
91
+ if icon_path and isinstance(icon_path, str):
92
+ icon_path = icon_path.replace('"', '')
93
+ path = os.path.dirname(icon_path)
94
+ logger.debug('Leidian installation path (from DisplayIcon): %s', path)
95
+ return path
96
+ install_dir = read_reg('HKCU', r'Software\leidian\LDPlayer9', 'InstallDir', default=None)
97
+ if install_dir and isinstance(install_dir, str):
98
+ install_dir = install_dir.replace('"', '')
99
+ logger.debug('Leidian installation path (from InstallDir): %s', install_dir)
100
+ return install_dir
101
+ except Exception as e:
102
+ logger.error(f'Failed to read Leidian installation path from registry: {e}')
103
+
104
+ return None
105
+
106
+ @staticmethod
107
+ def _invoke_manager(args: list[str]) -> str:
108
+ """
109
+ 调用 ldconsole.exe。
110
+
111
+ 参考文档:https://www.ldmnq.com/forum/30.html,以及命令行帮助。
112
+ 另外还有个 ld.exe,封装了 adb.exe,可以直接执行 adb 命令。(https://www.ldmnq.com/forum/9178.html)
113
+
114
+ :param args: 命令行参数列表。
115
+ :return: 命令执行的输出。
116
+ """
117
+ install_path = LeidianHost._read_install_path()
118
+ if install_path is None:
119
+ raise RuntimeError('Leidian is not installed.')
120
+ manager_path = os.path.join(install_path, 'ldconsole.exe')
121
+ logger.debug('ldconsole execute: %s', repr(args))
122
+ output = subprocess.run(
123
+ [manager_path] + args,
124
+ capture_output=True,
125
+ text=True,
126
+ # encoding='utf-8', # 居然不是 utf-8 编码
127
+ # https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
128
+ creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
129
+ )
130
+ if output.returncode != 0:
131
+ raise RuntimeError(f'Failed to invoke ldconsole: {output.stderr}')
132
+ return output.stdout
133
+
134
+ @staticmethod
135
+ def installed() -> bool:
136
+ return LeidianHost._read_install_path() is not None
137
+
138
+ @staticmethod
139
+ def list() -> list[Instance]:
140
+ output = LeidianHost._invoke_manager(['list2'])
141
+ instances = []
142
+
143
+ # 解析 list2 命令的输出
144
+ # 格式: 索引,标题,顶层窗口句柄,绑定窗口句柄,是否进入android,进程PID,VBox进程PID
145
+ for line in output.strip().split('\n'):
146
+ if not line:
147
+ continue
148
+
149
+ parts = line.split(',')
150
+ if len(parts) < 5:
151
+ logger.warning(f'Invalid list2 output line: {line}')
152
+ continue
153
+
154
+ index = parts[0]
155
+ name = parts[1]
156
+ is_android_started = parts[4] == '1'
157
+ # 端口号规则 https://help.ldmnq.com/docs/LD9adbserver#a67730c2e7e2e0400d40bcab37d0e0cf
158
+ adb_port = 5554 + (int(index) * 2)
159
+
160
+ instance = LeidianInstance(
161
+ id=index,
162
+ name=name,
163
+ adb_port=adb_port,
164
+ adb_ip='127.0.0.1',
165
+ adb_name=f'emulator-{adb_port}'
166
+ )
167
+ instance.index = int(index)
168
+ instance.is_running = is_android_started
169
+ logger.debug('Leidian instance: %s', repr(instance))
170
+ instances.append(instance)
171
+
172
+ return instances
173
+
174
+ @staticmethod
175
+ def query(*, id: str) -> Instance | None:
176
+ instances = LeidianHost.list()
177
+ for instance in instances:
178
+ if instance.id == id:
179
+ return instance
180
+ return None
181
+
182
+ @staticmethod
183
+ def recipes() -> 'list[LeidianRecipes]':
184
+ return ['adb', 'adb_raw', 'uiautomator2']
185
+
186
+ if __name__ == '__main__':
187
+ logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
188
+ print(LeidianHost._read_install_path())
189
+ print(LeidianHost.installed())
190
+ print(LeidianHost.list())
191
+ print(ins:=LeidianHost.query(id='0'))
192
+ assert isinstance(ins, LeidianInstance)
193
+ ins.start()
194
+ ins.wait_available()
195
+ print('status', ins.running(), ins.adb_port, ins.adb_ip)
196
+ # ins.stop()
202
197
  # print('status', ins.running(), ins.adb_port, ins.adb_ip)