kotonebot 0.2.0__tar.gz → 0.3.0__tar.gz
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-0.2.0/kotonebot.egg-info → kotonebot-0.3.0}/PKG-INFO +1 -1
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/ocr.py +20 -2
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/mumu12_host.py +157 -44
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +5 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0/kotonebot.egg-info}/PKG-INFO +1 -1
- {kotonebot-0.2.0 → kotonebot-0.3.0}/pyproject.toml +1 -1
- {kotonebot-0.2.0 → kotonebot-0.3.0}/LICENSE +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/MANIFEST.in +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/README.md +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/bot.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/color.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/context.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/context/task_action.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/core.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/entry.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/mock.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/server.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/debug/vars.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/dispatch.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/flow_controller.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/image.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/loop.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/backend/preprocessor.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/device.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/fast_screenshot.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/adb_common.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/custom.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/leidian_host.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/protocol.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/host/windows_common.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/adb.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/adb_raw.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/nemu_ipc.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/remote_windows.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/uiautomator2.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/windows.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/protocol.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/registration.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/base_config.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/config/manager.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/errors.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/message_box.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/reg.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/shortcut.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/interop/win/task_dialog.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/logging/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/logging/log.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/geometry.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/primitives/visual.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/tools/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/tools/mirror.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/file_host/sensio.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/file_host/tmp_send.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/__init__.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/image_host.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/protocol.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/pushkit/wxpusher.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/ui/user.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/util.py +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/SOURCES.txt +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/dependency_links.txt +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/requires.txt +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot.egg-info/top_level.txt +0 -0
- {kotonebot-0.2.0 → kotonebot-0.3.0}/setup.cfg +0 -0
|
@@ -24,6 +24,25 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
StringMatchFunction = Callable[[str], bool]
|
|
25
25
|
REGEX_NUMBERS = re.compile(r'\d+')
|
|
26
26
|
|
|
27
|
+
global_character_mapping: dict[str, str] = {
|
|
28
|
+
'ó': '6',
|
|
29
|
+
'ą': 'a',
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
全局字符映射表。某些字符可能在某些情况下被错误地识别,此时可以在这里添加映射。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def sanitize_text(text: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
对识别结果进行清理。此函数将被所有 OCR 引擎调用。
|
|
38
|
+
|
|
39
|
+
默认使用 `global_character_mapping` 中的映射数据进行清理。
|
|
40
|
+
可以重写此函数以实现自定义的清理逻辑。
|
|
41
|
+
"""
|
|
42
|
+
for k, v in global_character_mapping.items():
|
|
43
|
+
text = text.replace(k, v)
|
|
44
|
+
return text
|
|
45
|
+
|
|
27
46
|
@dataclass
|
|
28
47
|
class OcrResult:
|
|
29
48
|
text: str
|
|
@@ -330,8 +349,7 @@ class Ocr:
|
|
|
330
349
|
return OcrResultList()
|
|
331
350
|
ret = []
|
|
332
351
|
for r in result:
|
|
333
|
-
|
|
334
|
-
text = unicodedata.normalize('NFKC', r[1]).replace('ą', 'a')
|
|
352
|
+
text = sanitize_text(r[1])
|
|
335
353
|
# r[0] = [左上, 右上, 右下, 左下]
|
|
336
354
|
# 这里有个坑,返回的点不一定是矩形,只能保证是四边形
|
|
337
355
|
# 所以这里需要计算出四个点的外接矩形
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from typing import Any, Literal, overload
|
|
6
|
+
from typing import Any, Literal, overload, TYPE_CHECKING
|
|
7
7
|
from typing_extensions import override
|
|
8
8
|
|
|
9
9
|
from kotonebot import logging
|
|
@@ -22,6 +22,10 @@ else:
|
|
|
22
22
|
"""Stub for read_reg on non-Windows platforms."""
|
|
23
23
|
return default
|
|
24
24
|
|
|
25
|
+
# Forward declarations for type hints
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from typing import Type
|
|
28
|
+
|
|
25
29
|
logger = logging.getLogger(__name__)
|
|
26
30
|
MuMu12Recipes = AdbRecipes | Literal['nemu_ipc']
|
|
27
31
|
|
|
@@ -35,24 +39,137 @@ class MuMu12HostConfig(AdbHostConfig):
|
|
|
35
39
|
app_index: int = 0
|
|
36
40
|
"""多开应用索引,传给 get_display_id 方法。"""
|
|
37
41
|
|
|
42
|
+
class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
43
|
+
InstanceClass: 'Type[Mumu12Instance]'
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
@lru_cache(maxsize=1)
|
|
47
|
+
def _read_install_path() -> str | None:
|
|
48
|
+
r"""
|
|
49
|
+
从注册表中读取 MuMu Player 12 的安装路径。
|
|
50
|
+
|
|
51
|
+
返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
|
|
52
|
+
|
|
53
|
+
:return: 若找到,则返回安装路径;否则返回 None。
|
|
54
|
+
"""
|
|
55
|
+
if os.name != 'nt':
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
uninstall_subkeys = [
|
|
59
|
+
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer-12.0',
|
|
60
|
+
# TODO: 支持国际版 MuMu
|
|
61
|
+
# r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
for subkey in uninstall_subkeys:
|
|
65
|
+
icon_path = read_reg('HKLM', subkey, 'DisplayIcon', default=None)
|
|
66
|
+
if icon_path and isinstance(icon_path, str):
|
|
67
|
+
icon_path = icon_path.replace('"', '')
|
|
68
|
+
path = os.path.dirname(icon_path)
|
|
69
|
+
logger.debug('MuMu Player 12 installation path: %s', path)
|
|
70
|
+
# 返回根目录(去掉 shell 子目录)
|
|
71
|
+
if os.path.basename(path).lower() == 'shell':
|
|
72
|
+
path = os.path.dirname(path)
|
|
73
|
+
return path
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _invoke_manager(cls,args: list[str]) -> str:
|
|
78
|
+
"""
|
|
79
|
+
调用 MuMuManager.exe。
|
|
80
|
+
|
|
81
|
+
:param args: 命令行参数列表。
|
|
82
|
+
:return: 命令执行的输出。
|
|
83
|
+
"""
|
|
84
|
+
install_path = cls._read_install_path()
|
|
85
|
+
if install_path is None:
|
|
86
|
+
raise RuntimeError('MuMu Player 12 is not installed.')
|
|
87
|
+
manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
|
|
88
|
+
logger.debug('MuMuManager execute: %s', repr(args))
|
|
89
|
+
output = subprocess.run(
|
|
90
|
+
[manager_path] + args,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True,
|
|
93
|
+
encoding='utf-8',
|
|
94
|
+
# https://stackoverflow.com/questions/6011235/run-a-program-from-python-and-have-it-continue-to-run-after-the-script-is-kille
|
|
95
|
+
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
|
96
|
+
)
|
|
97
|
+
if output.returncode != 0:
|
|
98
|
+
# raise RuntimeError(f'Failed to invoke MuMuManager: {output.stderr}')
|
|
99
|
+
logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
|
|
100
|
+
return output.stdout
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def installed() -> bool:
|
|
104
|
+
return Mumu12Host._read_install_path() is not None
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def list(cls) -> list[Instance]:
|
|
108
|
+
nemu_path = cls._read_install_path()
|
|
109
|
+
if nemu_path is None:
|
|
110
|
+
raise RuntimeError("Nemu path not found.")
|
|
111
|
+
output = cls._invoke_manager(['info', '-v', 'all'])
|
|
112
|
+
logger.debug('MuMuManager.exe output: %s', output)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
data: dict[str, dict[str, Any]] = json.loads(output)
|
|
116
|
+
if 'name' in data.keys():
|
|
117
|
+
# 这里有个坑:
|
|
118
|
+
# 如果只有一个实例,返回的 JSON 结构是单个对象而不是数组
|
|
119
|
+
data = { '0': data }
|
|
120
|
+
instances = []
|
|
121
|
+
for index, instance_data in data.items():
|
|
122
|
+
instance = cls.InstanceClass(
|
|
123
|
+
id=index,
|
|
124
|
+
name=instance_data['name'],
|
|
125
|
+
adb_port=instance_data.get('adb_port'),
|
|
126
|
+
adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
|
|
127
|
+
adb_name=None,
|
|
128
|
+
)
|
|
129
|
+
instance.nemu_path = nemu_path
|
|
130
|
+
instance.index = int(index)
|
|
131
|
+
instance.is_android_started = instance_data.get('is_android_started', False)
|
|
132
|
+
logger.debug('Mumu12 instance: %s', repr(instance))
|
|
133
|
+
instances.append(instance)
|
|
134
|
+
return instances
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
raise RuntimeError(f'Failed to parse output: {e}')
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def query(cls, *, id: str) -> Instance | None:
|
|
140
|
+
instances = cls.list()
|
|
141
|
+
for instance in instances:
|
|
142
|
+
if instance.id == id:
|
|
143
|
+
return instance
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def recipes() -> 'list[MuMu12Recipes]':
|
|
148
|
+
return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
|
|
38
149
|
|
|
39
150
|
class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
151
|
+
HostClass: 'type[Mumu12Host]' = Mumu12Host
|
|
152
|
+
|
|
40
153
|
@copy_type(Instance.__init__)
|
|
41
154
|
def __init__(self, *args, **kwargs):
|
|
155
|
+
if not hasattr(self.HostClass, 'InstanceClass'):
|
|
156
|
+
raise RuntimeError(f"{self.HostClass.__name__}.InstanceClass not initialized")
|
|
157
|
+
|
|
42
158
|
super().__init__(*args, **kwargs)
|
|
43
159
|
self._args = args
|
|
44
160
|
self.index: int | None = None
|
|
45
161
|
self.is_android_started: bool = False
|
|
162
|
+
self.nemu_path: str | None = None
|
|
46
163
|
|
|
47
164
|
@override
|
|
48
165
|
def refresh(self):
|
|
49
|
-
ins =
|
|
50
|
-
|
|
51
|
-
if ins is not None:
|
|
166
|
+
ins = self.HostClass.query(id=self.id)
|
|
167
|
+
if ins is not None and isinstance(ins, self.__class__):
|
|
52
168
|
self.adb_port = ins.adb_port
|
|
53
169
|
self.adb_ip = ins.adb_ip
|
|
54
170
|
self.adb_name = ins.adb_name
|
|
55
171
|
self.is_android_started = ins.is_android_started
|
|
172
|
+
self.nemu_path = ins.nemu_path
|
|
56
173
|
logger.debug('Refreshed MuMu12 instance: %s', repr(ins))
|
|
57
174
|
|
|
58
175
|
@override
|
|
@@ -61,7 +178,7 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
|
61
178
|
logger.warning('Instance is already running.')
|
|
62
179
|
return
|
|
63
180
|
logger.info('Starting MuMu12 instance %s', self)
|
|
64
|
-
|
|
181
|
+
self.HostClass._invoke_manager(['control', '-v', self.id, 'launch'])
|
|
65
182
|
self.refresh()
|
|
66
183
|
|
|
67
184
|
@override
|
|
@@ -70,7 +187,7 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
|
70
187
|
logger.warning('Instance is not running.')
|
|
71
188
|
return
|
|
72
189
|
logger.info('Stopping MuMu12 instance id=%s name=%s...', self.id, self.name)
|
|
73
|
-
|
|
190
|
+
self.HostClass._invoke_manager(['control', '-v', self.id, 'shutdown'])
|
|
74
191
|
self.refresh()
|
|
75
192
|
|
|
76
193
|
@override
|
|
@@ -100,11 +217,10 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
|
100
217
|
|
|
101
218
|
if recipe == 'nemu_ipc' and isinstance(host_config, MuMu12HostConfig):
|
|
102
219
|
# NemuImpl
|
|
103
|
-
nemu_path
|
|
104
|
-
|
|
105
|
-
raise RuntimeError("无法找到 MuMu12 的安装路径。")
|
|
220
|
+
if self.nemu_path is None:
|
|
221
|
+
raise RuntimeError("Nemu path is not set.")
|
|
106
222
|
nemu_config = NemuIpcImplConfig(
|
|
107
|
-
nemu_folder=nemu_path,
|
|
223
|
+
nemu_folder=self.nemu_path,
|
|
108
224
|
instance_id=int(self.id),
|
|
109
225
|
display_id=host_config.display_id,
|
|
110
226
|
target_package_name=host_config.target_package_name,
|
|
@@ -129,12 +245,14 @@ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
|
|
|
129
245
|
else:
|
|
130
246
|
raise ValueError(f'Unknown recipe: {recipe}')
|
|
131
247
|
|
|
132
|
-
class Mumu12Host
|
|
133
|
-
|
|
248
|
+
class Mumu12V5Host(Mumu12Host):
|
|
249
|
+
InstanceClass: 'Type[Mumu12V5Instance]'
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
134
252
|
@lru_cache(maxsize=1)
|
|
135
|
-
def _read_install_path() -> str | None:
|
|
253
|
+
def _read_install_path(cls) -> str | None:
|
|
136
254
|
r"""
|
|
137
|
-
从注册表中读取 MuMu Player 12 的安装路径。
|
|
255
|
+
从注册表中读取 MuMu Player 12 v5.x 的安装路径。
|
|
138
256
|
|
|
139
257
|
返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
|
|
140
258
|
|
|
@@ -144,9 +262,7 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
|
144
262
|
return None
|
|
145
263
|
|
|
146
264
|
uninstall_subkeys = [
|
|
147
|
-
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer
|
|
148
|
-
# TODO: 支持国际版 MuMu
|
|
149
|
-
# r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayerGlobal-12.0'
|
|
265
|
+
r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MuMuPlayer',
|
|
150
266
|
]
|
|
151
267
|
|
|
152
268
|
for subkey in uninstall_subkeys:
|
|
@@ -156,23 +272,23 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
|
156
272
|
path = os.path.dirname(icon_path)
|
|
157
273
|
logger.debug('MuMu Player 12 installation path: %s', path)
|
|
158
274
|
# 返回根目录(去掉 shell 子目录)
|
|
159
|
-
if os.path.basename(path).lower() == '
|
|
275
|
+
if os.path.basename(path).lower() == 'nx_main':
|
|
160
276
|
path = os.path.dirname(path)
|
|
161
277
|
return path
|
|
162
278
|
return None
|
|
163
279
|
|
|
164
|
-
@
|
|
165
|
-
def _invoke_manager(args: list[str]) -> str:
|
|
280
|
+
@classmethod
|
|
281
|
+
def _invoke_manager(cls, args: list[str]) -> str:
|
|
166
282
|
"""
|
|
167
283
|
调用 MuMuManager.exe。
|
|
168
284
|
|
|
169
285
|
:param args: 命令行参数列表。
|
|
170
286
|
:return: 命令执行的输出。
|
|
171
287
|
"""
|
|
172
|
-
install_path =
|
|
288
|
+
install_path = cls._read_install_path()
|
|
173
289
|
if install_path is None:
|
|
174
|
-
raise RuntimeError('MuMu Player 12 is not installed.')
|
|
175
|
-
manager_path = os.path.join(install_path, '
|
|
290
|
+
raise RuntimeError('MuMu Player 12 v5.x is not installed.')
|
|
291
|
+
manager_path = os.path.join(install_path, 'nx_main', 'MuMuManager.exe')
|
|
176
292
|
logger.debug('MuMuManager execute: %s', repr(args))
|
|
177
293
|
output = subprocess.run(
|
|
178
294
|
[manager_path] + args,
|
|
@@ -187,13 +303,13 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
|
187
303
|
logger.warning('Failed to invoke MuMuManager: %s', output.stderr)
|
|
188
304
|
return output.stdout
|
|
189
305
|
|
|
190
|
-
@
|
|
191
|
-
def installed() -> bool:
|
|
192
|
-
return
|
|
306
|
+
@classmethod
|
|
307
|
+
def installed(cls) -> bool:
|
|
308
|
+
return cls._read_install_path() is not None
|
|
193
309
|
|
|
194
|
-
@
|
|
195
|
-
def list() -> list[Instance]:
|
|
196
|
-
output =
|
|
310
|
+
@classmethod
|
|
311
|
+
def list(cls) -> list[Instance]:
|
|
312
|
+
output = cls._invoke_manager(['info', '-v', 'all'])
|
|
197
313
|
logger.debug('MuMuManager.exe output: %s', output)
|
|
198
314
|
|
|
199
315
|
try:
|
|
@@ -204,40 +320,37 @@ class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
|
204
320
|
data = { '0': data }
|
|
205
321
|
instances = []
|
|
206
322
|
for index, instance_data in data.items():
|
|
207
|
-
instance =
|
|
323
|
+
instance = cls.InstanceClass(
|
|
208
324
|
id=index,
|
|
209
325
|
name=instance_data['name'],
|
|
210
326
|
adb_port=instance_data.get('adb_port'),
|
|
211
327
|
adb_ip=instance_data.get('adb_host_ip', '127.0.0.1'),
|
|
212
328
|
adb_name=None
|
|
213
329
|
)
|
|
330
|
+
instance.nemu_path = cls._read_install_path()
|
|
214
331
|
instance.index = int(index)
|
|
215
332
|
instance.is_android_started = instance_data.get('is_android_started', False)
|
|
216
|
-
logger.debug('Mumu12 instance: %s', repr(instance))
|
|
333
|
+
logger.debug('Mumu12 v5.x instance: %s', repr(instance))
|
|
217
334
|
instances.append(instance)
|
|
218
335
|
return instances
|
|
219
336
|
except json.JSONDecodeError as e:
|
|
220
337
|
raise RuntimeError(f'Failed to parse output: {e}')
|
|
221
|
-
|
|
222
|
-
@staticmethod
|
|
223
|
-
def query(*, id: str) -> Instance | None:
|
|
224
|
-
instances = Mumu12Host.list()
|
|
225
|
-
for instance in instances:
|
|
226
|
-
if instance.id == id:
|
|
227
|
-
return instance
|
|
228
|
-
return None
|
|
229
338
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
339
|
+
class Mumu12V5Instance(Mumu12Instance):
|
|
340
|
+
HostClass: 'type[Mumu12V5Host]' = Mumu12V5Host
|
|
341
|
+
|
|
342
|
+
# 延迟初始化 InstanceClass 变量
|
|
343
|
+
Mumu12Host.InstanceClass = Mumu12Instance
|
|
344
|
+
Mumu12V5Host.InstanceClass = Mumu12V5Instance
|
|
345
|
+
|
|
346
|
+
|
|
234
347
|
if __name__ == '__main__':
|
|
235
348
|
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
236
349
|
print(Mumu12Host._read_install_path())
|
|
237
350
|
print(Mumu12Host.installed())
|
|
238
351
|
print(Mumu12Host.list())
|
|
239
352
|
print(ins:=Mumu12Host.query(id='2'))
|
|
240
|
-
assert isinstance(ins,
|
|
353
|
+
assert isinstance(ins, Mumu12Host.InstanceClass)
|
|
241
354
|
ins.start()
|
|
242
355
|
ins.wait_available()
|
|
243
356
|
print('status', ins.running(), ins.adb_port, ins.adb_ip)
|
{kotonebot-0.2.0 → kotonebot-0.3.0}/kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py
RENAMED
|
@@ -204,6 +204,7 @@ class ExternalRendererIpc:
|
|
|
204
204
|
def __load_dll(self, mumu_root_folder: str) -> ctypes.CDLL:
|
|
205
205
|
"""尝试多条路径加载 DLL。传入为 MuMu 根目录。"""
|
|
206
206
|
candidate_paths = [
|
|
207
|
+
# <= 4.x
|
|
207
208
|
os.path.join(mumu_root_folder, "shell", "sdk", "external_renderer_ipc.dll"),
|
|
208
209
|
os.path.join(
|
|
209
210
|
mumu_root_folder,
|
|
@@ -213,6 +214,10 @@ class ExternalRendererIpc:
|
|
|
213
214
|
"sdk",
|
|
214
215
|
"external_renderer_ipc.dll",
|
|
215
216
|
),
|
|
217
|
+
# >= 5.x
|
|
218
|
+
os.path.join(
|
|
219
|
+
mumu_root_folder, "nx_device", "12.0", "shell", "sdk", "external_renderer_ipc.dll"
|
|
220
|
+
),
|
|
216
221
|
]
|
|
217
222
|
for p in candidate_paths:
|
|
218
223
|
if not os.path.exists(p):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kotonebot"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Kotonebot is game/app automation library based on computer vision technology, works for Windows and Android."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|