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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -302
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +49 -56
  6. kotonebot/backend/context/task_action.py +183 -175
  7. kotonebot/backend/core.py +129 -126
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/loop.py +12 -88
  15. kotonebot/backend/ocr.py +535 -529
  16. kotonebot/backend/preprocessor.py +103 -103
  17. kotonebot/client/__init__.py +9 -9
  18. kotonebot/client/device.py +528 -502
  19. kotonebot/client/fast_screenshot.py +377 -377
  20. kotonebot/client/host/__init__.py +43 -12
  21. kotonebot/client/host/adb_common.py +107 -94
  22. kotonebot/client/host/custom.py +118 -114
  23. kotonebot/client/host/leidian_host.py +196 -201
  24. kotonebot/client/host/mumu12_host.py +353 -358
  25. kotonebot/client/host/protocol.py +214 -213
  26. kotonebot/client/host/windows_common.py +58 -55
  27. kotonebot/client/implements/__init__.py +71 -7
  28. kotonebot/client/implements/adb.py +89 -85
  29. kotonebot/client/implements/adb_raw.py +162 -158
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -192
  34. kotonebot/client/implements/uiautomator2.py +85 -81
  35. kotonebot/client/implements/windows.py +176 -168
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/config/base_config.py +96 -96
  39. kotonebot/config/manager.py +36 -36
  40. kotonebot/errors.py +76 -71
  41. kotonebot/interop/win/__init__.py +10 -0
  42. kotonebot/interop/win/_mouse.py +311 -0
  43. kotonebot/interop/win/message_box.py +313 -313
  44. kotonebot/interop/win/reg.py +37 -37
  45. kotonebot/interop/win/shortcut.py +43 -43
  46. kotonebot/interop/win/task_dialog.py +513 -469
  47. kotonebot/logging/__init__.py +2 -2
  48. kotonebot/logging/log.py +17 -17
  49. kotonebot/primitives/__init__.py +17 -17
  50. kotonebot/primitives/geometry.py +862 -290
  51. kotonebot/primitives/visual.py +63 -63
  52. kotonebot/tools/mirror.py +354 -354
  53. kotonebot/ui/file_host/sensio.py +36 -36
  54. kotonebot/ui/file_host/tmp_send.py +54 -54
  55. kotonebot/ui/pushkit/__init__.py +3 -3
  56. kotonebot/ui/pushkit/image_host.py +88 -87
  57. kotonebot/ui/pushkit/protocol.py +13 -13
  58. kotonebot/ui/pushkit/wxpusher.py +54 -53
  59. kotonebot/ui/user.py +148 -143
  60. kotonebot/util.py +436 -409
  61. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
  62. kotonebot-0.5.0.dist-info/RECORD +71 -0
  63. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  64. kotonebot-0.3.1.dist-info/RECORD +0 -70
  65. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  66. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,193 +1,189 @@
