refactor(core): 组装 Device 改用 recipe 方案
原来组装 Device 的代码放在每个 Impl 文件下实现,通过 @register_impl 装饰器注册组装函数,然后通过统一接口 组装。现在将所有组装代码移动到了 Host 实现下,Impl 实现 只需要实现自身。
This commit is contained in:
parent
07186787b4
commit
f01e0224cb
|
@ -76,6 +76,9 @@ kaa 的开发主要用到了以下开源项目:
|
||||||
](https://github.com/AllenHeartcore/GkmasObjectManager):用于提取游戏图像资源,以 GPLv3 协议开源。
|
](https://github.com/AllenHeartcore/GkmasObjectManager):用于提取游戏图像资源,以 GPLv3 协议开源。
|
||||||
* [gakumasu-diff](https://github.com/vertesan/gakumasu-diff):游戏数据。
|
* [gakumasu-diff](https://github.com/vertesan/gakumasu-diff):游戏数据。
|
||||||
|
|
||||||
|
kaa 的开发还参考了以下开源项目:
|
||||||
|
* [EmulatorExtras](https://github.com/MaaXYZ/EmulatorExtras):MuMu 与雷电模拟器的截图与控制接口定义。
|
||||||
|
* [blue_archive_auto_script](https://github.com/pur1fying/blue_archive_auto_script):MuMu 与雷电模拟器的截图与控制接口的 Python 实现,以及各模拟器的控制实现。
|
||||||
|
|
||||||
## 免责声明
|
## 免责声明
|
||||||
**请在使用本项目前仔细阅读以下内容。使用本脚本将带来包括但不限于账号被封禁的风险。**
|
**请在使用本项目前仔细阅读以下内容。使用本脚本将带来包括但不限于账号被封禁的风险。**
|
||||||
|
|
|
@ -46,12 +46,10 @@ from kotonebot.backend.color import (
|
||||||
from kotonebot.backend.ocr import (
|
from kotonebot.backend.ocr import (
|
||||||
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
||||||
)
|
)
|
||||||
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
||||||
from kotonebot.config.manager import load_config, save_config
|
from kotonebot.config.manager import load_config, save_config
|
||||||
from kotonebot.config.base_config import UserConfig
|
from kotonebot.config.base_config import UserConfig
|
||||||
from kotonebot.backend.core import Image, HintBox
|
from kotonebot.backend.core import Image, HintBox
|
||||||
from kotonebot.errors import KotonebotWarning
|
from kotonebot.errors import KotonebotWarning
|
||||||
from kotonebot.client import DeviceImpl
|
|
||||||
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
||||||
from kotonebot.primitives import Rect
|
from kotonebot.primitives import Rect
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from .device import Device
|
from .device import Device
|
||||||
from .registration import create_device, DeviceImpl
|
from .registration import DeviceImpl
|
||||||
|
|
||||||
# 确保所有实现都被注册
|
# 确保所有实现都被注册
|
||||||
from . import implements # noqa: F401
|
from . import implements # noqa: F401
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Device',
|
'Device',
|
||||||
'create_device',
|
|
||||||
'DeviceImpl',
|
'DeviceImpl',
|
||||||
]
|
]
|
|
@ -0,0 +1,91 @@
|
||||||
|
from abc import ABC
|
||||||
|
from typing import Literal, TypeVar
|
||||||
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
from adbutils import adb
|
||||||
|
from adbutils._device import AdbDevice
|
||||||
|
from kotonebot import logging
|
||||||
|
from kotonebot.client.device import AndroidDevice
|
||||||
|
from .protocol import Instance, AdbHostConfig, Device
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
AdbRecipes = Literal['adb', 'adb_raw', 'uiautomator2']
|
||||||
|
|
||||||
|
def connect_adb(
|
||||||
|
ip: str,
|
||||||
|
port: int,
|
||||||
|
connect: bool = True,
|
||||||
|
disconnect: bool = True,
|
||||||
|
timeout: float = 180,
|
||||||
|
device_serial: str | None = None
|
||||||
|
) -> AdbDevice:
|
||||||
|
"""
|
||||||
|
创建 ADB 连接。
|
||||||
|
"""
|
||||||
|
if disconnect:
|
||||||
|
logger.debug('adb disconnect %s:%d', ip, port)
|
||||||
|
adb.disconnect(f'{ip}:{port}')
|
||||||
|
if connect:
|
||||||
|
logger.debug('adb connect %s:%d', ip, port)
|
||||||
|
result = adb.connect(f'{ip}:{port}')
|
||||||
|
if 'cannot connect to' in result:
|
||||||
|
raise ValueError(result)
|
||||||
|
serial = device_serial or f'{ip}:{port}'
|
||||||
|
logger.debug('adb wait for %s', serial)
|
||||||
|
adb.wait_for(serial, timeout=timeout)
|
||||||
|
devices = adb.device_list()
|
||||||
|
logger.debug('adb device_list: %s', devices)
|
||||||
|
d = [d for d in devices if d.serial == serial]
|
||||||
|
if len(d) == 0:
|
||||||
|
raise ValueError(f"Device {serial} not found")
|
||||||
|
d = d[0]
|
||||||
|
return d
|
||||||
|
|
||||||
|
class CommonAdbCreateDeviceMixin(ABC):
|
||||||
|
"""
|
||||||
|
通用 ADB 创建设备的 Mixin。
|
||||||
|
该 Mixin 定义了创建 ADB 设备的通用接口。
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# 下面的属性只是为了让类型检查通过,无实际实现
|
||||||
|
self.adb_ip: str
|
||||||
|
self.adb_port: int
|
||||||
|
self.adb_name: str
|
||||||
|
|
||||||
|
def create_device(self, recipe: AdbRecipes, config: AdbHostConfig) -> Device:
|
||||||
|
"""
|
||||||
|
创建 ADB 设备。
|
||||||
|
"""
|
||||||
|
connection = connect_adb(
|
||||||
|
self.adb_ip,
|
||||||
|
self.adb_port,
|
||||||
|
connect=True,
|
||||||
|
disconnect=True,
|
||||||
|
timeout=config.timeout,
|
||||||
|
device_serial=self.adb_name
|
||||||
|
)
|
||||||
|
d = AndroidDevice(connection)
|
||||||
|
match recipe:
|
||||||
|
case 'adb':
|
||||||
|
from kotonebot.client.implements.adb import AdbImpl
|
||||||
|
impl = AdbImpl(connection)
|
||||||
|
d._screenshot = impl
|
||||||
|
d._touch = impl
|
||||||
|
d.commands = impl
|
||||||
|
case 'adb_raw':
|
||||||
|
from kotonebot.client.implements.adb_raw import AdbRawImpl
|
||||||
|
impl = AdbRawImpl(connection)
|
||||||
|
d._screenshot = impl
|
||||||
|
d._touch = impl
|
||||||
|
d.commands = impl
|
||||||
|
case 'uiautomator2':
|
||||||
|
from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
|
||||||
|
from kotonebot.client.implements.adb import AdbImpl
|
||||||
|
impl = UiAutomator2Impl(d)
|
||||||
|
d._screenshot = impl
|
||||||
|
d._touch = impl
|
||||||
|
d.commands = AdbImpl(connection)
|
||||||
|
case _:
|
||||||
|
assert_never(f'Unsupported ADB recipe: {recipe}')
|
||||||
|
return d
|
|
@ -1,22 +1,21 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from psutil import process_iter
|
from psutil import process_iter
|
||||||
from .protocol import Instance, AdbHostConfig
|
from .protocol import Instance, AdbHostConfig, HostProtocol
|
||||||
from typing import ParamSpec, TypeVar, cast
|
from typing import ParamSpec, TypeVar
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
from kotonebot.client import DeviceImpl
|
from kotonebot.client import Device
|
||||||
from kotonebot.client.device import Device
|
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
||||||
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
||||||
from kotonebot.client.implements.adb import AdbImplConfig
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
CustomRecipes = AdbRecipes
|
||||||
|
|
||||||
P = ParamSpec('P')
|
P = ParamSpec('P')
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
class CustomInstance(Instance[AdbHostConfig]):
|
class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
||||||
def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
|
def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.exe_path: str | None = exe_path
|
self.exe_path: str | None = exe_path
|
||||||
|
@ -69,24 +68,12 @@ class CustomInstance(Instance[AdbHostConfig]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_device(self, impl: DeviceImpl, host_config: AdbHostConfig) -> Device:
|
def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
|
||||||
"""为自定义实例创建 Device。"""
|
"""为自定义实例创建 Device。"""
|
||||||
if self.adb_port is None:
|
if self.adb_port is None:
|
||||||
raise ValueError("ADB port is not set and is required.")
|
raise ValueError("ADB port is not set and is required.")
|
||||||
|
|
||||||
# 为 ADB 相关的实现创建配置
|
return super().create_device(impl, host_config)
|
||||||
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
||||||
config = AdbImplConfig(
|
|
||||||
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
||||||
connect=True,
|
|
||||||
disconnect=True,
|
|
||||||
device_serial=self.adb_name,
|
|
||||||
timeout=host_config.timeout
|
|
||||||
)
|
|
||||||
impl = cast(AdbBasedImpl, impl) # make pylance happy
|
|
||||||
return create_device(impl, config)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unsupported device implementation for Custom: {impl}')
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
||||||
|
@ -99,6 +86,25 @@ def _type_check(ins: Instance) -> CustomInstance:
|
||||||
def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
|
def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
|
||||||
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
|
return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
|
||||||
|
|
||||||
|
class CustomHost(HostProtocol[CustomRecipes]):
|
||||||
|
@staticmethod
|
||||||
|
def installed() -> bool:
|
||||||
|
# Custom instances don't have a specific installation requirement
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list() -> list[Instance]:
|
||||||
|
# Custom instances are created manually, not discovered
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def query(*, id: str) -> Instance | None:
|
||||||
|
# Custom instances are created manually, not discovered
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recipes() -> 'list[CustomRecipes]':
|
||||||
|
return ['adb', 'adb_raw', 'uiautomator2']
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
|
ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import cast
|
from typing import Literal
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
from kotonebot.client import DeviceImpl
|
from kotonebot.client import Device
|
||||||
from kotonebot.client.device import Device
|
|
||||||
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
||||||
from kotonebot.client.implements.adb import AdbImplConfig
|
|
||||||
from kotonebot.util import Countdown, Interval
|
from kotonebot.util import Countdown, Interval
|
||||||
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
||||||
|
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
LeidianRecipes = AdbRecipes
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
from ...interop.win.reg import read_reg
|
from ...interop.win.reg import read_reg
|
||||||
|
@ -21,7 +20,7 @@ else:
|
||||||
"""Stub for read_reg on non-Windows platforms."""
|
"""Stub for read_reg on non-Windows platforms."""
|
||||||
return default
|
return default
|
||||||
|
|
||||||
class LeidianInstance(Instance[AdbHostConfig]):
|
class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
||||||
@copy_type(Instance.__init__)
|
@copy_type(Instance.__init__)
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -64,35 +63,23 @@ class LeidianInstance(Instance[AdbHostConfig]):
|
||||||
it = Interval(5)
|
it = Interval(5)
|
||||||
while not cd.expired() and not self.running():
|
while not cd.expired() and not self.running():
|
||||||
it.wait()
|
it.wait()
|
||||||
|
self.refresh()
|
||||||
if not self.running():
|
if not self.running():
|
||||||
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def running(self) -> bool:
|
def running(self) -> bool:
|
||||||
result = LeidianHost._invoke_manager(['isrunning', '--index', str(self.index)])
|
return self.is_running
|
||||||
return result.strip() == 'running'
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_device(self, impl: DeviceImpl, host_config: AdbHostConfig) -> Device:
|
def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
|
||||||
"""为雷电模拟器实例创建 Device。"""
|
"""为雷电模拟器实例创建 Device。"""
|
||||||
if self.adb_port is None:
|
if self.adb_port is None:
|
||||||
raise ValueError("ADB port is not set and is required.")
|
raise ValueError("ADB port is not set and is required.")
|
||||||
|
|
||||||
# 为 ADB 相关的实现创建配置
|
return super().create_device(impl, host_config)
|
||||||
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
||||||
config = AdbImplConfig(
|
|
||||||
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
||||||
connect=False, # 雷电模拟器不需要 adb connect
|
|
||||||
disconnect=False,
|
|
||||||
device_serial=self.adb_name,
|
|
||||||
timeout=host_config.timeout
|
|
||||||
)
|
|
||||||
impl = cast(AdbBasedImpl, impl) # make pylance happy
|
|
||||||
return create_device(impl, config)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unsupported device implementation for Leidian: {impl}')
|
|
||||||
|
|
||||||
class LeidianHost(HostProtocol):
|
class LeidianHost(HostProtocol[LeidianRecipes]):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _read_install_path() -> str | None:
|
def _read_install_path() -> str | None:
|
||||||
|
@ -197,6 +184,10 @@ class LeidianHost(HostProtocol):
|
||||||
return instance
|
return instance
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recipes() -> 'list[LeidianRecipes]':
|
||||||
|
return ['adb', 'adb_raw', 'uiautomator2']
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
||||||
print(LeidianHost._read_install_path())
|
print(LeidianHost._read_install_path())
|
||||||
|
|
|
@ -2,19 +2,17 @@ import os
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any, cast
|
from typing import Any, Literal
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
from kotonebot.client import DeviceImpl, Device
|
from kotonebot.client import Device
|
||||||
from kotonebot.client.device import AndroidDevice
|
from kotonebot.client.device import AndroidDevice
|
||||||
from kotonebot.client.registration import AdbBasedImpl, create_device
|
from kotonebot.client.implements.adb import AdbImpl
|
||||||
from kotonebot.client.implements.adb import AdbImpl, AdbImplConfig, _create_adb_device_base
|
|
||||||
from kotonebot.client.implements.nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
|
from kotonebot.client.implements.nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
|
||||||
from kotonebot.util import Countdown, Interval
|
from kotonebot.util import Countdown, Interval
|
||||||
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
||||||
|
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin, connect_adb
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
from ...interop.win.reg import read_reg
|
from ...interop.win.reg import read_reg
|
||||||
|
@ -23,7 +21,10 @@ else:
|
||||||
"""Stub for read_reg on non-Windows platforms."""
|
"""Stub for read_reg on non-Windows platforms."""
|
||||||
return default
|
return default
|
||||||
|
|
||||||
class Mumu12Instance(Instance[AdbHostConfig]):
|
logger = logging.getLogger(__name__)
|
||||||
|
MuMu12Recipes = AdbRecipes | Literal['nemu_ipc']
|
||||||
|
|
||||||
|
class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
||||||
@copy_type(Instance.__init__)
|
@copy_type(Instance.__init__)
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -75,45 +76,35 @@ class Mumu12Instance(Instance[AdbHostConfig]):
|
||||||
return self.is_android_started
|
return self.is_android_started
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_device(self, impl: DeviceImpl, host_config: AdbHostConfig) -> Device:
|
def create_device(self, impl: MuMu12Recipes, host_config: AdbHostConfig) -> Device:
|
||||||
"""为MuMu12模拟器实例创建 Device。"""
|
"""为MuMu12模拟器实例创建 Device。"""
|
||||||
if self.adb_port is None:
|
if self.adb_port is None:
|
||||||
raise ValueError("ADB port is not set and is required.")
|
raise ValueError("ADB port is not set and is required.")
|
||||||
|
|
||||||
# 新增对 nemu_ipc 的支持
|
|
||||||
if impl == 'nemu_ipc':
|
if impl == 'nemu_ipc':
|
||||||
|
# NemuImpl
|
||||||
nemu_path = Mumu12Host._read_install_path()
|
nemu_path = Mumu12Host._read_install_path()
|
||||||
if not nemu_path:
|
if not nemu_path:
|
||||||
raise RuntimeError("无法找到 MuMu12 的安装路径。")
|
raise RuntimeError("无法找到 MuMu12 的安装路径。")
|
||||||
nemu_config = NemuIpcImplConfig(mumu_root_folder=nemu_path, instance_id=int(self.id))
|
nemu_config = NemuIpcImplConfig(nemu_folder=nemu_path, instance_id=int(self.id))
|
||||||
|
nemu_impl = NemuIpcImpl(nemu_config)
|
||||||
|
# AdbImpl
|
||||||
|
adb_impl = AdbImpl(connect_adb(
|
||||||
|
self.adb_ip,
|
||||||
|
self.adb_port,
|
||||||
|
timeout=host_config.timeout,
|
||||||
|
device_serial=self.adb_name
|
||||||
|
))
|
||||||
device = AndroidDevice()
|
device = AndroidDevice()
|
||||||
nemu_impl = NemuIpcImpl(device, nemu_config)
|
|
||||||
device._screenshot = nemu_impl
|
device._screenshot = nemu_impl
|
||||||
device._touch = nemu_impl
|
device._touch = nemu_impl
|
||||||
|
device.commands = adb_impl
|
||||||
# 组装命令部分 (Adb)
|
|
||||||
adb_config = AdbImplConfig(addr=f'{self.adb_ip}:{self.adb_port}', timeout=host_config.timeout)
|
|
||||||
# adb_impl = AdbImpl(device, adb_config)
|
|
||||||
_d = _create_adb_device_base(adb_config, AdbImpl)
|
|
||||||
device.commands = _d.commands
|
|
||||||
|
|
||||||
return device
|
return device
|
||||||
|
|
||||||
# 为 ADB 相关的实现创建配置
|
|
||||||
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
||||||
config = AdbImplConfig(
|
|
||||||
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
||||||
connect=True,
|
|
||||||
disconnect=True,
|
|
||||||
device_serial=self.adb_name,
|
|
||||||
timeout=host_config.timeout
|
|
||||||
)
|
|
||||||
impl = cast(AdbBasedImpl, impl) # make pylance happy
|
|
||||||
return create_device(impl, config)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unsupported device implementation for MuMu12: {impl}')
|
return super().create_device(impl, host_config)
|
||||||
|
|
||||||
class Mumu12Host(HostProtocol):
|
class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _read_install_path() -> str | None:
|
def _read_install_path() -> str | None:
|
||||||
|
@ -211,6 +202,10 @@ class Mumu12Host(HostProtocol):
|
||||||
return instance
|
return instance
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recipes() -> 'list[MuMu12Recipes]':
|
||||||
|
return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
||||||
print(Mumu12Host._read_install_path())
|
print(Mumu12Host._read_install_path())
|
||||||
|
|
|
@ -195,7 +195,8 @@ class Instance(Generic[T_HostConfig], ABC):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
|
return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
|
||||||
|
|
||||||
class HostProtocol(Protocol):
|
Recipe = TypeVar('Recipe', bound=str)
|
||||||
|
class HostProtocol(Generic[Recipe], Protocol):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def installed() -> bool: ...
|
def installed() -> bool: ...
|
||||||
|
|
||||||
|
@ -205,6 +206,8 @@ class HostProtocol(Protocol):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query(*, id: str) -> Instance | None: ...
|
def query(*, id: str) -> Instance | None: ...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recipes() -> 'list[Recipe]': ...
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
from abc import ABC
|
||||||
|
from typing import Literal
|
||||||
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
from kotonebot import logging
|
||||||
|
from kotonebot.client.device import WindowsDevice
|
||||||
|
from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
WindowsRecipes = Literal['windows', 'remote_windows']
|
||||||
|
|
||||||
|
# Windows 相关的配置类型联合
|
||||||
|
WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
|
||||||
|
|
||||||
|
class CommonWindowsCreateDeviceMixin(ABC):
|
||||||
|
"""
|
||||||
|
通用 Windows 创建设备的 Mixin。
|
||||||
|
该 Mixin 定义了创建 Windows 设备的通用接口。
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
|
||||||
|
"""
|
||||||
|
创建 Windows 设备。
|
||||||
|
"""
|
||||||
|
match recipe:
|
||||||
|
case 'windows':
|
||||||
|
if not isinstance(config, WindowsHostConfig):
|
||||||
|
raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
|
||||||
|
from kotonebot.client.implements.windows import WindowsImpl
|
||||||
|
d = WindowsDevice()
|
||||||
|
impl = WindowsImpl(
|
||||||
|
device=d,
|
||||||
|
window_title=config.window_title,
|
||||||
|
ahk_exe_path=config.ahk_exe_path
|
||||||
|
)
|
||||||
|
d._screenshot = impl
|
||||||
|
d._touch = impl
|
||||||
|
return d
|
||||||
|
case 'remote_windows':
|
||||||
|
if not isinstance(config, RemoteWindowsHostConfig):
|
||||||
|
raise ValueError(f"Expected RemoteWindowsHostConfig for 'remote_windows' recipe, got {type(config)}")
|
||||||
|
from kotonebot.client.implements.remote_windows import RemoteWindowsImpl
|
||||||
|
d = WindowsDevice()
|
||||||
|
impl = RemoteWindowsImpl(
|
||||||
|
device=d,
|
||||||
|
host=config.host,
|
||||||
|
port=config.port
|
||||||
|
)
|
||||||
|
d._screenshot = impl
|
||||||
|
d._touch = impl
|
||||||
|
return d
|
||||||
|
case _:
|
||||||
|
assert_never(f'Unsupported Windows recipe: {recipe}')
|
|
@ -9,7 +9,7 @@ from adbutils._device import AdbDevice as AdbUtilsDevice
|
||||||
|
|
||||||
from ..device import AndroidDevice
|
from ..device import AndroidDevice
|
||||||
from ..protocol import AndroidCommandable, Touchable, Screenshotable
|
from ..protocol import AndroidCommandable, Touchable, Screenshotable
|
||||||
from ..registration import register_impl, ImplConfig
|
from ..registration import ImplConfig
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -83,46 +83,3 @@ class AdbImpl(AndroidCommandable, Touchable, Screenshotable):
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
logger.warning("Swipe duration is not supported with AdbDevice. Ignoring duration.")
|
logger.warning("Swipe duration is not supported with AdbDevice. Ignoring duration.")
|
||||||
self.adb.shell(f"input touchscreen swipe {x1} {y1} {x2} {y2}")
|
self.adb.shell(f"input touchscreen swipe {x1} {y1} {x2} {y2}")
|
||||||
|
|
||||||
|
|
||||||
def _create_adb_device_base(config: AdbImplConfig, impl_class: type) -> AndroidDevice:
|
|
||||||
"""
|
|
||||||
通用的 ADB 设备创建工厂函数。
|
|
||||||
其他任意基于 ADB 的 Impl 可以直接复用这个函数。
|
|
||||||
|
|
||||||
:param config: ADB 实现配置
|
|
||||||
:param impl_class: 实现类或工厂函数。构造函数接收 adb_connection 参数。
|
|
||||||
"""
|
|
||||||
from adbutils import adb
|
|
||||||
|
|
||||||
if config.disconnect:
|
|
||||||
logger.debug('adb disconnect %s', config.addr)
|
|
||||||
adb.disconnect(config.addr)
|
|
||||||
if config.connect:
|
|
||||||
logger.debug('adb connect %s', config.addr)
|
|
||||||
result = adb.connect(config.addr)
|
|
||||||
if 'cannot connect to' in result:
|
|
||||||
raise ValueError(result)
|
|
||||||
serial = config.device_serial or config.addr
|
|
||||||
logger.debug('adb wait for %s', serial)
|
|
||||||
adb.wait_for(serial, timeout=config.timeout)
|
|
||||||
devices = adb.device_list()
|
|
||||||
logger.debug('adb device_list: %s', devices)
|
|
||||||
d = [d for d in devices if d.serial == serial]
|
|
||||||
if len(d) == 0:
|
|
||||||
raise ValueError(f"Device {config.addr} not found")
|
|
||||||
d = d[0]
|
|
||||||
|
|
||||||
device = AndroidDevice(d)
|
|
||||||
impl = impl_class(d)
|
|
||||||
device._touch = impl
|
|
||||||
device._screenshot = impl
|
|
||||||
device.commands = impl
|
|
||||||
|
|
||||||
return device
|
|
||||||
|
|
||||||
|
|
||||||
@register_impl('adb', config_model=AdbImplConfig)
|
|
||||||
def create_adb_device(config: AdbImplConfig) -> AndroidDevice:
|
|
||||||
"""AdbImpl 工厂函数"""
|
|
||||||
return _create_adb_device_base(config, AdbImpl)
|
|
||||||
|
|
|
@ -11,9 +11,8 @@ import numpy as np
|
||||||
from cv2.typing import MatLike
|
from cv2.typing import MatLike
|
||||||
from adbutils._utils import adb_path
|
from adbutils._utils import adb_path
|
||||||
|
|
||||||
from .adb import AdbImpl, AdbImplConfig, _create_adb_device_base
|
from .adb import AdbImpl
|
||||||
from adbutils._device import AdbDevice as AdbUtilsDevice
|
from adbutils._device import AdbDevice as AdbUtilsDevice
|
||||||
from ..registration import register_impl
|
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -158,9 +157,3 @@ class AdbRawImpl(AdbImpl):
|
||||||
data = self.__data
|
data = self.__data
|
||||||
self.__data = None
|
self.__data = None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@register_impl('adb_raw', config_model=AdbImplConfig)
|
|
||||||
def create_adb_raw_device(config: AdbImplConfig):
|
|
||||||
"""AdbRawImpl 工厂函数"""
|
|
||||||
return _create_adb_device_base(config, AdbRawImpl)
|
|
|
@ -11,8 +11,9 @@ from cv2.typing import MatLike
|
||||||
|
|
||||||
from ...device import AndroidDevice, Device
|
from ...device import AndroidDevice, Device
|
||||||
from ...protocol import Touchable, Screenshotable
|
from ...protocol import Touchable, Screenshotable
|
||||||
from ...registration import register_impl, ImplConfig
|
from ...registration import ImplConfig
|
||||||
from .external_renderer_ipc import ExternalRendererIpc
|
from .external_renderer_ipc import ExternalRendererIpc
|
||||||
|
from kotonebot.errors import KotonebotError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,16 +23,23 @@ class NemuIpcIncompatible(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NemuIpcError(Exception):
|
class NemuIpcError(KotonebotError):
|
||||||
"""调用 IPC 过程中发生错误"""
|
"""调用 IPC 过程中发生错误"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NemuIpcImplConfig(ImplConfig):
|
class NemuIpcImplConfig(ImplConfig):
|
||||||
"""nemu_ipc 设备实现的配置模型,mumu_root_folder 为 MuMu 根目录"""
|
r"""nemu_ipc 能力的配置模型。
|
||||||
mumu_root_folder: str
|
|
||||||
|
参数说明:
|
||||||
|
nemu_folder: MuMu12 根目录(如 F:\Apps\Netease\MuMuPlayer-12.0)。
|
||||||
|
instance_id: 模拟器实例 ID。
|
||||||
|
display_id: 目标显示器 ID,默认为 0(主显示器)。
|
||||||
|
"""
|
||||||
|
nemu_folder: str
|
||||||
instance_id: int
|
instance_id: int
|
||||||
|
display_id: int = 0
|
||||||
|
|
||||||
|
|
||||||
class NemuIpcImpl(Touchable, Screenshotable):
|
class NemuIpcImpl(Touchable, Screenshotable):
|
||||||
|
@ -39,23 +47,22 @@ class NemuIpcImpl(Touchable, Screenshotable):
|
||||||
利用 MuMu12 提供的 external_renderer_ipc.dll 进行截图与触摸控制。
|
利用 MuMu12 提供的 external_renderer_ipc.dll 进行截图与触摸控制。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device: Device, config: NemuIpcImplConfig):
|
def __init__(self, config: NemuIpcImplConfig):
|
||||||
self.device = device
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.__width: int = 0
|
self.__width: int = 0
|
||||||
self.__height: int = 0
|
self.__height: int = 0
|
||||||
self.__connected: bool = False
|
self.__connected: bool = False
|
||||||
self._connect_id: int = 0
|
self._connect_id: int = 0
|
||||||
self.display_id: int = 0
|
self.display_id: int = config.display_id
|
||||||
"""
|
"""
|
||||||
显示器 ID。`0` 表示主显示器。
|
显示器 ID。`0` 表示主显示器。
|
||||||
|
|
||||||
如果没有启用「后台保活」功能,一般为主显示器。
|
如果没有启用「后台保活」功能,一般为主显示器。
|
||||||
"""
|
"""
|
||||||
self.nemu_folder = config.mumu_root_folder
|
self.nemu_folder = config.nemu_folder
|
||||||
|
|
||||||
# --------------------------- DLL 封装 ---------------------------
|
# --------------------------- DLL 封装 ---------------------------
|
||||||
self._ipc = ExternalRendererIpc(config.mumu_root_folder)
|
self._ipc = ExternalRendererIpc(config.nemu_folder)
|
||||||
logger.info("ExternalRendererIpc initialized and DLL loaded")
|
logger.info("ExternalRendererIpc initialized and DLL loaded")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -223,15 +230,3 @@ class NemuIpcImpl(Touchable, Screenshotable):
|
||||||
# 最终抬起
|
# 最终抬起
|
||||||
self._ipc.input_touch_up(self._connect_id, self.display_id)
|
self._ipc.input_touch_up(self._connect_id, self.display_id)
|
||||||
sleep(0.01)
|
sleep(0.01)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# 工厂方法
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@register_impl("nemu_ipc", config_model=NemuIpcImplConfig)
|
|
||||||
def create_nemu_ipc_device(config: NemuIpcImplConfig):
|
|
||||||
"""创建一个 AndroidDevice,并挂载 NemuIpcImpl。"""
|
|
||||||
device = AndroidDevice()
|
|
||||||
impl = NemuIpcImpl(device, config)
|
|
||||||
device._touch = impl
|
|
||||||
device._screenshot = impl
|
|
||||||
return device
|
|
|
@ -23,8 +23,8 @@ from cv2.typing import MatLike
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
from ..device import Device, WindowsDevice
|
from ..device import Device, WindowsDevice
|
||||||
from ..protocol import Touchable, Screenshotable
|
from ..protocol import Touchable, Screenshotable
|
||||||
from ..registration import register_impl, ImplConfig
|
from ..registration import ImplConfig
|
||||||
from .windows import WindowsImpl, WindowsImplConfig, create_windows_device
|
from .windows import WindowsImpl, WindowsImplConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -63,7 +63,11 @@ class RemoteWindowsServer:
|
||||||
self.port = port
|
self.port = port
|
||||||
self.server = None
|
self.server = None
|
||||||
self.device = WindowsDevice()
|
self.device = WindowsDevice()
|
||||||
self.impl = create_windows_device(windows_impl_config)
|
self.impl = WindowsImpl(
|
||||||
|
WindowsDevice(),
|
||||||
|
ahk_exe_path=windows_impl_config.ahk_exe_path,
|
||||||
|
window_title=windows_impl_config.window_title
|
||||||
|
)
|
||||||
self.device._screenshot = self.impl
|
self.device._screenshot = self.impl
|
||||||
self.device._touch = self.impl
|
self.device._touch = self.impl
|
||||||
|
|
||||||
|
@ -187,13 +191,3 @@ class RemoteWindowsImpl(Touchable, Screenshotable):
|
||||||
"""Swipe from (x1, y1) to (x2, y2) on the remote server."""
|
"""Swipe from (x1, y1) to (x2, y2) on the remote server."""
|
||||||
if not self.proxy.swipe(x1, y1, x2, y2, duration):
|
if not self.proxy.swipe(x1, y1, x2, y2, duration):
|
||||||
raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
|
raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
|
||||||
|
|
||||||
|
|
||||||
# 编写并注册创建函数
|
|
||||||
@register_impl('remote_windows', config_model=RemoteWindowsImplConfig)
|
|
||||||
def create_remote_windows_device(config: RemoteWindowsImplConfig) -> Device:
|
|
||||||
device = WindowsDevice()
|
|
||||||
remote_impl = RemoteWindowsImpl(device, config.host, config.port)
|
|
||||||
device._touch = remote_impl
|
|
||||||
device._screenshot = remote_impl
|
|
||||||
return device
|
|
|
@ -8,8 +8,6 @@ from cv2.typing import MatLike
|
||||||
from kotonebot import logging
|
from kotonebot import logging
|
||||||
from ..device import Device
|
from ..device import Device
|
||||||
from ..protocol import Screenshotable, Commandable, Touchable
|
from ..protocol import Screenshotable, Commandable, Touchable
|
||||||
from ..registration import register_impl
|
|
||||||
from .adb import AdbImplConfig, _create_adb_device_base
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -85,9 +83,3 @@ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
||||||
滑动屏幕
|
滑动屏幕
|
||||||
"""
|
"""
|
||||||
self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)
|
self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)
|
||||||
|
|
||||||
|
|
||||||
@register_impl('uiautomator2', config_model=AdbImplConfig)
|
|
||||||
def create_uiautomator2_device(config: AdbImplConfig) -> Device:
|
|
||||||
"""UiAutomator2Impl 工厂函数"""
|
|
||||||
return _create_adb_device_base(config, UiAutomator2Impl)
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from cv2.typing import MatLike
|
||||||
|
|
||||||
from ..device import Device, WindowsDevice
|
from ..device import Device, WindowsDevice
|
||||||
from ..protocol import Commandable, Touchable, Screenshotable
|
from ..protocol import Commandable, Touchable, Screenshotable
|
||||||
from ..registration import register_impl, ImplConfig
|
from ..registration import ImplConfig
|
||||||
|
|
||||||
# 1. 定义配置模型
|
# 1. 定义配置模型
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -167,21 +167,6 @@ class WindowsImpl(Touchable, Screenshotable):
|
||||||
# TODO: 这个 speed 的单位是什么?
|
# TODO: 这个 speed 的单位是什么?
|
||||||
self.ahk.mouse_drag(x2, y2, from_position=(x1, y1), coord_mode='Client', speed=10)
|
self.ahk.mouse_drag(x2, y2, from_position=(x1, y1), coord_mode='Client', speed=10)
|
||||||
|
|
||||||
|
|
||||||
# 3. 编写并注册创建函数
|
|
||||||
@register_impl('windows', config_model=WindowsImplConfig)
|
|
||||||
def create_windows_device(config: WindowsImplConfig) -> Device:
|
|
||||||
device = WindowsDevice()
|
|
||||||
impl = WindowsImpl(
|
|
||||||
device,
|
|
||||||
window_title=config.window_title,
|
|
||||||
ahk_exe_path=config.ahk_exe_path
|
|
||||||
)
|
|
||||||
device._touch = impl
|
|
||||||
device._screenshot = impl
|
|
||||||
return device
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..device import Device
|
from ..device import Device
|
||||||
device = Device()
|
device = Device()
|
||||||
|
|
|
@ -22,73 +22,3 @@ class ImplRegistrationError(KotonebotError):
|
||||||
class ImplConfig:
|
class ImplConfig:
|
||||||
"""所有设备实现配置模型的名义上的基类,便于类型约束。"""
|
"""所有设备实现配置模型的名义上的基类,便于类型约束。"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
T_Config = TypeVar("T_Config", bound=ImplConfig)
|
|
||||||
|
|
||||||
# 定义两种创建者函数类型
|
|
||||||
CreatorWithConfig = Callable[[Any], Device]
|
|
||||||
CreatorWithoutConfig = Callable[[], Device]
|
|
||||||
|
|
||||||
# --- 底层 API: 公开的注册表 ---
|
|
||||||
|
|
||||||
# 注册表结构: {'impl_name': (创建函数, 配置模型类 或 None)}
|
|
||||||
DEVICE_CREATORS: Dict[str, tuple[Callable[..., Device], Type[ImplConfig] | None]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def register_impl(name: str, config_model: Type[ImplConfig] | None = None) -> Callable[..., Any]:
|
|
||||||
"""
|
|
||||||
一个统一的装饰器,用于向 DEVICE_CREATORS 注册表中注册一个设备实现。
|
|
||||||
|
|
||||||
:param name: 实现的名称 (e.g., 'windows', 'adb')
|
|
||||||
:param config_model: (可选) 与该实现关联的 dataclass 配置模型
|
|
||||||
"""
|
|
||||||
def decorator(creator_func: Callable[..., Device]) -> Callable[..., Device]:
|
|
||||||
# if name in DEVICE_CREATORS:
|
|
||||||
# raise ImplRegistrationError(f"实现 '{name}' 已被注册。")
|
|
||||||
DEVICE_CREATORS[name] = (creator_func, config_model)
|
|
||||||
return creator_func
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
# --- 高层 API: 带 overload 的便利函数 ---
|
|
||||||
|
|
||||||
# 为需要配置的已知 impl 提供 overload
|
|
||||||
@overload
|
|
||||||
def create_device(impl_name: Literal['windows'], config: 'WindowsImplConfig') -> Device: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def create_device(impl_name: Literal['remote_windows'], config: 'RemoteWindowsImplConfig') -> Device: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def create_device(impl_name: AdbBasedImpl, config: 'AdbImplConfig') -> Device: ...
|
|
||||||
|
|
||||||
# 新增 nemu_ipc overload
|
|
||||||
@overload
|
|
||||||
def create_device(impl_name: Literal['nemu_ipc'], config: 'NemuIpcImplConfig') -> Device: ...
|
|
||||||
|
|
||||||
# 函数的实际实现
|
|
||||||
def create_device(impl_name: DeviceImpl, config: ImplConfig | None = None) -> Device:
|
|
||||||
"""
|
|
||||||
根据名称和可选的配置对象,统一创建设备。
|
|
||||||
"""
|
|
||||||
creator_tuple = DEVICE_CREATORS.get(impl_name)
|
|
||||||
if not creator_tuple:
|
|
||||||
raise ImplRegistrationError(f"未找到名为 '{impl_name}' 的实现。")
|
|
||||||
|
|
||||||
creator_func, registered_config_model = creator_tuple
|
|
||||||
|
|
||||||
# 情况 A: 实现需要配置
|
|
||||||
if registered_config_model is not None:
|
|
||||||
creator_with_config = cast(CreatorWithConfig, creator_func)
|
|
||||||
if config is None:
|
|
||||||
raise ValueError(f"实现 '{impl_name}' 需要一个配置对象,但传入的是 None。")
|
|
||||||
if not isinstance(config, registered_config_model):
|
|
||||||
raise TypeError(f"为 '{impl_name}' 传入的配置类型错误,应为 '{registered_config_model.__name__}',实际为 '{type(config).__name__}'。")
|
|
||||||
return creator_with_config(config)
|
|
||||||
|
|
||||||
# 情况 B: 实现无需配置
|
|
||||||
else:
|
|
||||||
creator_without_config = cast(CreatorWithoutConfig, creator_func)
|
|
||||||
if config is not None:
|
|
||||||
print(f"提示:实现 '{impl_name}' 无需配置,但你提供了一个配置对象,它将被忽略。")
|
|
||||||
return creator_without_config()
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ from typing import Generic, TypeVar, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from kotonebot.client import DeviceImpl
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
BackendType = Literal['custom', 'mumu12', 'leidian', 'dmm']
|
BackendType = Literal['custom', 'mumu12', 'leidian', 'dmm']
|
||||||
|
DeviceRecipes = Literal['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
||||||
|
|
||||||
class ConfigBaseModel(BaseModel):
|
class ConfigBaseModel(BaseModel):
|
||||||
model_config = ConfigDict(use_attribute_docstrings=True)
|
model_config = ConfigDict(use_attribute_docstrings=True)
|
||||||
|
@ -27,7 +27,7 @@ class BackendConfig(ConfigBaseModel):
|
||||||
雷电模拟器需要设置正确的模拟器名,否则 自动启动模拟器 功能将无法正常工作。
|
雷电模拟器需要设置正确的模拟器名,否则 自动启动模拟器 功能将无法正常工作。
|
||||||
其他功能不受影响。
|
其他功能不受影响。
|
||||||
"""
|
"""
|
||||||
screenshot_impl: DeviceImpl = 'adb'
|
screenshot_impl: DeviceRecipes = 'adb'
|
||||||
"""
|
"""
|
||||||
截图方法。暂时推荐使用【adb】截图方式。
|
截图方法。暂时推荐使用【adb】截图方式。
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
from importlib import resources
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
from kotonebot.client import Device, DeviceImpl
|
from kotonebot.client import Device
|
||||||
from kotonebot.client.registration import create_device
|
|
||||||
from kotonebot.client.implements.windows import WindowsImplConfig
|
|
||||||
from kotonebot.client.implements.remote_windows import RemoteWindowsImplConfig
|
|
||||||
from kotonebot.client.host import HostProtocol, Instance
|
from kotonebot.client.host import HostProtocol, Instance
|
||||||
from kotonebot.client.host.protocol import WindowsHostConfig, RemoteWindowsHostConfig
|
from kotonebot.client.host.windows_common import WindowsRecipes, WindowsHostConfigs, CommonWindowsCreateDeviceMixin
|
||||||
|
|
||||||
|
|
||||||
DmmHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
|
DmmHostConfigs = WindowsHostConfigs
|
||||||
|
DmmRecipes = WindowsRecipes
|
||||||
|
|
||||||
# TODO: 可能应该把 start_game 和 end_game 里对启停的操作移动到这里来
|
# TODO: 可能应该把 start_game 和 end_game 里对启停的操作移动到这里来
|
||||||
class DmmInstance(Instance[DmmHostConfigs]):
|
class DmmInstance(CommonWindowsCreateDeviceMixin, Instance[DmmHostConfigs]):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('dmm', 'gakumas')
|
super().__init__('dmm', 'gakumas')
|
||||||
|
|
||||||
|
@ -37,31 +34,11 @@ class DmmInstance(Instance[DmmHostConfigs]):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_device(self, impl: DeviceImpl, host_config: DmmHostConfigs) -> Device:
|
def create_device(self, impl: DmmRecipes, host_config: DmmHostConfigs) -> Device:
|
||||||
if impl == 'windows':
|
"""为 DMM 实例创建 Device。"""
|
||||||
assert isinstance(host_config, WindowsHostConfig)
|
return super().create_device(impl, host_config)
|
||||||
win_config = WindowsImplConfig(
|
|
||||||
window_title=host_config.window_title,
|
|
||||||
ahk_exe_path=host_config.ahk_exe_path
|
|
||||||
)
|
|
||||||
return create_device(impl, win_config)
|
|
||||||
|
|
||||||
elif impl == 'remote_windows':
|
class DmmHost(HostProtocol[DmmRecipes]):
|
||||||
assert isinstance(host_config, RemoteWindowsHostConfig)
|
|
||||||
config = RemoteWindowsImplConfig(
|
|
||||||
windows_impl_config=WindowsImplConfig(
|
|
||||||
window_title=host_config.windows_host_config.window_title,
|
|
||||||
ahk_exe_path=host_config.windows_host_config.ahk_exe_path
|
|
||||||
),
|
|
||||||
host=host_config.host,
|
|
||||||
port=host_config.port
|
|
||||||
)
|
|
||||||
return create_device(impl, config)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unsupported impl for DMM: {impl}')
|
|
||||||
|
|
||||||
class DmmHost(HostProtocol):
|
|
||||||
instance = DmmInstance()
|
instance = DmmInstance()
|
||||||
"""DmmInstance 单例。"""
|
"""DmmInstance 单例。"""
|
||||||
|
|
||||||
|
@ -77,3 +54,7 @@ class DmmHost(HostProtocol):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query(*, id: str) -> Instance | None:
|
def query(*, id: str) -> Instance | None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recipes() -> 'list[DmmRecipes]':
|
||||||
|
return ['windows', 'remote_windows']
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, cast
|
||||||
import zipfile
|
import zipfile
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -9,8 +10,6 @@ from typing_extensions import override
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
from kotonebot.client.implements.nemu_ipc import NemuIpcImplConfig
|
|
||||||
|
|
||||||
from ...client import Device
|
from ...client import Device
|
||||||
from kotonebot.ui import user
|
from kotonebot.ui import user
|
||||||
from kotonebot import KotoneBot
|
from kotonebot import KotoneBot
|
||||||
|
@ -234,7 +233,10 @@ class Kaa(KotoneBot):
|
||||||
elif isinstance(self.backend_instance, (CustomInstance, Mumu12Instance, LeidianInstance)):
|
elif isinstance(self.backend_instance, (CustomInstance, Mumu12Instance, LeidianInstance)):
|
||||||
if impl_name in ['adb', 'adb_raw', 'uiautomator2'] or (impl_name == 'nemu_ipc' and isinstance(self.backend_instance, Mumu12Instance)):
|
if impl_name in ['adb', 'adb_raw', 'uiautomator2'] or (impl_name == 'nemu_ipc' and isinstance(self.backend_instance, Mumu12Instance)):
|
||||||
host_conf = AdbHostConfig(timeout=180)
|
host_conf = AdbHostConfig(timeout=180)
|
||||||
return self.backend_instance.create_device(impl_name, host_conf)
|
return self.backend_instance.create_device(
|
||||||
|
cast(Any, impl_name), # :(
|
||||||
|
host_conf
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"{user_config.backend.type} backend does not support implementation '{impl_name}'")
|
raise ValueError(f"{user_config.backend.type} backend does not support implementation '{impl_name}'")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue