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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) 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/context.py +1002 -1002
  6. kotonebot/backend/context/task_action.py +183 -183
  7. kotonebot/backend/core.py +86 -129
  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/image.py +36 -5
  15. kotonebot/backend/loop.py +222 -208
  16. kotonebot/backend/ocr.py +535 -535
  17. kotonebot/backend/preprocessor.py +103 -103
  18. kotonebot/client/__init__.py +9 -9
  19. kotonebot/client/device.py +369 -529
  20. kotonebot/client/fast_screenshot.py +377 -377
  21. kotonebot/client/host/__init__.py +43 -43
  22. kotonebot/client/host/adb_common.py +101 -107
  23. kotonebot/client/host/custom.py +118 -118
  24. kotonebot/client/host/leidian_host.py +196 -196
  25. kotonebot/client/host/mumu12_host.py +353 -353
  26. kotonebot/client/host/protocol.py +214 -214
  27. kotonebot/client/host/windows_common.py +58 -58
  28. kotonebot/client/implements/__init__.py +65 -70
  29. kotonebot/client/implements/adb.py +89 -89
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
  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 -188
  34. kotonebot/client/implements/uiautomator2.py +85 -85
  35. kotonebot/client/implements/windows.py +176 -176
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/client/scaler.py +467 -0
  39. kotonebot/config/base_config.py +96 -96
  40. kotonebot/config/config.py +61 -0
  41. kotonebot/config/manager.py +36 -36
  42. kotonebot/core/__init__.py +13 -0
  43. kotonebot/core/entities/base.py +182 -0
  44. kotonebot/core/entities/compound.py +75 -0
  45. kotonebot/core/entities/ocr.py +117 -0
  46. kotonebot/core/entities/template_match.py +198 -0
  47. kotonebot/devtools/__init__.py +42 -0
  48. kotonebot/devtools/cli/__init__.py +6 -0
  49. kotonebot/devtools/cli/main.py +53 -0
  50. kotonebot/{tools → devtools}/mirror.py +354 -354
  51. kotonebot/devtools/project/project.py +41 -0
  52. kotonebot/devtools/project/scanner.py +202 -0
  53. kotonebot/devtools/project/schema.py +99 -0
  54. kotonebot/devtools/resgen/__init__.py +42 -0
  55. kotonebot/devtools/resgen/codegen.py +331 -0
  56. kotonebot/devtools/resgen/core.py +94 -0
  57. kotonebot/devtools/resgen/parsers.py +360 -0
  58. kotonebot/devtools/resgen/utils.py +158 -0
  59. kotonebot/devtools/resgen/validation.py +115 -0
  60. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  61. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  62. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  63. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  64. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  65. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  66. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  67. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  68. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  69. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  70. kotonebot/devtools/web/dist/index.html +25 -0
  71. kotonebot/devtools/web/server/__init__.py +0 -0
  72. kotonebot/devtools/web/server/rest_api.py +217 -0
  73. kotonebot/devtools/web/server/server.py +85 -0
  74. kotonebot/errors.py +76 -76
  75. kotonebot/interop/win/__init__.py +11 -9
  76. kotonebot/interop/win/_mouse.py +310 -310
  77. kotonebot/interop/win/message_box.py +313 -313
  78. kotonebot/interop/win/reg.py +37 -37
  79. kotonebot/interop/win/shake_mouse.py +224 -0
  80. kotonebot/interop/win/shortcut.py +43 -43
  81. kotonebot/interop/win/task_dialog.py +513 -513
  82. kotonebot/logging/__init__.py +2 -2
  83. kotonebot/logging/log.py +17 -17
  84. kotonebot/primitives/__init__.py +19 -17
  85. kotonebot/primitives/geometry.py +1067 -862
  86. kotonebot/primitives/visual.py +143 -63
  87. kotonebot/ui/file_host/sensio.py +36 -36
  88. kotonebot/ui/file_host/tmp_send.py +54 -54
  89. kotonebot/ui/pushkit/__init__.py +3 -3
  90. kotonebot/ui/pushkit/image_host.py +88 -88
  91. kotonebot/ui/pushkit/protocol.py +13 -13
  92. kotonebot/ui/pushkit/wxpusher.py +54 -54
  93. kotonebot/ui/user.py +148 -148
  94. kotonebot/util.py +436 -436
  95. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
  96. kotonebot-0.6.0.dist-info/RECORD +105 -0
  97. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  98. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
  99. kotonebot/client/implements/adb_raw.py +0 -163
  100. kotonebot-0.5.0.dist-info/RECORD +0 -71
  101. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  102. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  103. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,197 +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
- 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()
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', '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()
197
197
  # print('status', ins.running(), ins.adb_port, ins.adb_ip)