1
- """
2
- Remote Windows implementation using XML-RPC.
3
-
4
- This module provides:
5
- 1. RemoteWindowsImpl - Client implementation that connects to a remote Windows machine
6
- 2. RemoteWindowsServer - Server implementation that exposes a WindowsImpl instance via XML-RPC
7
- """
8
-
9
- import io
10
- import base64
11
- import logging
12
- import xmlrpc.client
13
- import xmlrpc.server
14
- from typing import Literal, cast, Any, Tuple
15
- from functools import cached_property
16
- from threading import Thread
17
- from dataclasses import dataclass
18
-
19
- import cv2
20
- import numpy as np
21
- from cv2.typing import MatLike
22
-
23
- from kotonebot import logging
24
- from ..device import Device, WindowsDevice
25
- from ..protocol import Touchable, Screenshotable
26
- from ..registration import ImplConfig
27
- from .windows import WindowsImpl, WindowsImplConfig
28
-
29
- logger = logging.getLogger(__name__)
30
-
31
- # 定义配置模型
32
- @dataclass
33
- class RemoteWindowsImplConfig(ImplConfig):
34
- windows_impl_config: WindowsImplConfig
35
- host: str = "localhost"
36
- port: int = 8000
37
-
38
- def _encode_image(image: MatLike) -> str:
39
- """Encode an image as a base64 string."""
40
- success, buffer = cv2.imencode('.png', image)
41
- if not success:
42
- raise RuntimeError("Failed to encode image")
43
- return base64.b64encode(buffer.tobytes()).decode('ascii')
44
-
45
- def _decode_image(encoded_image: str) -> MatLike:
46
- """Decode a base64 string to an image."""
47
- buffer = base64.b64decode(encoded_image)
48
- image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
49
- if image is None:
50
- raise RuntimeError("Failed to decode image")
51
- return image
52
-
53
- class RemoteWindowsServer:
54
- """
55
- XML-RPC server that exposes a WindowsImpl instance.
56
-
57
- This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
58
- """
59
-
60
- def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
61
- """Initialize the server with the given host and port."""
62
- self.host = host
63
- self.port = port
64
- self.server = None
65
- self.device = WindowsDevice()
66
- self.impl = WindowsImpl(
67
- WindowsDevice(),
68
- ahk_exe_path=windows_impl_config.ahk_exe_path,
69
- window_title=windows_impl_config.window_title
70
- )
71
- self.device._screenshot = self.impl
72
- self.device._touch = self.impl
73
-
74
- def start(self):
75
- """Start the XML-RPC server."""
76
- self.server = xmlrpc.server.SimpleXMLRPCServer(
77
- (self.host, self.port),
78
- logRequests=True,
79
- allow_none=True
80
- )
81
- self.server.register_instance(self)
82
- logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
83
- self.server.serve_forever()
84
-
85
- def start_in_thread(self):
86
- """Start the XML-RPC server in a separate thread."""
87
- thread = Thread(target=self.start, daemon=True)
88
- thread.start()
89
- return thread
90
-
91
- # Screenshotable methods
92
-
93
- def screenshot(self) -> str:
94
- """Take a screenshot and return it as a base64-encoded string."""
95
- try:
96
- image = self.impl.screenshot()
97
- return _encode_image(image)
98
- except Exception as e:
99
- logger.error(f"Error taking screenshot: {e}")
100
- raise
101
-
102
- def get_screen_size(self) -> tuple[int, int]:
103
- """Get the screen size."""
104
- return self.impl.screen_size
105
-
106
- def detect_orientation(self) -> str | None:
107
- """Detect the screen orientation."""
108
- return self.impl.detect_orientation()
109
-
110
- # Touchable methods
111
-
112
- def click(self, x: int, y: int) -> bool:
113
- """Click at the given coordinates."""
114
- try:
115
- self.impl.click(x, y)
116
- return True
117
- except Exception as e:
118
- logger.error(f"Error clicking at ({x}, {y}): {e}")
119
- return False
120
-
121
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
122
- """Swipe from (x1, y1) to (x2, y2)."""
123
- try:
124
- self.impl.swipe(x1, y1, x2, y2, duration)
125
- return True
126
- except Exception as e:
127
- logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
128
- return False
129
-
130
- # Other methods
131
-
132
- def get_scale_ratio(self) -> float:
133
- """Get the scale ratio."""
134
- return self.impl.scale_ratio
135
-
136
- def ping(self) -> bool:
137
- """Check if the server is alive."""
138
- return True
139
-
140
-
141
- class RemoteWindowsImpl(Touchable, Screenshotable):
142
- """
143
- Client implementation that connects to a remote Windows machine via XML-RPC.
144
-
145
- This class implements the same interfaces as WindowsImpl but forwards all
146
- method calls to a remote server.
147
- """
148
-
149
- def __init__(self, device: Device, host="localhost", port=8000):
150
- """Initialize the client with the given device, host, and port."""
151
- self.device = device
152
- self.host = host
153
- self.port = port
154
- self.proxy = xmlrpc.client.ServerProxy(
155
- f"http://{host}:{port}/",
156
- allow_none=True
157
- )
158
- # Test connection
159
- try:
160
- if not self.proxy.ping():
161
- raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
162
- logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
163
- except Exception as e:
164
- raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
165
-
166
- @cached_property
167
- def scale_ratio(self) -> float:
168
- """Get the scale ratio from the remote server."""
169
- return cast(float, self.proxy.get_scale_ratio())
170
-
171
- @property
172
- def screen_size(self) -> tuple[int, int]:
173
- """Get the screen size from the remote server."""
174
- return cast(Tuple[int, int], self.proxy.get_screen_size())
175
-
176
- def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
177
- """Detect the screen orientation from the remote server."""
178
- return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
179
-
180
- def screenshot(self) -> MatLike:
181
- """Take a screenshot from the remote server."""
182
- encoded_image = cast(str, self.proxy.screenshot())
183
- return _decode_image(encoded_image)
184
-
185
- def click(self, x: int, y: int) -> None:
186
- """Click at the given coordinates on the remote server."""
187
- if not self.proxy.click(x, y):
188
- raise RuntimeError(f"Failed to click at ({x}, {y})")
189
-
190
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
191
- """Swipe from (x1, y1) to (x2, y2) on the remote server."""
192
- if not self.proxy.swipe(x1, y1, x2, y2, duration):
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('"RemoteWindowsImpl" implementation')
4
+
5
+ import io
6
+ import base64
7
+ import logging
8
+ import xmlrpc.client
9
+ import xmlrpc.server
10
+ from typing import Literal, cast, Any, Tuple
11
+ from functools import cached_property
12
+ from threading import Thread
13
+ from dataclasses import dataclass
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from cv2.typing import MatLike
18
+
19
+ from kotonebot import logging
20
+ from ..device import Device, WindowsDevice
21
+ from ..protocol import Touchable, Screenshotable
22
+ from ..registration import ImplConfig
23
+ from .windows import WindowsImpl, WindowsImplConfig
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # 定义配置模型
28
+ @dataclass
29
+ class RemoteWindowsImplConfig(ImplConfig):
30
+ windows_impl_config: WindowsImplConfig
31
+ host: str = "localhost"
32
+ port: int = 8000
33
+
34
+ def _encode_image(image: MatLike) -> str:
35
+ """Encode an image as a base64 string."""
36
+ success, buffer = cv2.imencode('.png', image)
37
+ if not success:
38
+ raise RuntimeError("Failed to encode image")
39
+ return base64.b64encode(buffer.tobytes()).decode('ascii')
40
+
41
+ def _decode_image(encoded_image: str) -> MatLike:
42
+ """Decode a base64 string to an image."""
43
+ buffer = base64.b64decode(encoded_image)
44
+ image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
45
+ if image is None:
46
+ raise RuntimeError("Failed to decode image")
47
+ return image
48
+
49
+ class RemoteWindowsServer:
50
+ """
51
+ XML-RPC server that exposes a WindowsImpl instance.
52
+
53
+ This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
54
+ """
55
+
56
+ def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
57
+ """Initialize the server with the given host and port."""
58
+ self.host = host
59
+ self.port = port
60
+ self.server = None
61
+ self.device = WindowsDevice()
62
+ self.impl = WindowsImpl(
63
+ WindowsDevice(),
64
+ ahk_exe_path=windows_impl_config.ahk_exe_path,
65
+ window_title=windows_impl_config.window_title
66
+ )
67
+ self.device._screenshot = self.impl
68
+ self.device._touch = self.impl
69
+
70
+ def start(self):
71
+ """Start the XML-RPC server."""
72
+ self.server = xmlrpc.server.SimpleXMLRPCServer(
73
+ (self.host, self.port),
74
+ logRequests=True,
75
+ allow_none=True
76
+ )
77
+ self.server.register_instance(self)
78
+ logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
79
+ self.server.serve_forever()
80
+
81
+ def start_in_thread(self):
82
+ """Start the XML-RPC server in a separate thread."""
83
+ thread = Thread(target=self.start, daemon=True)
84
+ thread.start()
85
+ return thread
86
+
87
+ # Screenshotable methods
88
+
89
+ def screenshot(self) -> str:
90
+ """Take a screenshot and return it as a base64-encoded string."""
91
+ try:
92
+ image = self.impl.screenshot()
93
+ return _encode_image(image)
94
+ except Exception as e:
95
+ logger.error(f"Error taking screenshot: {e}")
96
+ raise
97
+
98
+ def get_screen_size(self) -> tuple[int, int]:
99
+ """Get the screen size."""
100
+ return self.impl.screen_size
101
+
102
+ def detect_orientation(self) -> str | None:
103
+ """Detect the screen orientation."""
104
+ return self.impl.detect_orientation()
105
+
106
+ # Touchable methods
107
+
108
+ def click(self, x: int, y: int) -> bool:
109
+ """Click at the given coordinates."""
110
+ try:
111
+ self.impl.click(x, y)
112
+ return True
113
+ except Exception as e:
114
+ logger.error(f"Error clicking at ({x}, {y}): {e}")
115
+ return False
116
+
117
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
118
+ """Swipe from (x1, y1) to (x2, y2)."""
119
+ try:
120
+ self.impl.swipe(x1, y1, x2, y2, duration)
121
+ return True
122
+ except Exception as e:
123
+ logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
124
+ return False
125
+
126
+ # Other methods
127
+
128
+ def get_scale_ratio(self) -> float:
129
+ """Get the scale ratio."""
130
+ return self.impl.scale_ratio
131
+
132
+ def ping(self) -> bool:
133
+ """Check if the server is alive."""
134
+ return True
135
+
136
+
137
+ class RemoteWindowsImpl(Touchable, Screenshotable):
138
+ """
139
+ Client implementation that connects to a remote Windows machine via XML-RPC.
140
+
141
+ This class implements the same interfaces as WindowsImpl but forwards all
142
+ method calls to a remote server.
143
+ """
144
+
145
+ def __init__(self, device: Device, host="localhost", port=8000):
146
+ """Initialize the client with the given device, host, and port."""
147
+ self.device = device
148
+ self.host = host
149
+ self.port = port
150
+ self.proxy = xmlrpc.client.ServerProxy(
151
+ f"http://{host}:{port}/",
152
+ allow_none=True
153
+ )
154
+ # Test connection
155
+ try:
156
+ if not self.proxy.ping():
157
+ raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
158
+ logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
159
+ except Exception as e:
160
+ raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
161
+
162
+ @cached_property
163
+ def scale_ratio(self) -> float:
164
+ """Get the scale ratio from the remote server."""
165
+ return cast(float, self.proxy.get_scale_ratio())
166
+
167
+ @property
168
+ def screen_size(self) -> tuple[int, int]:
169
+ """Get the screen size from the remote server."""
170
+ return cast(Tuple[int, int], self.proxy.get_screen_size())
171
+
172
+ def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
173
+ """Detect the screen orientation from the remote server."""
174
+ return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
175
+
176
+ def screenshot(self) -> MatLike:
177
+ """Take a screenshot from the remote server."""
178
+ encoded_image = cast(str, self.proxy.screenshot())
179
+ return _decode_image(encoded_image)
180
+
181
+ def click(self, x: int, y: int) -> None:
182
+ """Click at the given coordinates on the remote server."""
183
+ if not self.proxy.click(x, y):
184
+ raise RuntimeError(f"Failed to click at ({x}, {y})")
185
+
186
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
187
+ """Swipe from (x1, y1) to (x2, y2) on the remote server."""
188
+ if not self.proxy.swipe(x1, y1, x2, y2, duration):
193
189
  raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
@@ -1,82 +1,86 @@
1
- import time
2
- from typing import Literal
3
-
4
- import numpy as np
5
- import uiautomator2 as u2
6
- from cv2.typing import MatLike
7
- from adbutils._device import AdbDevice as AdbUtilsDevice
8
-
9
- from kotonebot import logging
10
- from ..device import Device
11
- from ..protocol import Screenshotable, Commandable, Touchable
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- SCREENSHOT_INTERVAL = 0.2
16
-
17
- class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
18
- def __init__(self, adb_connection: AdbUtilsDevice):
19
- self.u2_client = u2.Device(adb_connection.serial)
20
- self.__last_screenshot_time = 0
21
-
22
- def screenshot(self) -> MatLike:
23
- """
24
- 截图
25
- """
26
- from kotonebot import sleep
27
- delta = time.time() - self.__last_screenshot_time
28
- if delta < SCREENSHOT_INTERVAL:
29
- time.sleep(SCREENSHOT_INTERVAL - delta)
30
- start_time = time.time()
31
- image = self.u2_client.screenshot(format='opencv')
32
- logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
33
- self.__last_screenshot_time = time.time()
34
- assert isinstance(image, np.ndarray)
35
- return image
36
-
37
- @property
38
- def screen_size(self) -> tuple[int, int]:
39
- info = self.u2_client.info
40
- sizes = info['displayWidth'], info['displayHeight']
41
- return sizes
42
-
43
- def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
44
- """
45
- 检测设备方向
46
- """
47
- orientation = self.u2_client.info['displayRotation']
48
- if orientation == 1:
49
- return 'portrait'
50
- elif orientation == 0:
51
- return 'landscape'
52
- else:
53
- return None
54
-
55
- def launch_app(self, package_name: str) -> None:
56
- """
57
- 启动应用
58
- """
59
- self.u2_client.app_start(package_name)
60
-
61
- def current_package(self) -> str | None:
62
- """
63
- 获取当前应用包名
64
- """
65
- try:
66
- result = self.u2_client.app_current()
67
- logger.verbose(f'uiautomator2 current_package: {result}')
68
- return result['package']
69
- except:
70
- return None
71
-
72
- def click(self, x: int, y: int) -> None:
73
- """
74
- 点击屏幕
75
- """
76
- self.u2_client.click(x, y)
77
-
78
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
79
- """
80
- 滑动屏幕
81
- """
1
+ import time
2
+ from typing import Literal
3
+
4
+ import numpy as np
5
+ try:
6
+ import uiautomator2 as u2
7
+ from adbutils._device import AdbDevice as AdbUtilsDevice
8
+ except ImportError as _e:
9
+ from kotonebot.errors import MissingDependencyError
10
+ raise MissingDependencyError(_e, 'android')
11
+ from cv2.typing import MatLike
12
+
13
+ from kotonebot import logging
14
+ from ..device import Device
15
+ from ..protocol import Screenshotable, Commandable, Touchable
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ SCREENSHOT_INTERVAL = 0.2
20
+
21
+ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
22
+ def __init__(self, adb_connection: AdbUtilsDevice):
23
+ self.u2_client = u2.Device(adb_connection.serial)
24
+ self.__last_screenshot_time = 0
25
+
26
+ def screenshot(self) -> MatLike:
27
+ """
28
+ 截图
29
+ """
30
+ from kotonebot import sleep
31
+ delta = time.time() - self.__last_screenshot_time
32
+ if delta < SCREENSHOT_INTERVAL:
33
+ time.sleep(SCREENSHOT_INTERVAL - delta)
34
+ start_time = time.time()
35
+ image = self.u2_client.screenshot(format='opencv')
36
+ logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
37
+ self.__last_screenshot_time = time.time()
38
+ assert isinstance(image, np.ndarray)
39
+ return image
40
+
41
+ @property
42
+ def screen_size(self) -> tuple[int, int]:
43
+ info = self.u2_client.info
44
+ sizes = info['displayWidth'], info['displayHeight']
45
+ return sizes
46
+
47
+ def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
48
+ """
49
+ 检测设备方向
50
+ """
51
+ orientation = self.u2_client.info['displayRotation']
52
+ if orientation == 1:
53
+ return 'portrait'
54
+ elif orientation == 0:
55
+ return 'landscape'
56
+ else:
57
+ return None
58
+
59
+ def launch_app(self, package_name: str) -> None:
60
+ """
61
+ 启动应用
62
+ """
63
+ self.u2_client.app_start(package_name)
64
+
65
+ def current_package(self) -> str | None:
66
+ """
67
+ 获取当前应用包名
68
+ """
69
+ try:
70
+ result = self.u2_client.app_current()
71
+ logger.verbose(f'uiautomator2 current_package: {result}')
72
+ return result['package']
73
+ except:
74
+ return None
75
+
76
+ def click(self, x: int, y: int) -> None:
77
+ """
78
+ 点击屏幕
79
+ """
80
+ self.u2_client.click(x, y)
81
+
82
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
83
+ """
84
+ 滑动屏幕
85
+ """
82
86
  self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)